feat(domain): complete phase 2 backend core

This commit is contained in:
poyrazavsever
2026-07-16 16:46:42 +03:00
parent 92bc99ba12
commit b155132acf
28 changed files with 5343 additions and 30 deletions
+13
View File
@@ -0,0 +1,13 @@
import "server-only";
import type { DomainActor } from "../domain/actor";
import type { SessionContext } from "./session";
export function domainActorFromSession(context: SessionContext): DomainActor {
return {
authUserId: context.user.id,
role: context.profile.role,
clientId: context.profile.clientId,
disabled: context.profile.disabled,
};
}
+50 -1
View File
@@ -2,7 +2,7 @@ import "server-only";
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { hashPassword } from "better-auth/crypto";
import { and, eq } from "drizzle-orm";
import { and, eq, isNull } from "drizzle-orm";
import { z } from "zod";
import type { SessionContext } from "@/server/auth/session";
import { getServerConfig } from "@/server/config";
@@ -11,6 +11,7 @@ import {
account,
appProfiles,
authAuditEvents,
clients,
portalInvitations,
session,
user,
@@ -37,6 +38,7 @@ export type PortalInvitationErrorCode =
| "INVITATION_NOT_FOUND"
| "INVITATION_NOT_PENDING"
| "INVITATION_EXPIRED"
| "CLIENT_NOT_FOUND"
| "CLIENT_ALREADY_LINKED"
| "EMAIL_ALREADY_REGISTERED";
@@ -70,6 +72,25 @@ export async function createPortalInvitation(
const { db } = getSqliteConnection();
const invitationId = db.transaction((tx) => {
const client = tx
.select({ id: clients.id, authUserId: clients.authUserId })
.from(clients)
.where(
and(eq(clients.id, parsed.clientId), eq(clients.ownerUserId, actor.user.id)),
)
.get();
if (!client) {
throw new PortalInvitationError("CLIENT_NOT_FOUND", "Müşteri bulunamadı.");
}
if (client.authUserId) {
throw new PortalInvitationError(
"CLIENT_ALREADY_LINKED",
"Bu müşteri için portal hesabı zaten mevcut.",
);
}
const [linkedProfile] = tx
.select({ id: appProfiles.id })
.from(appProfiles)
@@ -333,6 +354,24 @@ export async function acceptPortalInvitation(input: {
})
.run();
const linkedClient = tx
.update(clients)
.set({ authUserId, updatedAt: now })
.where(
and(
eq(clients.id, invitation.clientId),
isNull(clients.authUserId),
),
)
.run();
if (linkedClient.changes !== 1) {
throw new PortalInvitationError(
"CLIENT_ALREADY_LINKED",
"Müşteri kaydı bulunamadı veya başka bir hesaba bağlandı.",
);
}
const accepted = tx
.update(portalInvitations)
.set({ status: "accepted", acceptedAt: now })
@@ -417,6 +456,16 @@ export function setClientPortalAccess(
const { db } = getSqliteConnection();
db.transaction((tx) => {
const ownedClient = tx
.select({ id: clients.id })
.from(clients)
.where(and(eq(clients.id, clientId), eq(clients.ownerUserId, actor.user.id)))
.get();
if (!ownedClient) {
throw new PortalInvitationError("CLIENT_NOT_FOUND", "Müşteri bulunamadı.");
}
const [profile] = tx
.select({ authUserId: appProfiles.authUserId, email: appProfiles.email })
.from(appProfiles)
+24 -3
View File
@@ -1,13 +1,13 @@
import "server-only";
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { cache } from "react";
import { auth } from "@/server/auth/auth";
import type { UserRole } from "@/server/auth/types";
import { getSqliteConnection } from "@/server/db/client";
import { appProfiles } from "@/server/db/schema";
import { appProfiles, clients } from "@/server/db/schema";
type BetterAuthSession = NonNullable<Awaited<ReturnType<typeof auth.api.getSession>>>;
@@ -108,5 +108,26 @@ export function getProfileByAuthUserId(authUserId: string): SessionContext["prof
.limit(1)
.all();
return profile ?? null;
if (!profile) {
return null;
}
if (profile.role === "client") {
if (!profile.clientId) return null;
const linkedClient = db
.select({ id: clients.id })
.from(clients)
.where(
and(
eq(clients.id, profile.clientId),
eq(clients.authUserId, profile.authUserId),
),
)
.get();
if (!linkedClient) return null;
}
return profile;
}