37679e83e6
- Next.js 16 + Appwrite multi-tenant emlak CRM - Database: kovakemlak-db (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings) - Same stack as isletmem-kovakcrm (shadcn/ui template base) - Modules: portföy, müşteri takibi, arama kriterleri, otomatik eşleştirme, sunum linki, yatırımcı portalı
172 lines
4.4 KiB
TypeScript
172 lines
4.4 KiB
TypeScript
"use server";
|
||
|
||
import { revalidatePath } from "next/cache";
|
||
import { ID, Permission, Role } from "node-appwrite";
|
||
import { InputFile } from "node-appwrite/file";
|
||
|
||
import { logAudit } from "./audit";
|
||
import type { LogoActionState } from "./logo-types";
|
||
import { BUCKETS, DATABASE_ID, TABLES } from "./schema";
|
||
import { createAdminClient } from "./server";
|
||
import { requireRole, requireTenant } from "./tenant-guard";
|
||
|
||
const MAX_BYTES = 2 * 1024 * 1024;
|
||
const ALLOWED_TYPES = new Set([
|
||
"image/png",
|
||
"image/jpeg",
|
||
"image/jpg",
|
||
"image/webp",
|
||
"image/svg+xml",
|
||
]);
|
||
|
||
function teamLogoPermissions(tenantId: string) {
|
||
return [
|
||
Permission.read(Role.any()),
|
||
Permission.update(Role.team(tenantId, "owner")),
|
||
Permission.update(Role.team(tenantId, "admin")),
|
||
Permission.delete(Role.team(tenantId, "owner")),
|
||
Permission.delete(Role.team(tenantId, "admin")),
|
||
];
|
||
}
|
||
|
||
export async function uploadLogoAction(
|
||
_prev: LogoActionState,
|
||
formData: FormData,
|
||
): Promise<LogoActionState> {
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
requireRole(ctx, ["owner", "admin"]);
|
||
} catch {
|
||
return { ok: false, error: "Logo yüklemek için yönetici yetkisi gerekli." };
|
||
}
|
||
|
||
const file = formData.get("logo");
|
||
if (!(file instanceof File) || file.size === 0) {
|
||
return { ok: false, error: "Dosya seçin." };
|
||
}
|
||
|
||
if (file.size > MAX_BYTES) {
|
||
return { ok: false, error: "Dosya 2MB'dan büyük olamaz." };
|
||
}
|
||
if (!ALLOWED_TYPES.has(file.type)) {
|
||
return { ok: false, error: "Sadece PNG, JPG, WebP veya SVG yükleyebilirsiniz." };
|
||
}
|
||
if (!ctx.settings) {
|
||
return { ok: false, error: "Çalışma alanı ayarları bulunamadı." };
|
||
}
|
||
|
||
const { storage, tablesDB } = createAdminClient();
|
||
const previousLogoId = ctx.settings.logo;
|
||
|
||
let newFileId: string | null = null;
|
||
try {
|
||
const buffer = Buffer.from(await file.arrayBuffer());
|
||
const inputFile = InputFile.fromBuffer(buffer, file.name);
|
||
|
||
const created = await storage.createFile({
|
||
bucketId: BUCKETS.tenantLogos,
|
||
fileId: ID.unique(),
|
||
file: inputFile,
|
||
permissions: teamLogoPermissions(ctx.tenantId),
|
||
});
|
||
newFileId = created.$id;
|
||
|
||
await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, {
|
||
logo: newFileId,
|
||
});
|
||
|
||
if (previousLogoId && previousLogoId !== newFileId) {
|
||
try {
|
||
await storage.deleteFile({
|
||
bucketId: BUCKETS.tenantLogos,
|
||
fileId: previousLogoId,
|
||
});
|
||
} catch {
|
||
// best-effort — orphaned file is acceptable, won't block the new logo
|
||
}
|
||
}
|
||
|
||
await logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "update",
|
||
entityType: "tenant_logo",
|
||
entityId: newFileId,
|
||
changes: { previous: previousLogoId ?? null },
|
||
});
|
||
} catch (e) {
|
||
if (newFileId) {
|
||
try {
|
||
await storage.deleteFile({
|
||
bucketId: BUCKETS.tenantLogos,
|
||
fileId: newFileId,
|
||
});
|
||
} catch {
|
||
/* ignore cleanup error */
|
||
}
|
||
}
|
||
return {
|
||
ok: false,
|
||
error: e instanceof Error ? e.message : "Logo yüklenemedi.",
|
||
};
|
||
}
|
||
|
||
revalidatePath("/settings/workspace");
|
||
revalidatePath("/", "layout");
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function removeLogoAction(): Promise<LogoActionState> {
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
requireRole(ctx, ["owner", "admin"]);
|
||
} catch {
|
||
return { ok: false, error: "Logo silmek için yönetici yetkisi gerekli." };
|
||
}
|
||
|
||
if (!ctx.settings) {
|
||
return { ok: false, error: "Çalışma alanı ayarları bulunamadı." };
|
||
}
|
||
|
||
const previousLogoId = ctx.settings.logo;
|
||
if (!previousLogoId) {
|
||
return { ok: true };
|
||
}
|
||
|
||
try {
|
||
const { storage, tablesDB } = createAdminClient();
|
||
|
||
await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, {
|
||
logo: null,
|
||
});
|
||
|
||
try {
|
||
await storage.deleteFile({
|
||
bucketId: BUCKETS.tenantLogos,
|
||
fileId: previousLogoId,
|
||
});
|
||
} catch {
|
||
/* file already gone, fine */
|
||
}
|
||
|
||
await logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "delete",
|
||
entityType: "tenant_logo",
|
||
entityId: previousLogoId,
|
||
});
|
||
} catch (e) {
|
||
return {
|
||
ok: false,
|
||
error: e instanceof Error ? e.message : "Logo silinemedi.",
|
||
};
|
||
}
|
||
|
||
revalidatePath("/settings/workspace");
|
||
revalidatePath("/", "layout");
|
||
return { ok: true };
|
||
}
|