Converted Routes

This commit is contained in:
2025-12-06 19:29:47 +01:00
parent 7450f90a0f
commit d895583ea2
5 changed files with 898 additions and 61 deletions

View File

@@ -30,6 +30,11 @@ import notificationsRoutes from "./routes/notifications";
import staffTimeRoutes from "./routes/staff/time";
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
//Resources
import customerRoutes from "./routes/resources/customers";
import vendorRoutes from "./routes/resources/vendors";
//M2M
import authM2m from "./plugins/auth.m2m";
import helpdeskInboundEmailRoutes from "./routes/helpdesk.inbound.email";
@@ -114,6 +119,10 @@ async function main() {
await subApp.register(staffTimeRoutes);
await subApp.register(staffTimeConnectRoutes);
await subApp.register(customerRoutes);
await subApp.register(vendorRoutes);
},{prefix: "/api"})
app.ready(async () => {

View File

@@ -1,79 +1,140 @@
import { FastifyInstance } from "fastify";
import { FastifyInstance } from "fastify"
import {
authUsers,
authTenantUsers,
tenants,
authProfiles,
authUserRoles,
authRoles,
authRolePermissions,
} from "../../../db/schema"
import { eq, and, or, isNull } from "drizzle-orm"
export default async function meRoutes(server: FastifyInstance) {
server.get("/me", async (req, reply) => {
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
try {
const authUser = req.user
if (!authUser) {
return reply.code(401).send({ error: "Unauthorized" })
}
if (!authUser) {
return reply.code(401).send({ error: "Unauthorized" })
}
const user_id = req.user.user_id
const tenant_id = req.user.tenant_id
const userId = authUser.user_id
const activeTenantId = authUser.tenant_id
// 1. User laden
const { data: user, error: userError } = await server.supabase
.from("auth_users")
.select("id, email, created_at, must_change_password")
.eq("id", authUser.user_id)
.single()
// ----------------------------------------------------
// 1) USER LADEN
// ----------------------------------------------------
const userResult = await server.db
.select({
id: authUsers.id,
email: authUsers.email,
created_at: authUsers.created_at,
must_change_password: authUsers.must_change_password,
})
.from(authUsers)
.where(eq(authUsers.id, userId))
.limit(1)
if (userError || !user) {
return reply.code(401).send({ error: "User not found" })
}
const user = userResult[0]
// 2. Tenants laden (alle Tenants des Users)
const { data: tenantLinks, error: tenantLinksError } = await server.supabase
.from("auth_users")
.select(`*, tenants!auth_tenant_users ( id, name,short, locked, extraModules, businessInfo, numberRanges, dokuboxkey, standardEmailForInvoices, standardPaymentDays )`)
.eq("id", authUser.user_id)
.single();
if (!user) {
return reply.code(401).send({ error: "User not found" })
}
if (tenantLinksError) {
// ----------------------------------------------------
// 2) TENANTS LADEN
// ----------------------------------------------------
const tenantRows = await server.db
.select({
id: tenants.id,
name: tenants.name,
short: tenants.short,
locked: tenants.locked,
extraModules: tenants.extraModules,
businessInfo: tenants.businessInfo,
numberRanges: tenants.numberRanges,
dokuboxkey: tenants.dokuboxkey,
standardEmailForInvoices: tenants.standardEmailForInvoices,
standardPaymentDays: tenants.standardPaymentDays,
})
.from(authTenantUsers)
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
.where(eq(authTenantUsers.user_id, userId))
console.log(tenantLinksError)
const tenantList = tenantRows ?? []
return reply.code(401).send({ error: "Tenant Error" })
}
// ----------------------------------------------------
// 3) ACTIVE TENANT
// ----------------------------------------------------
const activeTenant = activeTenantId
const tenants = tenantLinks?.tenants
// ----------------------------------------------------
// 4) PROFIL LADEN
// ----------------------------------------------------
let profile = null
if (activeTenantId) {
const profileResult = await server.db
.select()
.from(authProfiles)
.where(
and(
eq(authProfiles.user_id, userId),
eq(authProfiles.tenant_id, activeTenantId)
)
)
.limit(1)
// 3. Aktiven Tenant bestimmen
const activeTenant = authUser.tenant_id /*|| tenants[0].id*/
profile = profileResult?.[0] ?? null
}
// 4. Profil für den aktiven Tenant laden
let profile = null
if (activeTenant) {
const { data: profileData } = await server.supabase
.from("auth_profiles")
.select("*")
.eq("user_id", user.id)
.eq("tenant_id", activeTenant)
.single()
// ----------------------------------------------------
// 5) PERMISSIONS — RPC ERSETZT
// ----------------------------------------------------
const permissionRows =
(await server.db
.select({
permission: authRolePermissions.permission,
})
.from(authUserRoles)
.innerJoin(
authRoles,
and(
eq(authRoles.id, authUserRoles.role_id),
or(
isNull(authRoles.tenant_id), // globale Rolle
eq(authRoles.tenant_id, activeTenantId) // tenant-spezifische Rolle
)
)
)
.innerJoin(
authRolePermissions,
eq(authRolePermissions.role_id, authRoles.id)
)
.where(
and(
eq(authUserRoles.user_id, userId),
eq(authUserRoles.tenant_id, activeTenantId)
)
)) ?? []
profile = profileData
}
const permissions = Array.from(
new Set(permissionRows.map((p) => p.permission))
)
// 5. Permissions laden (über Funktion)
const { data: permissionsData, error: permissionsError } = await server.supabase
.rpc("auth_get_user_permissions", {
uid: user.id,
tid: activeTenant || null
})
if(permissionsError) {
console.log(permissionsError)
}
const permissions = permissionsData.map(i => i.permission) || []
// 6. Response zurückgeben
return {
user,
tenants,
activeTenant,
profile,
permissions
// ----------------------------------------------------
// RESPONSE
// ----------------------------------------------------
return {
user,
tenants: tenantList,
activeTenant,
profile,
permissions,
}
} catch (err: any) {
console.error("ERROR in /me route:", err)
return reply.code(500).send({ error: "Internal server error" })
}
})
}
}

View File

@@ -0,0 +1,155 @@
import { FastifyInstance } from "fastify"
import { asc, desc, eq, ilike, and } from "drizzle-orm"
// Beispiel-Import — wird in der echten Datei ersetzt
import { exampleTable } from "../../../db/schema/exampleTable"
export default async function exampleRoutes(server: FastifyInstance) {
// -------------------------------------------
// LIST (ohne Pagination)
// -------------------------------------------
server.get("/resource/example", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let query = server.db
.select()
.from(exampleTable)
.where(eq(exampleTable.tenant, tenantId))
// 🔍 OPTIONAL: einfache Suche (LIKE)
if (search) {
query = server.db
.select()
.from(exampleTable)
.where(
and(
eq(exampleTable.tenant, tenantId),
ilike(exampleTable.name as any, `%${search}%`)
)
)
}
// 🔄 Sortierung
if (sort) {
query = query.orderBy(
ascQuery === "true"
? asc((exampleTable as any)[sort])
: desc((exampleTable as any)[sort])
)
}
const results = await query
return results
})
// -------------------------------------------
// PAGINATED LIST
// -------------------------------------------
server.get("/resource/example/paginated", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const {
offset = "0",
limit = "25",
search,
sort,
asc: ascQuery,
} = req.query as {
offset?: string
limit?: string
search?: string
sort?: string
asc?: string
}
const offsetNum = parseInt(offset)
const limitNum = parseInt(limit)
// --- Basis WHERE
let whereClause: any = eq(exampleTable.tenant, tenantId)
// --- Suche
if (search) {
whereClause = and(
eq(exampleTable.tenant, tenantId),
ilike(exampleTable.name as any, `%${search}%`)
)
}
// -------------------------
// 1. COUNT Query (total rows)
// -------------------------
const totalRowsResult = await server.db
.select({ count: server.db.fn.count(exampleTable.id) })
.from(exampleTable)
.where(whereClause)
const total = Number(totalRowsResult[0].count)
// -------------------------
// 2. DATA Query
// -------------------------
let dataQuery = server.db
.select()
.from(exampleTable)
.where(whereClause)
.offset(offsetNum)
.limit(limitNum)
if (sort) {
dataQuery = dataQuery.orderBy(
ascQuery === "true"
? asc((exampleTable as any)[sort])
: desc((exampleTable as any)[sort])
)
}
const rows = await dataQuery
return {
data: rows,
pagination: {
total,
offset: offsetNum,
limit: limitNum,
totalPages: Math.ceil(total / limitNum),
},
}
})
// -------------------------------------------
// DETAIL ROUTE
// -------------------------------------------
server.get("/resource/example/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const result = await server.db
.select()
.from(exampleTable)
.where(
and(
eq(exampleTable.id, id),
eq(exampleTable.tenant, tenantId)
)
)
.limit(1)
if (!result.length) {
return reply.code(404).send({ error: "Not found" })
}
return result[0]
})
}

