diff --git a/src/app/(auth)/sign-in/components/login-form-1.tsx b/src/app/(auth)/sign-in/components/login-form-1.tsx index 4a8ea78..cf94878 100644 --- a/src/app/(auth)/sign-in/components/login-form-1.tsx +++ b/src/app/(auth)/sign-in/components/login-form-1.tsx @@ -1,8 +1,8 @@ "use client"; import Link from "next/link"; -import { useActionState } from "react"; -import { Loader2 } from "lucide-react"; +import { useActionState, useState } from "react"; +import { FlaskConical, Loader2, Stethoscope } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -13,11 +13,14 @@ import { cn } from "@/lib/utils"; import { signInAction } from "@/lib/appwrite/auth-actions"; import { initialAuthState } from "@/lib/appwrite/auth-types"; +type Kind = "clinic" | "lab"; + export function LoginForm1({ className, inviteCode, ...props }: React.ComponentProps<"div"> & { inviteCode?: string }) { + const [kind, setKind] = useState("clinic"); const [state, formAction, isPending] = useActionState(signInAction, initialAuthState); return ( @@ -26,6 +29,7 @@ export function LoginForm1({
{inviteCode && } +
@@ -45,10 +49,14 @@ export function LoginForm1({

Tekrar hoş geldiniz

- Hesabınıza giriş yaparak işletmenizi yönetmeye devam edin + Hesabınıza giriş yapın

+ {!inviteCode && ( + + )} +
void; + disabled?: boolean; +}) { + return ( +
+ onChange("clinic")} + disabled={disabled} + > + + Klinik + + onChange("lab")} + disabled={disabled} + > + + Laboratuvar + +
+ ); +} + +function PillButton({ + active, + onClick, + disabled, + children, +}: { + active: boolean; + onClick: () => void; + disabled?: boolean; + children: React.ReactNode; +}) { + return ( + + ); +} + function BrandPanel() { return (
@@ -157,10 +231,10 @@ function BrandPanel() {

- Müşteriden faturaya, tek panelden işletmenizi yönetin. + Klinik ve laboratuvar tek panelde.

- Müşteriler, hizmetler, takvim, görevler ve finans — hepsi tek yerde, multi-tenant ve ekibinize özel. + İş yayınla, taranan dosyaları paylaş, protez aşamalarını adım adım takip et — finansal akışla birlikte.

Kovak Yazılım tarafından
diff --git a/src/lib/appwrite/auth-actions.ts b/src/lib/appwrite/auth-actions.ts index 9dd5c7d..bbdd5a9 100644 --- a/src/lib/appwrite/auth-actions.ts +++ b/src/lib/appwrite/auth-actions.ts @@ -2,9 +2,11 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { AppwriteException, ID } from "node-appwrite"; +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 { @@ -38,23 +40,90 @@ async function setSessionCookie(secret: string, expire: string) { }); } +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 { + 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 { 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) { 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"); }