Files
lab/src/lib/appwrite/profile-actions.ts
T
kovakmedya 12631cf9c5 perf+fix: file download proxy + drop awaits on audit/notifications/finance sync
Two problems reported by the user:

1. File downloads broken on the lab side.
   The link in JobFilesPanel pointed straight at Appwrite's
   /storage/.../view URL. Storage permissions are scoped to the job's two
   teams, but the browser only has a session cookie for our app domain,
   not for db.kovaksoft.com — so the cross-origin request hit Appwrite
   as a guest and 401'd.

   New /api/jobs/[jobId]/files/[fileId]/download route. requireTenant()
   first, then verify the caller's tenant is one of (clinicTenantId,
   labTenantId) on the parent job, then storage.getFileDownload via the
   admin SDK and stream the buffer back with Content-Disposition:
   attachment so the browser saves it under the original filename.
   listJobFiles now hands out that relative URL instead of the Appwrite
   one — same anchor in the panel, just routed through us.

2. Saves and edits feel slow whenever a notification is involved.
   Every mutation was awaiting logAudit, createNotification and
   syncFinanceForJob in sequence. None of these need to block the user
   response — audit is best-effort logging, notifications are async UX,
   and the finance sync is idempotent and re-runs on the next mutation
   anyway. Switched all 46 call sites across the action modules to
   void-fire-and-forget (matching the pattern we already used in
   clinic-pricing-actions). Net effect: each mutation drops ~3 sequential
   Appwrite roundtrips before the server action returns.
2026-05-22 01:05:25 +03:00

142 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server";
import { revalidatePath } from "next/cache";
import { AppwriteException } from "node-appwrite";
import { logAudit } from "./audit";
import { createSessionClient } from "./server";
import { getActiveTenantId } from "./tenant";
import type { ProfileState } from "./profile-types";
function appwriteError(e: unknown): string {
if (e instanceof AppwriteException) {
if (e.type === "user_invalid_credentials") return "Şifre hatalı.";
if (e.type === "user_password_mismatch") return "Şifreler eşleşmiyor.";
if (e.type === "user_email_already_exists")
return "Bu email zaten başka bir hesapta kullanımda.";
if (e.type === "user_password_recently_used")
return "Bu şifreyi yakın zamanda kullandınız, başka bir şifre seçin.";
if (e.type === "general_rate_limit_exceeded")
return "Çok fazla deneme. Birkaç dakika sonra tekrar deneyin.";
return e.message || "Beklenmeyen hata.";
}
return "Bağlantı hatası. Tekrar deneyin.";
}
async function audit(action: "update", entityType: string, changes: Record<string, unknown>) {
try {
const session = await createSessionClient();
const user = await session.account.get();
const tenantId = (await getActiveTenantId()) ?? "global";
void logAudit({
tenantId,
userId: user.$id,
action,
entityType,
entityId: user.$id,
changes,
});
} catch {
/* ignore */
}
}
export async function updateNameAction(
_prev: ProfileState,
formData: FormData,
): Promise<ProfileState> {
const name = String(formData.get("name") ?? "").trim();
if (!name) {
return { ok: false, error: "İsim boş olamaz.", fieldErrors: { name: "Zorunlu" } };
}
if (name.length > 128) {
return { ok: false, error: "İsim çok uzun." };
}
try {
const { account } = await createSessionClient();
await account.updateName(name);
await audit("update", "user_name", { name });
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/", "layout");
return { ok: true };
}
export async function updateEmailAction(
_prev: ProfileState,
formData: FormData,
): Promise<ProfileState> {
const email = String(formData.get("email") ?? "").trim().toLowerCase();
const password = String(formData.get("password") ?? "");
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
return {
ok: false,
error: "Geçerli bir email girin.",
fieldErrors: { email: "Geçersiz" },
};
}
if (!password) {
return {
ok: false,
error: "Doğrulama için şifrenizi girin.",
fieldErrors: { password: "Zorunlu" },
};
}
try {
const { account } = await createSessionClient();
await account.updateEmail(email, password);
await audit("update", "user_email", { email });
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/", "layout");
return { ok: true };
}
export async function updatePasswordAction(
_prev: ProfileState,
formData: FormData,
): Promise<ProfileState> {
const oldPassword = String(formData.get("oldPassword") ?? "");
const newPassword = String(formData.get("newPassword") ?? "");
const confirmPassword = String(formData.get("confirmPassword") ?? "");
if (!oldPassword) {
return {
ok: false,
error: "Mevcut şifrenizi girin.",
fieldErrors: { oldPassword: "Zorunlu" },
};
}
if (newPassword.length < 8) {
return {
ok: false,
error: "Yeni şifre en az 8 karakter olmalı.",
fieldErrors: { newPassword: "En az 8 karakter" },
};
}
if (newPassword !== confirmPassword) {
return {
ok: false,
error: "Şifreler eşleşmiyor.",
fieldErrors: { confirmPassword: "Eşleşmiyor" },
};
}
try {
const { account } = await createSessionClient();
await account.updatePassword(newPassword, oldPassword);
await audit("update", "user_password", { changed: true });
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
return { ok: true };
}