View File

@@ -0,0 +1,328 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and, count, inArray,
} from "drizzle-orm"
import {
customers,
projects,
plants,
contracts,
contacts,
createddocuments,
statementallocations,
files,
events,
} from "../../../db/schema" // dein zentraler index.ts Export
export default async function customerRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST
// -------------------------------------------------------------
server.get("/resource/customers", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
// Basisquery
let baseQuery = server.db
.select()
.from(customers)
.where(eq(customers.tenant, tenantId))
// Suche
if (search) {
baseQuery = server.db
.select()
.from(customers)
.where(
and(
eq(customers.tenant, tenantId),
ilike(customers.name, `%${search}%`)
)
)
}
// Sortierung
if (sort) {
const field = (customers as any)[sort]
if (field) {
// @ts-ignore
baseQuery = baseQuery.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
const customerList = await baseQuery
return customerList
})
// -------------------------------------------------------------
// PAGINATED
// -------------------------------------------------------------
server.get("/resource/customers/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" })
}
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled
} = queryConfig
const {
select,
search,
searchColumns,
distinctColumns
} = req.query as {
select?: string
search?: string
searchColumns?: string
distinctColumns?: string
}
// ----------------------------
// WHERE CONDITIONS
// ----------------------------
let whereCond: any = eq(customers.tenant, tenantId)
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (customers as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else if (val === true || val === false || val === null) {
whereCond = and(whereCond, eq(col, val))
} else {
whereCond = and(whereCond, eq(col, val))
}
}
}
if (search && search.trim().length > 0) {
const searchTerm = `%${search.trim().toLowerCase()}%`
whereCond = and(
whereCond,
ilike(customers.name, searchTerm)
)
}
// ----------------------------
// COUNT FIX (Drizzle-safe)
// ----------------------------
const totalRes = await server.db
.select({ value: count(customers.id) })
.from(customers)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
// ----------------------------
// DISTINCT VALUES (optional)
// ----------------------------
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (customers as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(customers)
.where(eq(customers.tenant, tenantId))
const values = rows
.map(r => r.v)
.filter(v => v != null && v !== "")
distinctValues[colName] = [...new Set(values)].sort()
}
}
// ----------------------------
// PAGINATION
// ----------------------------
let offset = 0
let limit = 999999
if (!paginationDisabled && pagination) {
offset = pagination.offset
limit = pagination.limit
}
// ----------------------------
// ORDER BY
// ----------------------------
let orderField = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (customers as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
// ----------------------------
// QUERY DATA
// ----------------------------
let dataQuery = server.db
.select()
.from(customers)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
// ----------------------------
// BUILD RETURN CONFIG
// ----------------------------
const totalPages = pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
}
return {
data,
queryConfig: enrichedConfig,
}
}
catch (e) {
console.log(e)
}
})
// -------------------------------------------------------------
// DETAIL (mit ALLEN JOINS)
// -------------------------------------------------------------
server.get("/resource/customers/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
// --- 1) Customer selbst laden
const customerRecord = await server.db
.select()
.from(customers)
.where(and(eq(customers.id, Number(id)), eq(customers.tenant, tenantId)))
.limit(1)
if (!customerRecord.length) {
return reply.code(404).send({ error: "Customer not found" })
}
const customer = customerRecord[0]
// --- 2) Relations:
const [
customerProjects,
customerPlants,
customerContracts,
customerContacts,
customerDocuments,
customerFiles,
customerEvents,
] = await Promise.all([
// Projekte, die dem Kunden zugeordnet sind
server.db
.select()
.from(projects)
.where(eq(projects.customer, Number(id))),
server.db
.select()
.from(plants)
.where(eq(plants.customer, Number(id))),
server.db
.select()
.from(contracts)
.where(eq(contracts.customer, Number(id))),
server.db
.select()
.from(contacts)
.where(eq(contacts.customer, Number(id))),
// createddocuments + inner join statementallocations
server.db
.select({
...createddocuments,
allocations: statementallocations,
})
.from(createddocuments)
.leftJoin(
statementallocations,
eq(statementallocations.cd_id, createddocuments.id)
)
.where(eq(createddocuments.customer, Number(id))),
server.db
.select()
.from(files)
.where(eq(files.customer, Number(id))),
server.db
.select()
.from(events)
.where(eq(events.customer, Number(id))),
])
return {
...customer,
projects: customerProjects,
plants: customerPlants,
contracts: customerContracts,
contacts: customerContacts,
createddocuments: customerDocuments,
files: customerFiles,
events: customerEvents,
}
})
}

