Files
lab/src/lib/appwrite/auth-actions.ts
T
kovakmedya 9ea35e88cf fix: patch node-fetch-native-with-agent to bypass bundled undici on Node 26
node-appwrite 23.1.0 ships a bundled undici Agent via node-fetch-native-with-agent.
That bundle uses an older undici dispatcher API that crashes on Node 26 with
'invalid onError method' (UND_ERR_INVALID_ARG), making every Appwrite call
fail with 'fetch failed' / our user-facing 'Bağlantı hatası' fallback.

The patch replaces createAgent/createFetch with thin pass-throughs to
globalThis.fetch — Node native fetch handles HTTPS to db.kovaksoft.com
directly, no proxy/agent customization needed. Verified end-to-end via
users.listMemberships against the live project.

Also added dev-mode error surfacing in appwriteError so future SDK
exceptions show the real message instead of 'Bağlantı hatası'.
2026-05-21 19:31:16 +03:00

186 lines
6.3 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 { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { AppwriteException, ID, Query } from "node-appwrite";
import { APPWRITE_SESSION_COOKIE, createAdminClient, createSessionClient } from "./server";
import { DATABASE_ID, TABLES, type TenantKind, type TenantSettings } from "./schema";
import { ACTIVE_TENANT_COOKIE } from "./tenant-types";
import type { AuthState } from "./auth-types";
function appwriteError(e: unknown): string {
if (e instanceof AppwriteException) {
switch (e.type) {
case "user_invalid_credentials":
return "Email veya şifre hatalı.";
case "user_blocked":
return "Hesabınız engellenmiş.";
case "user_already_exists":
case "user_email_already_exists":
return "Bu email ile zaten bir hesap var.";
case "user_password_mismatch":
return "Şifreler eşleşmiyor.";
case "general_rate_limit_exceeded":
return "Çok fazla deneme. Birkaç dakika sonra tekrar deneyin.";
default:
return e.message || "Beklenmeyen bir hata oluştu.";
}
}
if (process.env.NODE_ENV !== "production" && e instanceof Error) {
return `Bağlantı hatası: ${e.message}`;
}
return "Bağlantı hatası. Tekrar deneyin.";
}
async function setSessionCookie(secret: string, expire: string) {
(await cookies()).set(APPWRITE_SESSION_COOKIE, secret, {
path: "/",
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
expires: new Date(expire),
});
}
async function setActiveTenantCookie(tenantId: string) {
(await cookies()).set(ACTIVE_TENANT_COOKIE, tenantId, {
path: "/",
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 365,
});
}
async function pickTenantIdByKind(userId: string, kind: TenantKind): Promise<string | null> {
const { users, tablesDB } = createAdminClient();
const memberships = await users.listMemberships(userId);
const teamIds = memberships.memberships.map((m) => m.teamId);
if (teamIds.length === 0) return null;
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings,
queries: [
Query.equal("tenantId", teamIds),
Query.equal("kind", kind),
Query.limit(1),
],
});
const row = result.rows[0] as unknown as TenantSettings | undefined;
return row?.tenantId ?? null;
}
export async function signInAction(_prev: AuthState, formData: FormData): Promise<AuthState> {
const email = String(formData.get("email") ?? "").trim();
const password = String(formData.get("password") ?? "");
const inviteCode = String(formData.get("inviteCode") ?? "").trim();
const rawKind = String(formData.get("kind") ?? "").trim();
const kind: TenantKind | null =
rawKind === "lab" || rawKind === "clinic" ? rawKind : null;
if (!email || !password) {
return { ok: false, error: "Email ve şifre zorunlu." };
}
let sessionUserId: string | null = null;
let sessionId: string | null = null;
try {
const { account } = createAdminClient();
const session = await account.createEmailPasswordSession(email, password);
sessionUserId = session.userId;
sessionId = session.$id;
await setSessionCookie(session.secret, session.expire);
} catch (e) {
console.error("[signInAction] createEmailPasswordSession", e);
return { ok: false, error: appwriteError(e) };
}
// Invite flow short-circuits the kind check — invite code drives team membership
if (!inviteCode && kind && sessionUserId) {
const matchedTenantId = await pickTenantIdByKind(sessionUserId, kind);
if (!matchedTenantId) {
// Roll back session: user has no tenant of the requested kind
try {
const { users } = createAdminClient();
if (sessionId) await users.deleteSession(sessionUserId, sessionId);
} catch {
/* best-effort */
}
(await cookies()).delete(APPWRITE_SESSION_COOKIE);
const label = kind === "lab" ? "laboratuvar" : "klinik";
return {
ok: false,
error: `Bu hesap için ${label} kaydı bulunamadı. Diğer hesap türünü seçin veya yeni çalışma alanı oluşturun.`,
};
}
await setActiveTenantCookie(matchedTenantId);
try {
const { users } = createAdminClient();
const user = await users.get(sessionUserId);
await users.updatePrefs(sessionUserId, {
...(user.prefs ?? {}),
activeTenant: matchedTenantId,
});
} catch {
/* best-effort */
}
}
redirect(inviteCode ? `/d/${inviteCode}` : "/dashboard");
}
export async function signUpAction(_prev: AuthState, formData: FormData): Promise<AuthState> {
const name = String(formData.get("name") ?? "").trim();
const email = String(formData.get("email") ?? "").trim();
const password = String(formData.get("password") ?? "");
const inviteCode = String(formData.get("inviteCode") ?? "").trim();
if (!name || !email || !password) {
return { ok: false, error: "Tüm alanlar zorunlu." };
}
if (password.length < 8) {
return { ok: false, error: "Şifre en az 8 karakter olmalı." };
}
try {
const { account } = createAdminClient();
await account.create(ID.unique(), email, password, name);
const session = await account.createEmailPasswordSession(email, password);
await setSessionCookie(session.secret, session.expire);
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
redirect(inviteCode ? `/d/${inviteCode}` : "/onboarding");
}
export async function forgotPasswordAction(
_prev: AuthState,
formData: FormData,
): Promise<AuthState> {
const email = String(formData.get("email") ?? "").trim();
if (!email) return { ok: false, error: "Email zorunlu." };
try {
const { account } = createAdminClient();
const recoveryUrl = `${process.env.APP_URL ?? "http://localhost:3000"}/reset-password`;
await account.createRecovery(email, recoveryUrl);
return { ok: true };
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
}
export async function signOutAction() {
try {
const { account } = await createSessionClient();
await account.deleteSession("current");
} catch {
// ignore — cookie will be cleared anyway
}
(await cookies()).delete(APPWRITE_SESSION_COOKIE);
redirect("/sign-in");
}