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:
@@ -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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user