View File

@@ -0,0 +1,284 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and,
count,
inArray
} from "drizzle-orm"
import {
vendors,
contacts,
files,
} from "../../../db/schema"
export default async function vendorRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST
// -------------------------------------------------------------
server.get("/resource/vendors", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let baseQuery = server.db
.select()
.from(vendors)
.where(eq(vendors.tenant, tenantId))
// 🔎 Suche
if (search) {
baseQuery = server.db
.select()
.from(vendors)
.where(
and(
eq(vendors.tenant, tenantId),
ilike(vendors.name, `%${search}%`)
)
)
}
// 🔽 Sortierung
if (sort) {
const field = (vendors as any)[sort]
if (field) {
// @ts-ignore
baseQuery = baseQuery.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
return await baseQuery
} catch (err) {
console.error("ERROR /resource/vendors:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// PAGINATED
// -------------------------------------------------------------
server.get("/resource/vendors/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" })
}
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled
} = queryConfig
const {
select,
search,
searchColumns,
distinctColumns
} = req.query as {
select?: string
search?: string
searchColumns?: string
distinctColumns?: string
}
// ----------------------------
// WHERE CONDITIONS
// ----------------------------
let whereCond: any = eq(vendors.tenant, tenantId)
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (vendors as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else if (val === true || val === false || val === null) {
whereCond = and(whereCond, eq(col, val))
} else {
whereCond = and(whereCond, eq(col, val))
}
}
}
if (search && search.trim().length > 0) {
whereCond = and(
whereCond,
ilike(vendors.name, `%${search.trim().toLowerCase()}%`)
)
}
// ----------------------------
// COUNT
// ----------------------------
const totalRes = await server.db
.select({ value: count(vendors.id) })
.from(vendors)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
// ----------------------------
// DISTINCT FILTER VALUES
// ----------------------------
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (vendors as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(vendors)
.where(eq(vendors.tenant, tenantId))
const values = rows
.map(r => r.v)
.filter(v => v != null && v !== "")
distinctValues[colName] = [...new Set(values)].sort()
}
}
// ----------------------------
// PAGINATION
// ----------------------------
let offset = 0
let limit = 999999
if (!paginationDisabled && pagination) {
offset = pagination.offset
limit = pagination.limit
}
// ----------------------------
// ORDER BY
// ----------------------------
let orderField = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (vendors as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
// ----------------------------
// QUERY DATA
// ----------------------------
let dataQuery = server.db
.select()
.from(vendors)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
const totalPages = pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
}
return { data, queryConfig: enrichedConfig }
} catch (err) {
console.error("ERROR /resource/vendors/paginated:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// DETAIL (mit JOINS)
// -------------------------------------------------------------
server.get("/resource/vendors/:id", async (req, reply) => {
try {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const vendorId = Number(id)
const vendorRecord = await server.db
.select()
.from(vendors)
.where(
and(
eq(vendors.id, vendorId),
eq(vendors.tenant, tenantId)
)
)
.limit(1)
if (!vendorRecord.length) {
return reply.code(404).send({ error: "Vendor not found" })
}
const vendor = vendorRecord[0]
// ----------------------------
// RELATIONS
// ----------------------------
const [
vendorContacts,
vendorFiles,
] = await Promise.all([
server.db
.select()
.from(contacts)
.where(eq(contacts.vendor, vendorId)),
server.db
.select()
.from(files)
.where(eq(files.vendor, vendorId)),
])
return {
...vendor,
contacts: vendorContacts,
files: vendorFiles,
}
} catch (err) {
console.error("ERROR /resource/vendors/:id:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
}