fix(auth): SDK'yı kaldırıp ince REST katmanına geç (lib/appwrite-rest.ts)
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
This commit is contained in:
+6
-9
@@ -1,7 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { ID } from "appwrite";
|
import { DATABASE_ID, ID, TABLES, tablesDB } from "@/lib/appwrite-rest";
|
||||||
import { publicDB, DATABASE_ID, TABLES } from "@/lib/appwrite-server";
|
|
||||||
|
|
||||||
export type ContactFormState = {
|
export type ContactFormState = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -34,20 +33,18 @@ export async function submitContact(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await publicDB.createRow({
|
await tablesDB.createRow(DATABASE_ID, TABLES.contactMessages, ID.unique(), {
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.contactMessages,
|
|
||||||
rowId: ID.unique(),
|
|
||||||
data: {
|
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
phone: phone || null,
|
phone: phone || null,
|
||||||
subject: subject || null,
|
subject: subject || null,
|
||||||
message,
|
message,
|
||||||
status: "new",
|
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) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : "Bilinmeyen hata";
|
const detail = err instanceof Error ? err.message : "Bilinmeyen hata";
|
||||||
return { ok: false, message: `Kayıt başarısız: ${detail}` };
|
return { ok: false, message: `Kayıt başarısız: ${detail}` };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getRow } from "@/lib/data";
|
import { getRow } from "@/lib/data";
|
||||||
import { TABLES } from "@/lib/appwrite-server";
|
import { TABLES } from "@/lib/appwrite-rest";
|
||||||
import type { BlogPostRow } from "@/lib/types";
|
import type { BlogPostRow } from "@/lib/types";
|
||||||
import { BlogForm } from "../../form";
|
import { BlogForm } from "../../form";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getRow } from "@/lib/data";
|
import { getRow } from "@/lib/data";
|
||||||
import { TABLES } from "@/lib/appwrite-server";
|
import { TABLES } from "@/lib/appwrite-rest";
|
||||||
import type { ServiceRow } from "@/lib/types";
|
import type { ServiceRow } from "@/lib/types";
|
||||||
import { ServiceForm } from "../../form";
|
import { ServiceForm } from "../../form";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { PageHeader } from "@/components/admin/form";
|
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 { requireSessionSecret } from "@/lib/auth";
|
||||||
import { Query } from "appwrite";
|
|
||||||
import { Upload } from "lucide-react";
|
import { Upload } from "lucide-react";
|
||||||
import { DeleteButton } from "@/components/admin/delete-button";
|
import { DeleteButton } from "@/components/admin/delete-button";
|
||||||
import { uploadMedia, deleteMediaFile } from "@/lib/admin-actions";
|
import { uploadMedia, deleteMediaFile } from "@/lib/admin-actions";
|
||||||
@@ -9,10 +8,11 @@ import { uploadMedia, deleteMediaFile } from "@/lib/admin-actions";
|
|||||||
async function listFiles() {
|
async function listFiles() {
|
||||||
try {
|
try {
|
||||||
const secret = await requireSessionSecret();
|
const secret = await requireSessionSecret();
|
||||||
const res = await userStorage(secret).listFiles({
|
const res = await storage.listFiles(
|
||||||
bucketId: MEDIA_BUCKET_ID,
|
MEDIA_BUCKET_ID,
|
||||||
queries: [Query.orderDesc("$createdAt"), Query.limit(100)],
|
[Q.orderDesc("$createdAt"), Q.limit(100)],
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
return res.files;
|
return res.files;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
@@ -20,7 +20,7 @@ async function listFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fileViewUrl(id: string) {
|
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() {
|
export default async function MediaPage() {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { Query } from "appwrite";
|
import { DATABASE_ID, Q, TABLES, tablesDB } from "@/lib/appwrite-rest";
|
||||||
import { DATABASE_ID, TABLES, userDB } from "@/lib/appwrite-server";
|
|
||||||
import { requireSessionSecret } from "@/lib/auth";
|
import { requireSessionSecret } from "@/lib/auth";
|
||||||
|
|
||||||
async function safeCount(tableId: string, queries: string[] = []) {
|
async function safeCount(tableId: string, queries: string[] = []) {
|
||||||
try {
|
try {
|
||||||
const secret = await requireSessionSecret();
|
const secret = await requireSessionSecret();
|
||||||
const res = await userDB(secret).listRows({
|
const res = await tablesDB.listRows(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId,
|
tableId,
|
||||||
queries: [...queries, Query.limit(1)],
|
[...queries, Q.limit(1)],
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
return res.total ?? 0;
|
return res.total ?? 0;
|
||||||
} catch {
|
} catch {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -21,12 +21,12 @@ async function safeCount(tableId: string, queries: string[] = []) {
|
|||||||
export default async function AdminDashboard() {
|
export default async function AdminDashboard() {
|
||||||
const [posts, drafts, services, projects, testimonials, newMessages, totalMessages] =
|
const [posts, drafts, services, projects, testimonials, newMessages, totalMessages] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
safeCount(TABLES.blogPosts, [Query.equal("status", "published")]),
|
safeCount(TABLES.blogPosts, [Q.equal("status", "published")]),
|
||||||
safeCount(TABLES.blogPosts, [Query.equal("status", "draft")]),
|
safeCount(TABLES.blogPosts, [Q.equal("status", "draft")]),
|
||||||
safeCount(TABLES.services),
|
safeCount(TABLES.services),
|
||||||
safeCount(TABLES.projects),
|
safeCount(TABLES.projects),
|
||||||
safeCount(TABLES.testimonials),
|
safeCount(TABLES.testimonials),
|
||||||
safeCount(TABLES.contactMessages, [Query.equal("status", "new")]),
|
safeCount(TABLES.contactMessages, [Q.equal("status", "new")]),
|
||||||
safeCount(TABLES.contactMessages),
|
safeCount(TABLES.contactMessages),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getRow } from "@/lib/data";
|
import { getRow } from "@/lib/data";
|
||||||
import { TABLES } from "@/lib/appwrite-server";
|
import { TABLES } from "@/lib/appwrite-rest";
|
||||||
import type { ProjectRow } from "@/lib/types";
|
import type { ProjectRow } from "@/lib/types";
|
||||||
import { ProjectForm } from "../../form";
|
import { ProjectForm } from "../../form";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getRow } from "@/lib/data";
|
import { getRow } from "@/lib/data";
|
||||||
import { TABLES } from "@/lib/appwrite-server";
|
import { TABLES } from "@/lib/appwrite-rest";
|
||||||
import type { TestimonialRow } from "@/lib/types";
|
import type { TestimonialRow } from "@/lib/types";
|
||||||
import { TestimonialForm } from "../../form";
|
import { TestimonialForm } from "../../form";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getRow } from "@/lib/data";
|
import { getRow } from "@/lib/data";
|
||||||
import { TABLES } from "@/lib/appwrite-server";
|
import { TABLES } from "@/lib/appwrite-rest";
|
||||||
import type { SeoPageRow } from "@/lib/types";
|
import type { SeoPageRow } from "@/lib/types";
|
||||||
import { SeoPageForm } from "../../page-form";
|
import { SeoPageForm } from "../../page-form";
|
||||||
|
|
||||||
|
|||||||
+31
-11
@@ -2,14 +2,27 @@
|
|||||||
|
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Account, Client } from "appwrite";
|
import { account, AppwriteError } from "@/lib/appwrite-rest";
|
||||||
import { SESSION_COOKIE } from "@/lib/auth";
|
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 };
|
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(
|
export async function loginAction(
|
||||||
_prev: LoginState | undefined,
|
_prev: LoginState | undefined,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
@@ -19,10 +32,13 @@ export async function loginAction(
|
|||||||
|
|
||||||
if (!email || !password) return { error: "E-posta ve şifre zorunlu" };
|
if (!email || !password) return { error: "E-posta ve şifre zorunlu" };
|
||||||
|
|
||||||
|
let session;
|
||||||
try {
|
try {
|
||||||
const client = new Client().setEndpoint(endpoint).setProject(projectId);
|
session = await account.createEmailPasswordSession(email, password);
|
||||||
const account = new Account(client);
|
} catch (err) {
|
||||||
const session = await account.createEmailPasswordSession({ email, password });
|
return { error: friendly(err) };
|
||||||
|
}
|
||||||
|
|
||||||
const store = await cookies();
|
const store = await cookies();
|
||||||
store.set(SESSION_COOKIE, session.secret, {
|
store.set(SESSION_COOKIE, session.secret, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
@@ -31,16 +47,20 @@ export async function loginAction(
|
|||||||
path: "/",
|
path: "/",
|
||||||
expires: new Date(session.expire),
|
expires: new Date(session.expire),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : "Giriş başarısız";
|
|
||||||
return { error: msg };
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect("/admin");
|
redirect("/admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logoutAction() {
|
export async function logoutAction() {
|
||||||
const store = await cookies();
|
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);
|
store.delete(SESSION_COOKIE);
|
||||||
redirect("/admin/login");
|
redirect("/admin/login");
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-140
@@ -1,25 +1,16 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { ID } from "appwrite";
|
|
||||||
import {
|
import {
|
||||||
DATABASE_ID,
|
DATABASE_ID,
|
||||||
|
ID,
|
||||||
MEDIA_BUCKET_ID,
|
MEDIA_BUCKET_ID,
|
||||||
|
storage,
|
||||||
TABLES,
|
TABLES,
|
||||||
userDB,
|
tablesDB,
|
||||||
userStorage,
|
} from "@/lib/appwrite-rest";
|
||||||
} from "@/lib/appwrite-server";
|
|
||||||
import { requireSessionSecret } from "@/lib/auth";
|
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) {
|
function slugify(s: string) {
|
||||||
return s
|
return s
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -55,37 +46,34 @@ function strArr(v: FormDataEntryValue | null) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Media upload ────────────────────────────────────────────────
|
// ─── Media ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function uploadMedia(formData: FormData): Promise<void> {
|
export async function uploadMedia(formData: FormData): Promise<void> {
|
||||||
|
const secret = await requireSessionSecret();
|
||||||
const file = formData.get("file");
|
const file = formData.get("file");
|
||||||
if (!(file instanceof File) || file.size === 0) {
|
if (!(file instanceof File) || file.size === 0) {
|
||||||
throw new Error("Dosya seçilmedi");
|
throw new Error("Dosya seçilmedi");
|
||||||
}
|
}
|
||||||
const s = await storage();
|
await storage.createFile(MEDIA_BUCKET_ID, ID.unique(), file, secret);
|
||||||
await s.createFile({
|
|
||||||
bucketId: MEDIA_BUCKET_ID,
|
|
||||||
fileId: ID.unique(),
|
|
||||||
file,
|
|
||||||
});
|
|
||||||
revalidatePath("/admin/medya");
|
revalidatePath("/admin/medya");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMediaFile(fileId: string) {
|
export async function deleteMediaFile(fileId: string) {
|
||||||
const s = await storage();
|
const secret = await requireSessionSecret();
|
||||||
await s.deleteFile({ bucketId: MEDIA_BUCKET_ID, fileId });
|
await storage.deleteFile(MEDIA_BUCKET_ID, fileId, secret);
|
||||||
revalidatePath("/admin/medya");
|
revalidatePath("/admin/medya");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Blog ────────────────────────────────────────────────────────
|
// ─── Blog ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveBlogPost(formData: FormData) {
|
export async function saveBlogPost(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = str(formData.get("id"));
|
const id = str(formData.get("id"));
|
||||||
const title = str(formData.get("title"));
|
const title = str(formData.get("title"));
|
||||||
if (!title) throw new Error("Başlık zorunlu");
|
if (!title) throw new Error("Başlık zorunlu");
|
||||||
const slug = str(formData.get("slug")) || slugify(title);
|
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 = {
|
const data = {
|
||||||
slug,
|
slug,
|
||||||
@@ -107,19 +95,15 @@ export async function saveBlogPost(formData: FormData) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
await d.updateRow({
|
await tablesDB.updateRow(DATABASE_ID, TABLES.blogPosts, id, data, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.blogPosts,
|
|
||||||
rowId: id,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await d.createRow({
|
await tablesDB.createRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.blogPosts,
|
TABLES.blogPosts,
|
||||||
rowId: ID.unique(),
|
ID.unique(),
|
||||||
data,
|
data,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
revalidatePath("/admin/blog");
|
revalidatePath("/admin/blog");
|
||||||
revalidatePath("/blog");
|
revalidatePath("/blog");
|
||||||
@@ -127,13 +111,9 @@ export async function saveBlogPost(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteBlogPost(formData: FormData) {
|
export async function deleteBlogPost(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = String(formData.get("id"));
|
const id = String(formData.get("id"));
|
||||||
await d.deleteRow({
|
await tablesDB.deleteRow(DATABASE_ID, TABLES.blogPosts, id, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.blogPosts,
|
|
||||||
rowId: id,
|
|
||||||
});
|
|
||||||
revalidatePath("/admin/blog");
|
revalidatePath("/admin/blog");
|
||||||
revalidatePath("/blog");
|
revalidatePath("/blog");
|
||||||
}
|
}
|
||||||
@@ -141,7 +121,7 @@ export async function deleteBlogPost(formData: FormData) {
|
|||||||
// ─── Services ────────────────────────────────────────────────────
|
// ─── Services ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveService(formData: FormData) {
|
export async function saveService(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = str(formData.get("id"));
|
const id = str(formData.get("id"));
|
||||||
const title = str(formData.get("title"));
|
const title = str(formData.get("title"));
|
||||||
if (!title) throw new Error("Başlık zorunlu");
|
if (!title) throw new Error("Başlık zorunlu");
|
||||||
@@ -158,19 +138,15 @@ export async function saveService(formData: FormData) {
|
|||||||
featured: bool(formData.get("featured")),
|
featured: bool(formData.get("featured")),
|
||||||
};
|
};
|
||||||
if (id) {
|
if (id) {
|
||||||
await d.updateRow({
|
await tablesDB.updateRow(DATABASE_ID, TABLES.services, id, data, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.services,
|
|
||||||
rowId: id,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await d.createRow({
|
await tablesDB.createRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.services,
|
TABLES.services,
|
||||||
rowId: slug,
|
slug,
|
||||||
data,
|
data,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
revalidatePath("/admin/hizmetler");
|
revalidatePath("/admin/hizmetler");
|
||||||
revalidatePath("/hizmetler");
|
revalidatePath("/hizmetler");
|
||||||
@@ -178,13 +154,9 @@ export async function saveService(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteService(formData: FormData) {
|
export async function deleteService(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = String(formData.get("id"));
|
const id = String(formData.get("id"));
|
||||||
await d.deleteRow({
|
await tablesDB.deleteRow(DATABASE_ID, TABLES.services, id, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.services,
|
|
||||||
rowId: id,
|
|
||||||
});
|
|
||||||
revalidatePath("/admin/hizmetler");
|
revalidatePath("/admin/hizmetler");
|
||||||
revalidatePath("/hizmetler");
|
revalidatePath("/hizmetler");
|
||||||
}
|
}
|
||||||
@@ -192,7 +164,7 @@ export async function deleteService(formData: FormData) {
|
|||||||
// ─── Projects ────────────────────────────────────────────────────
|
// ─── Projects ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveProject(formData: FormData) {
|
export async function saveProject(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = str(formData.get("id"));
|
const id = str(formData.get("id"));
|
||||||
const title = str(formData.get("title"));
|
const title = str(formData.get("title"));
|
||||||
if (!title) throw new Error("Başlık zorunlu");
|
if (!title) throw new Error("Başlık zorunlu");
|
||||||
@@ -213,19 +185,15 @@ export async function saveProject(formData: FormData) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
await d.updateRow({
|
await tablesDB.updateRow(DATABASE_ID, TABLES.projects, id, data, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.projects,
|
|
||||||
rowId: id,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await d.createRow({
|
await tablesDB.createRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.projects,
|
TABLES.projects,
|
||||||
rowId: ID.unique(),
|
ID.unique(),
|
||||||
data,
|
data,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
revalidatePath("/admin/projeler");
|
revalidatePath("/admin/projeler");
|
||||||
revalidatePath("/projeler");
|
revalidatePath("/projeler");
|
||||||
@@ -233,13 +201,9 @@ export async function saveProject(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProject(formData: FormData) {
|
export async function deleteProject(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = String(formData.get("id"));
|
const id = String(formData.get("id"));
|
||||||
await d.deleteRow({
|
await tablesDB.deleteRow(DATABASE_ID, TABLES.projects, id, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.projects,
|
|
||||||
rowId: id,
|
|
||||||
});
|
|
||||||
revalidatePath("/admin/projeler");
|
revalidatePath("/admin/projeler");
|
||||||
revalidatePath("/projeler");
|
revalidatePath("/projeler");
|
||||||
}
|
}
|
||||||
@@ -247,7 +211,7 @@ export async function deleteProject(formData: FormData) {
|
|||||||
// ─── Testimonials ────────────────────────────────────────────────
|
// ─── Testimonials ────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveTestimonial(formData: FormData) {
|
export async function saveTestimonial(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = str(formData.get("id"));
|
const id = str(formData.get("id"));
|
||||||
const name = str(formData.get("name"));
|
const name = str(formData.get("name"));
|
||||||
if (!name) throw new Error("Ad zorunlu");
|
if (!name) throw new Error("Ad zorunlu");
|
||||||
@@ -266,39 +230,31 @@ export async function saveTestimonial(formData: FormData) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
await d.updateRow({
|
await tablesDB.updateRow(DATABASE_ID, TABLES.testimonials, id, data, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.testimonials,
|
|
||||||
rowId: id,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await d.createRow({
|
await tablesDB.createRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.testimonials,
|
TABLES.testimonials,
|
||||||
rowId: ID.unique(),
|
ID.unique(),
|
||||||
data,
|
data,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
revalidatePath("/admin/referanslar");
|
revalidatePath("/admin/referanslar");
|
||||||
revalidatePath("/");
|
revalidatePath("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTestimonial(formData: FormData) {
|
export async function deleteTestimonial(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = String(formData.get("id"));
|
const id = String(formData.get("id"));
|
||||||
await d.deleteRow({
|
await tablesDB.deleteRow(DATABASE_ID, TABLES.testimonials, id, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.testimonials,
|
|
||||||
rowId: id,
|
|
||||||
});
|
|
||||||
revalidatePath("/admin/referanslar");
|
revalidatePath("/admin/referanslar");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── SEO Settings (singleton) ────────────────────────────────────
|
// ─── SEO Settings ────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveSeoSettings(formData: FormData) {
|
export async function saveSeoSettings(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const data = {
|
const data = {
|
||||||
site_name: str(formData.get("site_name")),
|
site_name: str(formData.get("site_name")),
|
||||||
site_description: str(formData.get("site_description")),
|
site_description: str(formData.get("site_description")),
|
||||||
@@ -311,28 +267,30 @@ export async function saveSeoSettings(formData: FormData) {
|
|||||||
gtm_id: str(formData.get("gtm_id")),
|
gtm_id: str(formData.get("gtm_id")),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await d.updateRow({
|
await tablesDB.updateRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.seoSettings,
|
TABLES.seoSettings,
|
||||||
rowId: "global",
|
"global",
|
||||||
data,
|
data,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
await d.createRow({
|
await tablesDB.createRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.seoSettings,
|
TABLES.seoSettings,
|
||||||
rowId: "global",
|
"global",
|
||||||
data,
|
data,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
revalidatePath("/", "layout");
|
revalidatePath("/", "layout");
|
||||||
revalidatePath("/admin/seo");
|
revalidatePath("/admin/seo");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── SEO Page (per path) ─────────────────────────────────────────
|
// ─── SEO Page ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveSeoPage(formData: FormData) {
|
export async function saveSeoPage(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = str(formData.get("id"));
|
const id = str(formData.get("id"));
|
||||||
const path = str(formData.get("path"));
|
const path = str(formData.get("path"));
|
||||||
if (!path) throw new Error("Path zorunlu");
|
if (!path) throw new Error("Path zorunlu");
|
||||||
@@ -347,61 +305,55 @@ export async function saveSeoPage(formData: FormData) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
await d.updateRow({
|
await tablesDB.updateRow(DATABASE_ID, TABLES.seoPages, id, data, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.seoPages,
|
|
||||||
rowId: id,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await d.createRow({
|
await tablesDB.createRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.seoPages,
|
TABLES.seoPages,
|
||||||
rowId: ID.unique(),
|
ID.unique(),
|
||||||
data,
|
data,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
revalidatePath(path);
|
revalidatePath(path);
|
||||||
revalidatePath("/admin/seo");
|
revalidatePath("/admin/seo");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSeoPage(formData: FormData) {
|
export async function deleteSeoPage(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = String(formData.get("id"));
|
const id = String(formData.get("id"));
|
||||||
await d.deleteRow({
|
await tablesDB.deleteRow(DATABASE_ID, TABLES.seoPages, id, secret);
|
||||||
databaseId: DATABASE_ID,
|
|
||||||
tableId: TABLES.seoPages,
|
|
||||||
rowId: id,
|
|
||||||
});
|
|
||||||
revalidatePath("/admin/seo");
|
revalidatePath("/admin/seo");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Contact messages ────────────────────────────────────────────
|
// ─── Contact ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function updateMessageStatus(formData: FormData) {
|
export async function updateMessageStatus(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = String(formData.get("id"));
|
const id = String(formData.get("id"));
|
||||||
const status = String(formData.get("status")) as
|
const status = String(formData.get("status")) as
|
||||||
| "new"
|
| "new"
|
||||||
| "read"
|
| "read"
|
||||||
| "replied"
|
| "replied"
|
||||||
| "archived";
|
| "archived";
|
||||||
await d.updateRow({
|
await tablesDB.updateRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.contactMessages,
|
TABLES.contactMessages,
|
||||||
rowId: id,
|
id,
|
||||||
data: { status },
|
{ status },
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
revalidatePath("/admin/iletisim");
|
revalidatePath("/admin/iletisim");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMessage(formData: FormData) {
|
export async function deleteMessage(formData: FormData) {
|
||||||
const d = await db();
|
const secret = await requireSessionSecret();
|
||||||
const id = String(formData.get("id"));
|
const id = String(formData.get("id"));
|
||||||
await d.deleteRow({
|
await tablesDB.deleteRow(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.contactMessages,
|
TABLES.contactMessages,
|
||||||
rowId: id,
|
id,
|
||||||
});
|
secret,
|
||||||
|
);
|
||||||
revalidatePath("/admin/iletisim");
|
revalidatePath("/admin/iletisim");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string, string | string[] | number | undefined>;
|
||||||
|
formData?: FormData;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function aw<T>(path: string, opts: FetchOpts = {}): Promise<T> {
|
||||||
|
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<string, string> = {
|
||||||
|
"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<T> {
|
||||||
|
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<AwSession>("/account/sessions/email", {
|
||||||
|
method: "POST",
|
||||||
|
body: { email, password },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get(session: string) {
|
||||||
|
return aw<AwUser>("/account", { session });
|
||||||
|
},
|
||||||
|
deleteSession(sessionId: string, session: string) {
|
||||||
|
return aw<void>(`/account/sessions/${sessionId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── TablesDB ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const tablesDB = {
|
||||||
|
listRows<T = AwRow>(
|
||||||
|
databaseId: string,
|
||||||
|
tableId: string,
|
||||||
|
queries: string[] = [],
|
||||||
|
session?: string,
|
||||||
|
) {
|
||||||
|
return aw<AwListResponse<T>>(
|
||||||
|
`/tablesdb/${databaseId}/tables/${tableId}/rows`,
|
||||||
|
{ query: { queries }, session },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getRow<T = AwRow>(
|
||||||
|
databaseId: string,
|
||||||
|
tableId: string,
|
||||||
|
rowId: string,
|
||||||
|
session?: string,
|
||||||
|
) {
|
||||||
|
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createRow<T = AwRow>(
|
||||||
|
databaseId: string,
|
||||||
|
tableId: string,
|
||||||
|
rowId: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
session?: string,
|
||||||
|
) {
|
||||||
|
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { rowId, data },
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateRow<T = AwRow>(
|
||||||
|
databaseId: string,
|
||||||
|
tableId: string,
|
||||||
|
rowId: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
session?: string,
|
||||||
|
) {
|
||||||
|
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: { data },
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteRow(
|
||||||
|
databaseId: string,
|
||||||
|
tableId: string,
|
||||||
|
rowId: string,
|
||||||
|
session?: string,
|
||||||
|
) {
|
||||||
|
return aw<void>(`/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<AwFile>(`/storage/buckets/${bucketId}/files`, {
|
||||||
|
method: "POST",
|
||||||
|
formData: fd,
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteFile(bucketId: string, fileId: string, session?: string) {
|
||||||
|
return aw<void>(`/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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { userAccount } from "@/lib/appwrite-server";
|
import { account } from "@/lib/appwrite-rest";
|
||||||
|
|
||||||
export const SESSION_COOKIE = "kovak_session";
|
export const SESSION_COOKIE = "kovak_session";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export async function getCurrentUser() {
|
|||||||
const secret = await getSessionSecret();
|
const secret = await getSessionSecret();
|
||||||
if (!secret) return null;
|
if (!secret) return null;
|
||||||
try {
|
try {
|
||||||
return await userAccount(secret).get();
|
return await account.get(secret);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-37
@@ -1,6 +1,6 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import { Query } from "appwrite";
|
import { DATABASE_ID, Q, TABLES, tablesDB } from "@/lib/appwrite-rest";
|
||||||
import { publicDB, DATABASE_ID, TABLES } from "@/lib/appwrite-server";
|
import { getSessionSecret } from "@/lib/auth";
|
||||||
import type {
|
import type {
|
||||||
BlogPostRow,
|
BlogPostRow,
|
||||||
ContactMessageRow,
|
ContactMessageRow,
|
||||||
@@ -13,98 +13,114 @@ import type {
|
|||||||
|
|
||||||
async function safeList<T>(tableId: string, queries: string[]): Promise<T[]> {
|
async function safeList<T>(tableId: string, queries: string[]): Promise<T[]> {
|
||||||
try {
|
try {
|
||||||
const res = await publicDB.listRows({
|
const res = await tablesDB.listRows<T>(DATABASE_ID, tableId, queries);
|
||||||
databaseId: DATABASE_ID,
|
return res.rows;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeListAuth<T>(tableId: string, queries: string[]): Promise<T[]> {
|
||||||
|
try {
|
||||||
|
const secret = await getSessionSecret();
|
||||||
|
const res = await tablesDB.listRows<T>(
|
||||||
|
DATABASE_ID,
|
||||||
tableId,
|
tableId,
|
||||||
queries,
|
queries,
|
||||||
});
|
secret ?? undefined,
|
||||||
return res.rows as T[];
|
);
|
||||||
|
return res.rows;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listServices(opts?: { featured?: boolean }) {
|
export async function listServices(opts?: { featured?: boolean }) {
|
||||||
const q = [Query.orderAsc("order"), Query.limit(50)];
|
const q = [Q.orderAsc("order"), Q.limit(50)];
|
||||||
if (opts?.featured) q.unshift(Query.equal("featured", true));
|
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||||
return safeList<ServiceRow>(TABLES.services, q);
|
return safeList<ServiceRow>(TABLES.services, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listProjects(opts?: { featured?: boolean; limit?: number }) {
|
export async function listProjects(opts?: { featured?: boolean; limit?: number }) {
|
||||||
const q = [Query.orderDesc("year"), Query.limit(opts?.limit ?? 50)];
|
const q = [Q.orderDesc("year"), Q.limit(opts?.limit ?? 50)];
|
||||||
if (opts?.featured) q.unshift(Query.equal("featured", true));
|
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||||
return safeList<ProjectRow>(TABLES.projects, q);
|
return safeList<ProjectRow>(TABLES.projects, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listPublishedPosts(opts?: { limit?: number }) {
|
export async function listPublishedPosts(opts?: { limit?: number }) {
|
||||||
return safeList<BlogPostRow>(TABLES.blogPosts, [
|
return safeList<BlogPostRow>(TABLES.blogPosts, [
|
||||||
Query.equal("status", "published"),
|
Q.equal("status", "published"),
|
||||||
Query.orderDesc("published_at"),
|
Q.orderDesc("published_at"),
|
||||||
Query.limit(opts?.limit ?? 50),
|
Q.limit(opts?.limit ?? 50),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAllPosts() {
|
export async function listAllPosts() {
|
||||||
return safeList<BlogPostRow>(TABLES.blogPosts, [
|
return safeListAuth<BlogPostRow>(TABLES.blogPosts, [
|
||||||
Query.orderDesc("$createdAt"),
|
Q.orderDesc("$createdAt"),
|
||||||
Query.limit(200),
|
Q.limit(200),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPostBySlug(slug: string): Promise<BlogPostRow | null> {
|
export async function getPostBySlug(slug: string): Promise<BlogPostRow | null> {
|
||||||
const res = await safeList<BlogPostRow>(TABLES.blogPosts, [
|
const res = await safeList<BlogPostRow>(TABLES.blogPosts, [
|
||||||
Query.equal("slug", slug),
|
Q.equal("slug", slug),
|
||||||
Query.limit(1),
|
Q.limit(1),
|
||||||
]);
|
]);
|
||||||
return res[0] ?? null;
|
return res[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listTestimonials(opts?: { featured?: boolean }) {
|
export async function listTestimonials(opts?: { featured?: boolean }) {
|
||||||
const q = [Query.orderAsc("order"), Query.limit(50)];
|
const q = [Q.orderAsc("order"), Q.limit(50)];
|
||||||
if (opts?.featured) q.unshift(Query.equal("featured", true));
|
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||||
return safeList<TestimonialRow>(TABLES.testimonials, q);
|
return safeList<TestimonialRow>(TABLES.testimonials, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listMessages(status?: ContactMessageRow["status"]) {
|
export async function listMessages(status?: ContactMessageRow["status"]) {
|
||||||
const q = [Query.orderDesc("$createdAt"), Query.limit(200)];
|
const q = [Q.orderDesc("$createdAt"), Q.limit(200)];
|
||||||
if (status) q.unshift(Query.equal("status", status));
|
if (status) q.unshift(Q.equal("status", status));
|
||||||
return safeList<ContactMessageRow>(TABLES.contactMessages, q);
|
return safeListAuth<ContactMessageRow>(TABLES.contactMessages, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSeoPage(path: string): Promise<SeoPageRow | null> {
|
export async function getSeoPage(path: string): Promise<SeoPageRow | null> {
|
||||||
const res = await safeList<SeoPageRow>(TABLES.seoPages, [
|
const res = await safeList<SeoPageRow>(TABLES.seoPages, [
|
||||||
Query.equal("path", path),
|
Q.equal("path", path),
|
||||||
Query.limit(1),
|
Q.limit(1),
|
||||||
]);
|
]);
|
||||||
return res[0] ?? null;
|
return res[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listSeoPages() {
|
export async function listSeoPages() {
|
||||||
return safeList<SeoPageRow>(TABLES.seoPages, [
|
return safeListAuth<SeoPageRow>(TABLES.seoPages, [
|
||||||
Query.orderAsc("path"),
|
Q.orderAsc("path"),
|
||||||
Query.limit(200),
|
Q.limit(200),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSeoSettings(): Promise<SeoSettingsRow | null> {
|
export async function getSeoSettings(): Promise<SeoSettingsRow | null> {
|
||||||
try {
|
try {
|
||||||
return (await publicDB.getRow({
|
return await tablesDB.getRow<SeoSettingsRow>(
|
||||||
databaseId: DATABASE_ID,
|
DATABASE_ID,
|
||||||
tableId: TABLES.seoSettings,
|
TABLES.seoSettings,
|
||||||
rowId: "global",
|
"global",
|
||||||
})) as unknown as SeoSettingsRow;
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRow<T>(tableId: string, rowId: string): Promise<T | null> {
|
export async function getRow<T>(
|
||||||
|
tableId: string,
|
||||||
|
rowId: string,
|
||||||
|
): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
return (await publicDB.getRow({
|
const secret = await getSessionSecret();
|
||||||
databaseId: DATABASE_ID,
|
return await tablesDB.getRow<T>(
|
||||||
|
DATABASE_ID,
|
||||||
tableId,
|
tableId,
|
||||||
rowId,
|
rowId,
|
||||||
})) as unknown as T;
|
secret ?? undefined,
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -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;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -9,7 +11,7 @@ export interface ServiceRow extends Models.Row {
|
|||||||
featured?: boolean | null;
|
featured?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectRow extends Models.Row {
|
export interface ProjectRow extends AwRow {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -21,7 +23,7 @@ export interface ProjectRow extends Models.Row {
|
|||||||
featured?: boolean | null;
|
featured?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlogPostRow extends Models.Row {
|
export interface BlogPostRow extends AwRow {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
excerpt?: string | null;
|
excerpt?: string | null;
|
||||||
@@ -37,7 +39,7 @@ export interface BlogPostRow extends Models.Row {
|
|||||||
seo_image?: string | null;
|
seo_image?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestimonialRow extends Models.Row {
|
export interface TestimonialRow extends AwRow {
|
||||||
name: string;
|
name: string;
|
||||||
role?: string | null;
|
role?: string | null;
|
||||||
company?: string | null;
|
company?: string | null;
|
||||||
@@ -48,7 +50,7 @@ export interface TestimonialRow extends Models.Row {
|
|||||||
featured?: boolean | null;
|
featured?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeoPageRow extends Models.Row {
|
export interface SeoPageRow extends AwRow {
|
||||||
path: string;
|
path: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
@@ -57,7 +59,7 @@ export interface SeoPageRow extends Models.Row {
|
|||||||
noindex?: boolean | null;
|
noindex?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeoSettingsRow extends Models.Row {
|
export interface SeoSettingsRow extends AwRow {
|
||||||
site_name?: string | null;
|
site_name?: string | null;
|
||||||
site_description?: string | null;
|
site_description?: string | null;
|
||||||
default_og_image?: string | null;
|
default_og_image?: string | null;
|
||||||
@@ -69,7 +71,7 @@ export interface SeoSettingsRow extends Models.Row {
|
|||||||
gtm_id?: string | null;
|
gtm_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContactMessageRow extends Models.Row {
|
export interface ContactMessageRow extends AwRow {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
|
|||||||
Generated
-31
@@ -8,7 +8,6 @@
|
|||||||
"name": "kovak-yazilim",
|
"name": "kovak-yazilim",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"appwrite": "^25.1.1",
|
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.16.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
@@ -1079,18 +1078,6 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@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": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.31",
|
"version": "2.10.31",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
||||||
@@ -1103,15 +1090,6 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001793",
|
"version": "1.0.30001793",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
|
||||||
@@ -1186,15 +1164,6 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"appwrite": "^25.1.1",
|
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.16.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
|
|||||||
Reference in New Issue
Block a user