import { FastifyInstance } from "fastify"; import { eq, and } from "drizzle-orm"; import { authProfiles, } from "../../db/schema"; import { loadProfileWithBranches, resolveTenantBranchIds, syncProfileBranches, } from "../utils/profileBranches"; import { enrichProfilesWithTeams, resolveTenantTeamIds, syncProfileTeams, } from "../utils/profileTeams"; import { enrichProfileWithCalendarSubscription, generateProfileCalendarSubscriptionToken, } from "../utils/calendarSubscription"; export default async function authProfilesRoutes(server: FastifyInstance) { // ------------------------------------------------------------- // GET SINGLE PROFILE // ------------------------------------------------------------- server.get("/profiles/:id", async (req, reply) => { try { const { id } = req.params as { id: string }; const tenantId = (req.user as any)?.tenant_id; if (!tenantId) { return reply.code(400).send({ error: "No tenant selected" }); } const profileWithBranches = await loadProfileWithBranches(server, id, tenantId) const [profile] = profileWithBranches ? await enrichProfilesWithTeams(server, [profileWithBranches]) : [null] if (!profile) { return reply.code(404).send({ error: "User not found or not in tenant" }); } return enrichProfileWithCalendarSubscription(profile); } catch (error) { console.error("GET /profiles/:id ERROR:", error); return reply.code(500).send({ error: "Internal Server Error" }); } }); function sanitizeProfileUpdate(body: any) { const cleaned: any = { ...body } // ❌ Systemfelder entfernen const forbidden = [ "id", "user_id", "tenant_id", "created_at", "updated_at", "updatedAt", "updatedBy", "old_profile_id", "full_name", "branch", "calendar_subscription_token", "calendar_subscription_path", "calendar_subscription_url" ] forbidden.forEach(f => delete cleaned[f]) // ❌ Falls NULL Strings vorkommen → in null umwandeln for (const key of Object.keys(cleaned)) { if (cleaned[key] === "") cleaned[key] = null } // ✅ Date-Felder sauber konvertieren, falls vorhanden const dateFields = ["birthday", "entry_date"] for (const field of dateFields) { if (cleaned[field]) { const d = new Date(cleaned[field]) if (!isNaN(d.getTime())) cleaned[field] = d else delete cleaned[field] // invalid → entfernen } } return cleaned } // ------------------------------------------------------------- // UPDATE PROFILE // ------------------------------------------------------------- server.put("/profiles/:id", async (req, reply) => { try { const tenantId = req.user?.tenant_id const userId = req.user?.user_id if (!tenantId) { return reply.code(400).send({ error: "No tenant selected" }) } const { id } = req.params as { id: string } let body = req.body as any // Clean + Normalize body = sanitizeProfileUpdate(body) const { primaryBranchId, branchIds } = await resolveTenantBranchIds( server, tenantId, [ ...(Array.isArray(body.branch_ids) ? body.branch_ids : []), ...(Array.isArray(body.branches) ? body.branches : []), ], body.branch_id ?? body.branch?.id ?? null ) const teamIds = await resolveTenantTeamIds( server, tenantId, [ ...(Array.isArray(body.team_ids) ? body.team_ids : []), ...(Array.isArray(body.teams) ? body.teams : []), ], ) delete body.branch_ids delete body.branches delete body.team_ids delete body.teams const updateData = { ...body, branch_id: primaryBranchId, updatedAt: new Date(), updatedBy: userId } const updated = await server.db .update(authProfiles) .set(updateData) .where( and( eq(authProfiles.id, id), eq(authProfiles.tenant_id, tenantId) ) ) .returning() if (!updated.length) { return reply.code(404).send({ error: "User not found or not in tenant" }) } await syncProfileBranches(server, id, branchIds, userId) await syncProfileTeams(server, id, teamIds, userId) const profileWithBranches = await loadProfileWithBranches(server, id, tenantId) const [profile] = profileWithBranches ? await enrichProfilesWithTeams(server, [profileWithBranches]) : [null] return enrichProfileWithCalendarSubscription(profile || updated[0]) } catch (err) { console.error("PUT /profiles/:id ERROR:", err) if (err instanceof Error && ["INVALID_BRANCH_SELECTION", "INVALID_PRIMARY_BRANCH"].includes(err.message)) { return reply.code(400).send({ error: "Ungültige Niederlassungsauswahl" }) } if (err instanceof Error && err.message === "INVALID_TEAM_SELECTION") { return reply.code(400).send({ error: "Ungültige Teamauswahl" }) } return reply.code(500).send({ error: "Internal Server Error" }) } }) server.post("/profiles/:id/calendar-subscription-token", async (req, reply) => { try { const tenantId = req.user?.tenant_id if (!tenantId) { return reply.code(400).send({ error: "No tenant selected" }) } const { id } = req.params as { id: string } const updatedProfile = await generateProfileCalendarSubscriptionToken(server, id, tenantId) if (!updatedProfile) { return reply.code(404).send({ error: "User not found or not in tenant" }) } const profileWithBranches = await loadProfileWithBranches(server, id, tenantId) const [profile] = profileWithBranches ? await enrichProfilesWithTeams(server, [profileWithBranches]) : [updatedProfile] return enrichProfileWithCalendarSubscription(profile) } catch (err) { console.error("POST /profiles/:id/calendar-subscription-token ERROR:", err) return reply.code(500).send({ error: "Internal Server Error" }) } }) }