diff --git a/src/index.ts b/src/index.ts index 5a54508..fa4a204 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,6 @@ import resourceRoutesSpecial from "./routes/resourcesSpecial"; import fastifyCookie from "@fastify/cookie"; import historyRoutes from "./routes/history"; import fileRoutes from "./routes/files"; -import userRoutes from "./routes/auth/user" import functionRoutes from "./routes/functions"; import bankingRoutes from "./routes/banking"; import exportRoutes from "./routes/exports" @@ -30,8 +29,6 @@ import staffTimeRoutes from "./routes/staff/time"; import staffTimeConnectRoutes from "./routes/staff/timeconnects"; //Resources -import productsAndServicesRoutes from "./routes/resources/productsServices"; - import resourceRoutes from "./routes/resources/main"; //M2M @@ -106,7 +103,6 @@ async function main() { await subApp.register(resourceRoutesSpecial); await subApp.register(historyRoutes); await subApp.register(fileRoutes); - await subApp.register(userRoutes); await subApp.register(functionRoutes); await subApp.register(bankingRoutes); await subApp.register(exportRoutes); @@ -118,7 +114,6 @@ async function main() { await subApp.register(staffTimeConnectRoutes); - await subApp.register(productsAndServicesRoutes); await subApp.register(resourceRoutes); },{prefix: "/api"}) diff --git a/src/resource.config.ts b/src/resource.config.ts new file mode 100644 index 0000000..47be3c3 --- /dev/null +++ b/src/resource.config.ts @@ -0,0 +1,114 @@ +import { + contacts, + contracts, costcentres, createddocuments, + customers, + files, filetags, folders, hourrates, inventoryitemgroups, + inventoryitems, letterheads, ownaccounts, + plants, productcategories, products, + projects, + projecttypes, servicecategories, services, spaces, tasks, texttemplates, units, vehicles, + vendors +} from "../db/schema"; + +export const resourceConfig = { + projects: { + searchColumns: ["name"], + mtoLoad: ["customer","plant","contract","projecttype"], + mtmLoad: ["tasks", "files"], + table: projects + }, + customers: { + searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"], + mtmLoad: ["contacts","projects"], + table: customers, + }, + contacts: { + searchColumns: ["firstName", "lastName", "email", "phone", "notes"], + table: contacts, + mtoLoad: ["customer","vendor"] + }, + contracts: { + table: contracts, + searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"] + }, + plants: { + table: plants, + mtoLoad: ["customer"], + mtmLoad: ["projects","tasks","files"], + }, + projecttypes: { + table: projecttypes + }, + vendors: { + table: vendors, + searchColumns: ["name","vendorNumber","notes","defaultPaymentType"], + }, + files: { + table: files + }, + folders: { + table: folders + }, + filetags: { + table: filetags + }, + inventoryitems: { + table: inventoryitems + }, + inventoryitemgroups: { + table: inventoryitemgroups + }, + products: { + table: products, + searchColumns: ["name","manufacturer","ean","barcode","description","manfacturer_number","article_number"], + }, + productcategories: { + table: productcategories + }, + services: { + table: services, + mtoLoad: ["unit"], + searchColumns: ["name","description"], + }, + servicecategories: { + table: servicecategories + }, + units: { + table: units, + }, + vehicles: { + table: vehicles, + searchColumns: ["name","license_plate","vin","color"], + }, + hourrates: { + table: hourrates, + searchColumns: ["name"], + }, + spaces: { + table: spaces, + searchColumns: ["name","space_number","type","info_data"], + }, + ownaccounts: { + table: ownaccounts, + searchColumns: ["name","description","number"], + }, + costcentres: { + table: costcentres, + searchColumns: ["name","number","description"], + mtoLoad: ["vehicle","project","inventoryitem"] + }, + tasks: { + table: tasks, + }, + letterheads: { + table: letterheads, + + }, + createddocuments: { + table: createddocuments, + mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead",] + }, + texttemplates: { + table: texttemplates + } +} \ No newline at end of file diff --git a/src/routes/admin.ts b/src/routes/admin.ts index ebf3e49..9120d18 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -1,94 +1,117 @@ import { FastifyInstance } from "fastify"; +import { eq } from "drizzle-orm"; + +import { + authTenantUsers, + authUsers, + tenants, +} from "../../db/schema"; export default async function adminRoutes(server: FastifyInstance) { + + // ------------------------------------------------------------- + // POST /admin/add-user-to-tenant + // ------------------------------------------------------------- server.post("/admin/add-user-to-tenant", async (req, reply) => { - const body = req.body as { - user_id: string; - tenant_id: string; - role?: string; - mode?: "single" | "multi"; - }; + try { + const body = req.body as { + user_id: string; + tenant_id: number; + role?: string; + mode?: "single" | "multi"; + }; - if (!body.user_id || !body.tenant_id) { - return reply.code(400).send({ error: "user_id and tenant_id required" }); + if (!body.user_id || !body.tenant_id) { + return reply.code(400).send({ + error: "user_id and tenant_id required" + }); + } + + const mode = body.mode ?? "multi"; + + // ---------------------------- + // SINGLE MODE → alte Verknüpfungen löschen + // ---------------------------- + if (mode === "single") { + await server.db + .delete(authTenantUsers) + .where(eq(authTenantUsers.user_id, body.user_id)); + } + + // ---------------------------- + // Neue Verknüpfung hinzufügen + // ---------------------------- + + await server.db + .insert(authTenantUsers) + // @ts-ignore + .values({ + user_id: body.user_id, + tenantId: body.tenant_id, + role: body.role ?? "member", + }); + + return { success: true, mode }; + + } catch (err) { + console.error("ERROR /admin/add-user-to-tenant:", err); + return reply.code(500).send({ error: "Internal Server Error" }); } - - // Default: "multi" - const mode = body.mode ?? "multi"; - - if (mode === "single") { - // Erst alle alten Verknüpfungen löschen - await server.supabase - .from("auth_tenant_users") - .delete() - .eq("user_id", body.user_id); - } - - const { error } = await server.supabase - .from("auth_tenant_users") - .insert({ - tenant_id: body.tenant_id, - user_id: body.user_id, - role: body.role ?? "member", - }); - - if (error) { - return reply.code(400).send({ error: error.message }); - } - - // Neuen Eintrag setzen - - - return { success: true, mode }; }); - /** - * Alle Tenants eines Users abfragen - */ + + // ------------------------------------------------------------- + // GET /admin/user-tenants/:user_id + // ------------------------------------------------------------- server.get("/admin/user-tenants/:user_id", async (req, reply) => { - const { user_id } = req.params as { user_id: string }; + try { + const { user_id } = req.params as { user_id: string }; - if (!user_id) { - return reply.code(400).send({ error: "user_id required" }); + if (!user_id) { + return reply.code(400).send({ error: "user_id required" }); + } + + // ---------------------------- + // 1) User existiert? + // ---------------------------- + const [user] = await server.db + .select() + .from(authUsers) + .where(eq(authUsers.id, user_id)) + .limit(1); + + if (!user) { + return reply.code(400).send({ error: "faulty user_id presented" }); + } + + // ---------------------------- + // 2) Tenants Join über auth_tenant_users + // ---------------------------- + const tenantRecords = await server.db + .select({ + id: tenants.id, + name: tenants.name, + short: tenants.short, + locked: tenants.locked, + numberRanges: tenants.numberRanges, + extraModules: tenants.extraModules, + }) + .from(authTenantUsers) + .innerJoin( + tenants, + eq(authTenantUsers.tenant_id, tenants.id) + ) + .where(eq(authTenantUsers.user_id, user_id)); + + return { + user_id, + tenants: tenantRecords, + }; + + } catch (err) { + console.error("ERROR /admin/user-tenants:", err); + return reply.code(500).send({ error: "Internal Server Error" }); } - - const {data:user, error: userError} = await server.supabase.from("auth_users").select("*,tenants(*)").eq("id", user_id).single(); - - console.log(userError) - console.log(user) - - if(!user) { - return reply.code(400).send({ error: "faulty user_id presented" }); - } else { - return { user_id, tenants: user.tenants }; - } - }); - /** - * Alle User eines Tenants abfragen - * TODO: Aktuell nur Multi Tenant - */ - /*server.get("/admin/tenant-users/:tenant_id", async (req, reply) => { - const { tenant_id } = req.params as { tenant_id: string }; - - if (!tenant_id) { - return reply.code(400).send({ error: "tenant_id required" }); - } - - const { data, error } = await server.supabase - .from("auth_tenant_users") - .select(` - user_id, - role, - users ( id, email, created_at ) - `) - .eq("tenant_id", tenant_id); - - if (error) { - return reply.code(400).send({ error: error.message }); - } - - return { tenant_id, users: data }; - });*/ -} \ No newline at end of file +} diff --git a/src/routes/auth/auth-authenticated.ts b/src/routes/auth/auth-authenticated.ts index 93e254c..b7626c0 100644 --- a/src/routes/auth/auth-authenticated.ts +++ b/src/routes/auth/auth-authenticated.ts @@ -1,12 +1,15 @@ -import { FastifyInstance } from "fastify"; -import bcrypt from "bcrypt"; +import { FastifyInstance } from "fastify" +import bcrypt from "bcrypt" +import { eq } from "drizzle-orm" +import { authUsers } from "../../../db/schema" // wichtig: Drizzle Schema importieren! export default async function authRoutesAuthenticated(server: FastifyInstance) { + server.post("/auth/password/change", { schema: { tags: ["Auth"], - summary: "Reset Password after forced change", + summary: "Change password (after login or forced reset)", body: { type: "object", required: ["old_password", "new_password"], @@ -25,54 +28,69 @@ export default async function authRoutesAuthenticated(server: FastifyInstance) { }, }, }, async (req, reply) => { - const { old_password, new_password } = req.body as { old_password: string; new_password: string }; - console.log(req.user) + try { + const { old_password, new_password } = req.body as { + old_password: string + new_password: string + } - const user_id = req.user?.user_id; // kommt aus JWT Middleware - if (!user_id) { - // @ts-ignore - return reply.code(401).send({ error: "Unauthorized" }); + const userId = req.user?.user_id + if (!userId) { + //@ts-ignore + return reply.code(401).send({ error: "Unauthorized" }) + } + + // ----------------------------------------------------- + // 1) User laden + // ----------------------------------------------------- + const [user] = await server.db + .select({ + id: authUsers.id, + passwordHash: authUsers.passwordHash, + mustChangePassword: authUsers.must_change_password + }) + .from(authUsers) + .where(eq(authUsers.id, userId)) + .limit(1) + + if (!user) { + //@ts-ignore + return reply.code(404).send({ error: "User not found" }) + } + + // ----------------------------------------------------- + // 2) Altes PW prüfen + // ----------------------------------------------------- + const valid = await bcrypt.compare(old_password, user.passwordHash) + if (!valid) { + //@ts-ignore + return reply.code(401).send({ error: "Old password incorrect" }) + } + + // ----------------------------------------------------- + // 3) Neues PW hashen + // ----------------------------------------------------- + const newHash = await bcrypt.hash(new_password, 10) + + // ----------------------------------------------------- + // 4) Updaten + // ----------------------------------------------------- + await server.db + .update(authUsers) + .set({ + passwordHash: newHash, + must_change_password: false, + updatedAt: new Date(), + }) + .where(eq(authUsers.id, userId)) + + return { success: true } + + } catch (err) { + console.error("POST /auth/password/change ERROR:", err) + //@ts-ignore + return reply.code(500).send({ error: "Internal Server Error" }) } - - // Nutzer laden - const { data: user, error } = await server.supabase - .from("auth_users") - .select("id, password_hash, must_change_password") - .eq("id", user_id) - .single(); - - if (error || !user) { - // @ts-ignore - return reply.code(404).send({ error: "User not found" }); - } - - // Altes Passwort prüfen - const valid = await bcrypt.compare(old_password, user.password_hash); - if (!valid) { - // @ts-ignore - return reply.code(401).send({ error: "Old password incorrect" }); - } - - // Neues Passwort hashen - const newHash = await bcrypt.hash(new_password, 10); - - // Speichern + Flag zurücksetzen - const { error: updateError } = await server.supabase - .from("auth_users") - .update({ - password_hash: newHash, - must_change_password: false, - updated_at: new Date().toISOString(), - }) - .eq("id", user_id); - - if (updateError) { - console.log(updateError); - // @ts-ignore - return reply.code(500).send({ error: "Password update failed" }); - } - - return { success: true }; - }); -} \ No newline at end of file + }) +} diff --git a/src/routes/emailAsUser.ts b/src/routes/emailAsUser.ts index 45693b3..525fa25 100644 --- a/src/routes/emailAsUser.ts +++ b/src/routes/emailAsUser.ts @@ -1,169 +1,31 @@ import nodemailer from "nodemailer" +import { FastifyInstance } from "fastify" +import { eq } from "drizzle-orm" + +import { sendMailAsUser } from "../utils/emailengine" +import { encrypt, decrypt } from "../utils/crypt" +import { userCredentials } from "../../db/schema" +// Pfad ggf. anpassen -import { FastifyInstance } from "fastify"; -import {sendMailAsUser} from "../utils/emailengine"; -import {encrypt, decrypt} from "../utils/crypt" -import {secrets} from "../utils/secrets"; // @ts-ignore -import MailComposer from 'nodemailer/lib/mail-composer/index.js' - -import {ImapFlow} from "imapflow" +import MailComposer from "nodemailer/lib/mail-composer/index.js" +import { ImapFlow } from "imapflow" export default async function emailAsUserRoutes(server: FastifyInstance) { - // Create E-Mail Account + + // ====================================================================== + // CREATE OR UPDATE EMAIL ACCOUNT + // ====================================================================== server.post("/email/accounts/:id?", async (req, reply) => { - if (!req.user?.tenant_id) { - return reply.code(400).send({ error: "No tenant selected" }); - } - - const { id } = req.params as { id: string }; - - - const body = req.body as { - email: string - password: string - smtp_host: string - smtp_port: number - smtp_ssl: boolean - imap_host: string - imap_port: number - imap_ssl: boolean - }; - - if(id) { - //SAVE Existing - let saveData = { - email_encrypted: body.email ? encrypt(body.email) : undefined, - password_encrypted: body.password ? encrypt(body.password) : undefined, - smtp_host_encrypted: body.smtp_host ? encrypt(body.smtp_host) : undefined, - smtp_port: body.smtp_port, - smtp_ssl: body.smtp_ssl, - imap_host_encrypted: body.imap_host ? encrypt(body.imap_host) : undefined, - imap_port: body.imap_port, - imap_ssl: body.imap_ssl, - } - - - const { data, error } = await server.supabase - .from("user_credentials") - .update(saveData) - .eq("id", id) - .select("*") - .single(); - - if (error) { - return reply.code(400).send({ error: error.message }); - } else { - return reply.send({success: true}) - } - } else { - //Create New - let createData = { - user_id: req.user.user_id, - email_encrypted: encrypt(body.email), - password_encrypted: encrypt(body.password), - tenant_id: req.user.tenant_id, - smtp_host_encrypted: encrypt(body.smtp_host), - smtp_port: body.smtp_port, - smtp_ssl: body.smtp_ssl, - type: "mail", - imap_host_encrypted: encrypt(body.imap_host), - imap_port: body.imap_port, - imap_ssl: body.imap_ssl, - } - - - const { data, error } = await server.supabase - .from("user_credentials") - .insert(createData) - .select("*") - .single(); - - if (error) { - return reply.code(400).send({ error: error.message }); - } else { - return reply.send({success: true}) - } - } - - - - }); - - server.get("/email/accounts/:id?", async (req, reply) => { - if (!req.user?.tenant_id) { - return reply.code(400).send({ error: "No tenant selected" }); - } - - const { id } = req.params as { id: string }; - - if(id) { - let returnData = {} - // @ts-ignore - const { data, error } = await server.supabase - .from("user_credentials") - .select("id, email_encrypted, smtp_host_encrypted, smtp_port, smtp_ssl, imap_host_encrypted, imap_port, imap_ssl, user_id, tenant_id") - .eq("id", id) - .eq("tenant_id", req.user.tenant_id) - .eq("type", "mail") - .single(); - - if (error || !data) { - return reply.code(404).send({ error: "Not found" }); - } else { - Object.keys(data).forEach((key) => { - if(key.includes("encrypted")){ - returnData[key.substring(0,key.length-10)] = decrypt(data[key]) - } else { - returnData[key] = data[key] - } - }) - } - - return returnData; - } else { - - const { data, error } = await server.supabase - .from("user_credentials") - .select("id, email_encrypted, user_id, tenant_id") - .eq("tenant_id", req.user.tenant_id) - .eq("type", "mail") - - let accounts = [] - data.forEach(item => { - let temp = {} - Object.keys(item).forEach((key) => { - if(key.includes("encrypted")){ - temp[key.substring(0,key.length-10)] = decrypt(item[key]) - } else { - temp[key] = item[key] - } - }) - accounts.push(temp) - }) - - - - return accounts - } - }); - - server.post("/email/send", async (req, reply) => { - const body = req.body as { - to: string - cc?: string - bcc?: string - subject?: string - text?: string - html?: string - attachments?: any, - account: string - } - try { + if (!req.user?.tenant_id) { + return reply.code(400).send({ error: "No tenant selected" }) + } - let accountData = {} as { + const { id } = req.params as { id?: string } + + const body = req.body as { email: string password: string smtp_host: string @@ -173,32 +35,173 @@ export default async function emailAsUserRoutes(server: FastifyInstance) { imap_port: number imap_ssl: boolean } - // @ts-ignore - const { data, error } = await server.supabase - .from("user_credentials") - .select("id, email_encrypted,password_encrypted, smtp_host_encrypted, smtp_port, smtp_ssl,imap_host_encrypted,imap_port, imap_ssl, user_id, tenant_id") - .eq("id", body.account) - .eq("tenant_id", req.user.tenant_id) - .eq("type", "mail") - .single(); - if (error || !data) { - return reply.code(404).send({ error: "Not found" }); - } else { - Object.keys(data).forEach((key) => { - if(key.includes("encrypted")){ - accountData[key.substring(0,key.length-10)] = decrypt(data[key]) - } else { - accountData[key] = data[key] - } - }) + // ----------------------------- + // UPDATE EXISTING + // ----------------------------- + if (id) { + const saveData = { + emailEncrypted: body.email ? encrypt(body.email) : undefined, + passwordEncrypted: body.password ? encrypt(body.password) : undefined, + smtpHostEncrypted: body.smtp_host ? encrypt(body.smtp_host) : undefined, + smtpPort: body.smtp_port, + smtpSsl: body.smtp_ssl, + imapHostEncrypted: body.imap_host ? encrypt(body.imap_host) : undefined, + imapPort: body.imap_port, + imapSsl: body.imap_ssl, + } + + await server.db + .update(userCredentials) + .set(saveData) + .where(eq(userCredentials.id, id)) + + return reply.send({ success: true }) } + // ----------------------------- + // CREATE NEW + // ----------------------------- + const insertData = { + userId: req.user.user_id, + tenantId: req.user.tenant_id, + type: "mail", + emailEncrypted: encrypt(body.email), + passwordEncrypted: encrypt(body.password), + + smtpHostEncrypted: encrypt(body.smtp_host), + smtpPort: body.smtp_port, + smtpSsl: body.smtp_ssl, + + imapHostEncrypted: encrypt(body.imap_host), + imapPort: body.imap_port, + imapSsl: body.imap_ssl, + } + + await server.db.insert(userCredentials).values(insertData) + + return reply.send({ success: true }) + } catch (err) { + console.error("POST /email/accounts error:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ====================================================================== + // GET SINGLE OR ALL ACCOUNTS + // ====================================================================== + server.get("/email/accounts/:id?", async (req, reply) => { + try { + if (!req.user?.tenant_id) { + return reply.code(400).send({ error: "No tenant selected" }) + } + + const { id } = req.params as { id?: string } + + // ============================================================ + // LOAD SINGLE ACCOUNT + // ============================================================ + if (id) { + const rows = await server.db + .select() + .from(userCredentials) + .where(eq(userCredentials.id, id)) + + const row = rows[0] + if (!row) return reply.code(404).send({ error: "Not found" }) + + const returnData: any = {} + + Object.entries(row).forEach(([key, val]) => { + if (key.endsWith("Encrypted")) { + const cleanKey = key.replace("Encrypted", "") + // @ts-ignore + returnData[cleanKey] = decrypt(val as string) + } else { + returnData[key] = val + } + }) + + return reply.send(returnData) + } + + // ============================================================ + // LOAD ALL ACCOUNTS FOR TENANT + // ============================================================ + const rows = await server.db + .select() + .from(userCredentials) + .where(eq(userCredentials.tenantId, req.user.tenant_id)) + + const accounts = rows.map(row => { + const temp: any = {} + Object.entries(row).forEach(([key, val]) => { + if (key.endsWith("Encrypted")) { + // @ts-ignore + temp[key.replace("Encrypted", "")] = decrypt(val as string) + } else { + temp[key] = val + } + }) + return temp + }) + + return reply.send(accounts) + + } catch (err) { + console.error("GET /email/accounts error:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ====================================================================== + // SEND EMAIL + SAVE IN IMAP SENT FOLDER + // ====================================================================== + server.post("/email/send", async (req, reply) => { + try { + const body = req.body as { + to: string + cc?: string + bcc?: string + subject?: string + text?: string + html?: string + attachments?: any + account: string + } + + // Fetch email credentials + const rows = await server.db + .select() + .from(userCredentials) + .where(eq(userCredentials.id, body.account)) + + const row = rows[0] + if (!row) return reply.code(404).send({ error: "Account not found" }) + + const accountData: any = {} + + Object.entries(row).forEach(([key, val]) => { + if (key.endsWith("Encrypted")) { + // @ts-ignore + accountData[key.replace("Encrypted", "")] = decrypt(val as string) + } else { + accountData[key] = val + } + }) + + // ------------------------- + // SEND EMAIL VIA SMTP + // ------------------------- const transporter = nodemailer.createTransport({ - host: accountData.smtp_host, - port: accountData.smtp_port, - secure: accountData.smtp_ssl, + host: accountData.smtpHost, + port: accountData.smtpPort, + secure: accountData.smtpSsl, auth: { user: accountData.email, pass: accountData.password, @@ -208,62 +211,48 @@ export default async function emailAsUserRoutes(server: FastifyInstance) { const message = { from: accountData.email, to: body.to, - cc: body.cc ? body.cc : undefined, - bcc: body.bcc ? body.bcc : undefined, + cc: body.cc, + bcc: body.bcc, subject: body.subject, - html: body.html ? body.html : undefined, + html: body.html, text: body.text, - attachments: body.attachments ? body.attachments : undefined, + attachments: body.attachments, } - const info = await transporter.sendMail(message) - const imapClient = new ImapFlow({ - host: accountData.imap_host, - port: accountData.imap_port, - secure: accountData.imap_ssl, + // ------------------------- + // SAVE TO IMAP SENT FOLDER + // ------------------------- + const imap = new ImapFlow({ + host: accountData.imapHost, + port: accountData.imapPort, + secure: accountData.imapSsl, auth: { user: accountData.email, pass: accountData.password, }, - logger: false }) - await imapClient.connect() + await imap.connect() const mail = new MailComposer(message) + const raw = await mail.compile().build() - const raw = await mail.compile().build() // → Buffer mit kompletter MIME - - - for await (const mailbox of await imapClient.list()) { - // mailbox.flags enthält z. B. ['\\Sent', '\\HasChildren'] - console.log(mailbox.specialUse) - if (mailbox.specialUse == '\\Sent') { - console.log('📨 Sent folder gefunden:', mailbox.path) - await imapClient.mailboxOpen(mailbox.path) - - await imapClient.append(mailbox.path, raw, ['\\Seen']) - - await imapClient.logout() - - break + for await (const mailbox of await imap.list()) { + if (mailbox.specialUse === "\\Sent") { + await imap.mailboxOpen(mailbox.path) + await imap.append(mailbox.path, raw, ["\\Seen"]) + await imap.logout() } } - if(info.response.includes("OK")){ - reply.send({success: true}) - }{ - reply.status(500) - } - + return reply.send({ success: true }) } catch (err) { - console.log(err) - reply.code(500).send({ error: "Failed to send E-Mail as User" }) + console.error("POST /email/send error:", err) + return reply.code(500).send({ error: "Failed to send email" }) } }) - -} \ No newline at end of file +} diff --git a/src/routes/files.ts b/src/routes/files.ts index 886fd2d..d3abce1 100644 --- a/src/routes/files.ts +++ b/src/routes/files.ts @@ -1,184 +1,200 @@ import { FastifyInstance } from "fastify" import multipart from "@fastify/multipart" import { s3 } from "../utils/s3" -import {GetObjectCommand, PutObjectCommand} from "@aws-sdk/client-s3" -import {getSignedUrl} from "@aws-sdk/s3-request-presigner"; +import { + GetObjectCommand, + PutObjectCommand +} from "@aws-sdk/client-s3" +import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import archiver from "archiver" -import {secrets} from "../utils/secrets" +import { secrets } from "../utils/secrets" + +import { eq, inArray } from "drizzle-orm" +import { + files, + createddocuments, + customers +} from "../../db/schema" + export default async function fileRoutes(server: FastifyInstance) { - await server.register(multipart,{ - limits: { - fileSize: 20 * 1024 * 1024, // 20 MB - } + + // ------------------------------------------------------------- + // MULTIPART INIT + // ------------------------------------------------------------- + await server.register(multipart, { + limits: { fileSize: 20 * 1024 * 1024 } // 20 MB }) + + // ------------------------------------------------------------- + // UPLOAD FILE + // ------------------------------------------------------------- server.post("/files/upload", async (req, reply) => { - const tenantId = req.user?.tenant_id - if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + try { + const tenantId = req.user?.tenant_id + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) - const data:any = await req.file() - const fileBuffer = await data.toBuffer() + const data: any = await req.file() + if (!data?.file) return reply.code(400).send({ error: "No file uploaded" }) + const fileBuffer = await data.toBuffer() - console.log(data) + const meta = data.fields?.meta?.value ? JSON.parse(data.fields.meta.value) : {} + // 1️⃣ DB-Eintrag erzeugen + const inserted = await server.db + .insert(files) + .values({ tenant: tenantId }) + .returning() - let meta = JSON.parse(data.fields?.meta?.value) + const created = inserted[0] + if (!created) throw new Error("Could not create DB entry") - if (!data.file) return reply.code(400).send({ error: "No file uploaded" }) - - - const {data:createdFileData,error:createdFileError} = await server.supabase - .from("files") - .insert({ - tenant: tenantId, - }) - .select() - .single() - - if(createdFileError) { - console.log(createdFileError) - return reply.code(500).send({ error: "Internal Server Error" }) - } else if(createdFileData && data.file) { - const fileKey = `${tenantId}/filesbyid/${createdFileData.id}/${data.filename}` + // 2️⃣ Datei in S3 speichern + const fileKey = `${tenantId}/filesbyid/${created.id}/${data.filename}` await s3.send(new PutObjectCommand({ Bucket: secrets.S3_BUCKET, Key: fileKey, Body: fileBuffer, - ContentType: data.mimetype, + ContentType: data.mimetype })) - //Update File with Corresponding Path - const {data:updateFileData, error:updateFileError} = await server.supabase - .from("files") - .update({ + // 3️⃣ DB updaten: meta + path + await server.db + .update(files) + .set({ ...meta, - path: fileKey, + path: fileKey }) - .eq("id", createdFileData.id) - - if(updateFileError) { - console.log(updateFileError) - return reply.code(500).send({ error: "Internal Server Error" }) - - } else { - /*const {data:tagData, error:tagError} = await server.supabase - .from("filetagmembers") - .insert(tags.map(tag => { - return { - file_id: createdFileData.id, - tag_id: tag - } - }))*/ - - return { id: createdFileData.id, filename: data.filename, path: fileKey } + .where(eq(files.id, created.id)) + return { + id: created.id, + filename: data.filename, + path: fileKey } - + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Upload failed" }) } }) + + + // ------------------------------------------------------------- + // GET FILE OR LIST FILES + // ------------------------------------------------------------- server.get("/files/:id?", async (req, reply) => { - const { id } = req.params as { id?: string } + try { + const { id } = req.params as { id?: string } - if(id) { - try { - const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single() + // 🔹 EINZELNE DATEI + if (id) { + const rows = await server.db + .select() + .from(files) + .where(eq(files.id, id)) - return {...data} - } catch (err) { - req.log.error(err); - reply.code(500).send({ error: "Could not generate presigned URL" }); + const file = rows[0] + if (!file) return reply.code(404).send({ error: "Not found" }) + + return file } - } else { - try { - const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").eq("tenant",req.user.tenant_id) + // 🔹 ALLE DATEIEN DES TENANTS (mit createddocument + customer) + const tenantId = req.user?.tenant_id + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + const list = await server.db + .select({ + ...files, + createddocument: createddocuments, + customer: customers + }) + .from(files) + .leftJoin( + createddocuments, + eq(files.createddocument, createddocuments.id) + ) + .leftJoin( + customers, + eq(createddocuments.customer, customers.id) + ) + .where(eq(files.tenant, tenantId)) - return { files: supabaseFileEntries } - } catch (err) { - req.log.error(err) - reply.code(500).send({ error: "Could not generate presigned URLs" }) - } + return { files: list } + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Could not load files" }) } - }) + + + // ------------------------------------------------------------- + // DOWNLOAD (SINGLE OR MULTI ZIP) + // ------------------------------------------------------------- server.post("/files/download/:id?", async (req, reply) => { - const { id } = req.params as { id?: string } - - // @ts-ignore - const ids = req.body?.ids || [] - - try { - if (id) { - // 🔹 Einzeldownload - const { data, error } = await server.supabase - .from("files") - .select("*") - .eq("id", id) - .single() + const { id } = req.params as { id?: string } + const ids = req.body?.ids || [] - if (error || !data) { - return reply.code(404).send({ error: "File not found" }) - } + // ------------------------------------------------- + // 1️⃣ SINGLE DOWNLOAD + // ------------------------------------------------- + if (id) { + const rows = await server.db + .select() + .from(files) + .where(eq(files.id, id)) + + const file = rows[0] + if (!file) return reply.code(404).send({ error: "File not found" }) const command = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, - Key: data.path, + Key: file.path! }) const { Body, ContentType } = await s3.send(command) const chunks: any[] = [] - // @ts-ignore - for await (const chunk of Body) { - chunks.push(chunk) - } + for await (const chunk of Body as any) chunks.push(chunk) const buffer = Buffer.concat(chunks) reply.header("Content-Type", ContentType || "application/octet-stream") - reply.header( - "Content-Disposition", - `attachment; filename="${data.path.split("/").pop()}"` - ) + reply.header("Content-Disposition", `attachment; filename="${file.path?.split("/").pop()}"`) return reply.send(buffer) } - console.log(ids) + // ------------------------------------------------- + // 2️⃣ MULTI DOWNLOAD → ZIP + // ------------------------------------------------- if (Array.isArray(ids) && ids.length > 0) { - // 🔹 Multi-Download → ZIP zurückgeben - const { data: supabaseFiles, error } = await server.supabase - .from("files") - .select("*") - .in("id", ids) + const rows = await server.db + .select() + .from(files) + .where(inArray(files.id, ids)) - if (error || !supabaseFiles?.length) { - return reply.code(404).send({ error: "Files not found" }) - } - - console.log(supabaseFiles) + if (!rows.length) return reply.code(404).send({ error: "Files not found" }) reply.header("Content-Type", "application/zip") - reply.header("Content-Disposition", "attachment; filename=dateien.zip") + reply.header("Content-Disposition", `attachment; filename="dateien.zip"`) const archive = archiver("zip", { zlib: { level: 9 } }) - archive.on("warning", console.warn) - for (const entry of supabaseFiles) { - const command = new GetObjectCommand({ + for (const entry of rows) { + const cmd = new GetObjectCommand({ Bucket: secrets.S3_BUCKET, - Key: entry.path, + Key: entry.path! }) + const { Body } = await s3.send(cmd) - const { Body } = await s3.send(command) - const filename = entry.path.split("/").pop() || entry.id - console.log(filename) - archive.append(Body as any, { name: filename }) + archive.append(Body as any, { + name: entry.path?.split("/").pop() || entry.id + }) } await archive.finalize() @@ -186,80 +202,80 @@ export default async function fileRoutes(server: FastifyInstance) { } return reply.code(400).send({ error: "No id or ids provided" }) + } catch (err) { - console.log(err) - reply.code(500).send({ error: "Download failed" }) + console.error(err) + return reply.code(500).send({ error: "Download failed" }) } }) + + + // ------------------------------------------------------------- + // GENERATE PRESIGNED URL(S) + // ------------------------------------------------------------- server.post("/files/presigned/:id?", async (req, reply) => { - const { id } = req.params as { id: string }; - const { ids } = req.body as { ids: string[] } + try { + const { id } = req.params as { id?: string } + const { ids } = req.body as { ids?: string[] } + const tenantId = req.user?.tenant_id - if(id) { - try { - const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single() + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) - const command = new GetObjectCommand({ - Bucket: secrets.S3_BUCKET, - Key: data.path, - }); + // ------------------------------------------------- + // SINGLE FILE PRESIGNED URL + // ------------------------------------------------- + if (id) { + const rows = await server.db + .select() + .from(files) + .where(eq(files.id, id)) - // URL für 15 Minuten gültig - const url = await getSignedUrl(s3, command, { expiresIn: 900 }); + const file = rows[0] + if (!file) return reply.code(404).send({ error: "Not found" }) - return { ...data, url }; - } catch (err) { - req.log.error(err); - reply.code(500).send({ error: "Could not generate presigned URL" }); - } - } else { - if (!Array.isArray(ids) || ids.length === 0) { - return reply.code(400).send({ error: "No file keys provided" }) - } + const url = await getSignedUrl( + s3, + new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), + { expiresIn: 900 } + ) - try { - const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").eq("tenant",req.user.tenant_id).is("archived",false) + return { ...file, url } + } else { + // ------------------------------------------------- + // MULTIPLE PRESIGNED URLs + // ------------------------------------------------- + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return reply.code(400).send({ error: "No ids provided" }) + } - console.log(error) + const rows = await server.db + .select() + .from(files) + .where(eq(files.tenant, tenantId)) - let filteredFiles = supabaseFileEntries.filter(i => ids.includes(i.id)) - filteredFiles = filteredFiles.filter(i => i.path) + const selected = rows.filter(f => ids.includes(f.id)) - console.log(filteredFiles.filter(i => !i.path)) - - - - let urls = await Promise.all( - ids.map(async (id) => { - let file = filteredFiles.find(i => i.id === id) - - if(!file) return - - let key = file.path - if(!key) console.log(file) - - const command = new GetObjectCommand({ - Bucket: secrets.S3_BUCKET, - Key: key, - }) - - const url = await getSignedUrl(s3, command, { expiresIn: 900 }) // 15 min gültig - - - return {...filteredFiles.find(i => i.id === id), url} + const output = await Promise.all( + selected.map(async (file) => { + const url = await getSignedUrl( + s3, + new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), + { expiresIn: 900 } + ) + return { ...file, url } }) ) - urls = urls.filter(i => i) - - return { files: urls } - } catch (err) { - console.log(err) - reply.code(500).send({ error: "Could not generate presigned URLs" }) + return { files: output } } + + + + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Could not create presigned URLs" }) } - - }) -} \ No newline at end of file + +} diff --git a/src/routes/functions.ts b/src/routes/functions.ts index 96cc111..651aba4 100644 --- a/src/routes/functions.ts +++ b/src/routes/functions.ts @@ -13,6 +13,8 @@ import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js" import duration from "dayjs/plugin/duration.js"; import timezone from "dayjs/plugin/timezone.js"; import {generateTimesEvaluation} from "../modules/time/evaluation.service"; +import {citys} from "../../db/schema"; +import {eq} from "drizzle-orm"; dayjs.extend(customParseFormat) dayjs.extend(isoWeek) dayjs.extend(isBetween) @@ -102,7 +104,11 @@ export default async function functionRoutes(server: FastifyInstance) { } try { - const { data, error } = await server.supabase + //@ts-ignore + const data = await server.db.select().from(citys).where(eq(citys.zip,zip)) + + + /*const { data, error } = await server.supabase .from('citys') .select() .eq('zip', zip) @@ -111,7 +117,7 @@ export default async function functionRoutes(server: FastifyInstance) { if (error) { console.log(error) return reply.code(500).send({ error: 'Database error' }) - } + }*/ if (!data) { return reply.code(404).send({ error: 'ZIP not found' }) diff --git a/src/routes/profiles.ts b/src/routes/profiles.ts index 17e9158..833f748 100644 --- a/src/routes/profiles.ts +++ b/src/routes/profiles.ts @@ -1,54 +1,120 @@ import { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; + +import { + authProfiles, +} from "../../db/schema"; export default async function authProfilesRoutes(server: FastifyInstance) { - // Ein einzelnes Profil laden (nur im aktuellen Tenant) + + // ------------------------------------------------------------- + // GET SINGLE PROFILE + // ------------------------------------------------------------- server.get("/profiles/:id", async (req, reply) => { - const { id } = req.params as {id:string}; - const tenantId = (req.user as any)?.tenant_id; + 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" }); + if (!tenantId) { + return reply.code(400).send({ error: "No tenant selected" }); + } + + const rows = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.id, id), + eq(authProfiles.tenant_id, tenantId) + ) + ) + .limit(1); + + if (!rows.length) { + return reply.code(404).send({ error: "User not found or not in tenant" }); + } + + return rows[0]; + + } catch (error) { + console.error("GET /profiles/:id ERROR:", error); + return reply.code(500).send({ error: "Internal Server Error" }); } - - const { data, error } = await server.supabase - .from("auth_profiles") - .select() - .eq("id", id) - .eq("tenant_id", tenantId) - .single(); - - if (error || !data) { - console.log(error) - return reply.code(404).send({ error: "User not found or not in tenant" }); - } - - console.log(data); - - reply.send(data) }); - server.put("/profiles/:id", async (req, reply) => { - if (!req.user.tenant_id) { - return reply.code(400).send({ error: "No tenant selected" }); + 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" + ] + 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 } - const { id } = req.params as { id: string }; - const body = req.body as any + // ✅ Date-Felder sauber konvertieren, falls vorhanden + const dateFields = ["birthday", "entry_date"] - delete body.full_name + 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 + } - const { data, error } = await server.supabase - .from("auth_profiles") - .update(body) - .eq("id", id) - .eq("tenant_id", req.user.tenant_id) - .select("*") - .single(); + // ------------------------------------------------------------- + // UPDATE PROFILE + // ------------------------------------------------------------- + server.put("/profiles/:id", async (req, reply) => { + try { + const tenantId = req.user?.tenant_id + const userId = req.user?.user_id - if (error || !data) { - console.log(error) - return reply.code(404).send({ error: "User not found or not in tenant" }); + 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 updateData = { + ...body, + 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" }) + } + + return updated[0] + + } catch (err) { + console.error("PUT /profiles/:id ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) } }) -} \ No newline at end of file +} diff --git a/src/routes/resources/main.ts b/src/routes/resources/main.ts index 0121e42..4a77cba 100644 --- a/src/routes/resources/main.ts +++ b/src/routes/resources/main.ts @@ -11,18 +11,8 @@ import { } from "drizzle-orm" -import { - projects, - customers, - plants, - contracts, - projecttypes, - createddocuments, - files, - events, - tasks, contacts, vendors -} from "../../../db/schema" -import * as sea from "node:sea"; + +import {resourceConfig} from "../../resource.config"; // ------------------------------------------------------------- // SQL Volltextsuche auf mehreren Feldern @@ -50,7 +40,7 @@ export default async function resourceRoutes(server: FastifyInstance) { // ------------------------------------------------------------- // LIST // ------------------------------------------------------------- - /*server.get("/resource/:resource", async (req, reply) => { + server.get("/resource/:resource", async (req, reply) => { try { const tenantId = req.user?.tenant_id if (!tenantId) @@ -62,34 +52,85 @@ export default async function resourceRoutes(server: FastifyInstance) { asc?: string } + const {resource} = req.params as {resource: string} + const table = resourceConfig[resource].table + // WHERE-Basis - let whereCond: any = eq(projects.tenant, tenantId) + let whereCond: any = eq(table.tenant, tenantId) // 🔍 SQL Search - const searchCond = buildProjectSearch(search) - if (searchCond) whereCond = and(whereCond, searchCond) + if(search) { + const searchCond = buildSearchCondition( + table, + resourceConfig[resource].searchColumns, + search.trim() + ) + + if (searchCond) { + whereCond = and(whereCond, searchCond) + } + } // Base Query - let q = server.db.select().from(projects).where(whereCond) + let q = server.db.select().from(table).where(whereCond) // Sortierung if (sort) { - const col = (projects as any)[sort] + const col = (table as any)[sort] if (col) { + //@ts-ignore q = ascQuery === "true" ? q.orderBy(asc(col)) : q.orderBy(desc(col)) } } - const data = await q + const queryData = await q + + + // RELATION LOADING (MANY-TO-ONE) + + let ids = {} + let lists = {} + let maps = {} + let data = [] + + if(resourceConfig[resource].mtoLoad) { + resourceConfig[resource].mtoLoad.forEach(relation => { + ids[relation] = [...new Set(queryData.map(r => r[relation]).filter(Boolean))]; + }) + + for await (const relation of resourceConfig[resource].mtoLoad ) { + lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : [] + + } + + resourceConfig[resource].mtoLoad.forEach(relation => { + maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); + }) + + data = queryData.map(row => { + let toReturn = { + ...row + } + + resourceConfig[resource].mtoLoad.forEach(relation => { + toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null + }) + + return toReturn + }); + } else { + data = queryData + } + return data } catch (err) { - console.error("ERROR /resource/projects", err) + console.error("ERROR /resource/:resource", err) return reply.code(500).send({ error: "Internal Server Error" }) } - })*/ + }) // ------------------------------------------------------------- @@ -118,41 +159,10 @@ export default async function resourceRoutes(server: FastifyInstance) { }; - const config = { - projects: { - searchColumns: ["name"], - mtoLoad: ["customer","plant","contract","projecttype"], - table: projects - }, - customers: { - searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"], - table: customers, - }, - contacts: { - searchColumns: ["firstName", "lastName", "email", "phone", "notes"], - table: contacts, - mtoLoad: ["customer","vendor"] - }, - contracts: { - table: contracts, - searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"] - }, - plants: { - table: plants - }, - projecttypes: { - table: projecttypes - }, - vendors: { - table: vendors, - searchColumns: ["name","vendorNumber","notes","defaultPaymentType"], - }, - files: { - table: files - } - } - let table = config[resource].table + + let table = resourceConfig[resource].table + let whereCond: any = eq(table.tenant, tenantId); @@ -160,7 +170,7 @@ export default async function resourceRoutes(server: FastifyInstance) { if(search) { const searchCond = buildSearchCondition( table, - config[resource].searchColumns, + resourceConfig[resource].searchColumns, search.trim() ) @@ -225,7 +235,7 @@ export default async function resourceRoutes(server: FastifyInstance) { if (sort?.length > 0) { const s = sort[0]; - const col = (projects as any)[s.field]; + const col = (table as any)[s.field]; if (col) { orderField = col; direction = s.direction === "asc" ? "asc" : "desc"; @@ -268,17 +278,17 @@ export default async function resourceRoutes(server: FastifyInstance) { let maps = {} let data = [] - if(config[resource].mtoLoad) { - config[resource].mtoLoad.forEach(relation => { + if(resourceConfig[resource].mtoLoad) { + resourceConfig[resource].mtoLoad.forEach(relation => { ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))]; }) - for await (const relation of config[resource].mtoLoad ) { - lists[relation] = ids[relation].length ? await server.db.select().from(config[relation + "s"].table).where(inArray(config[relation + "s"].table.id, ids[relation])) : [] + for await (const relation of resourceConfig[resource].mtoLoad ) { + lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : [] } - config[resource].mtoLoad.forEach(relation => { + resourceConfig[resource].mtoLoad.forEach(relation => { maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); }) @@ -287,12 +297,14 @@ export default async function resourceRoutes(server: FastifyInstance) { ...row } - config[resource].mtoLoad.forEach(relation => { + resourceConfig[resource].mtoLoad.forEach(relation => { toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null }) return toReturn }); + } else { + data = rows } // ----------------------------------------------- @@ -318,96 +330,55 @@ export default async function resourceRoutes(server: FastifyInstance) { // ------------------------------------------------------------- // DETAIL (mit JOINS) // ------------------------------------------------------------- - /*server.get("/resource/projects/:id", async (req, reply) => { + server.get("/resource/:resource/: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 pid = Number(id) + const {resource} = req.params as { resource: string } + const table = resourceConfig[resource].table const projRows = await server.db .select() - .from(projects) - .where(and(eq(projects.id, pid), eq(projects.tenant, tenantId))) + .from(table) + .where(and(eq(table.id, id), eq(table.tenant, tenantId))) .limit(1) if (!projRows.length) - return reply.code(404).send({ error: "Project not found" }) - - const project = projRows[0] + return reply.code(404).send({ error: "Resource not found" }) // ------------------------------------ // LOAD RELATIONS // ------------------------------------ - const [ - customerRecord, - plantRecord, - contractRecord, - projectTypeRecord, - projectTasks, - projectFiles, - projectDocuments, - projectEvents, - ] = await Promise.all([ - project.customer - ? server.db.select().from(customers).where(eq(customers.id, project.customer)) - : [], - - project.plant - ? server.db.select().from(plants).where(eq(plants.id, project.plant)) - : [], - - project.contract - ? server.db.select().from(contracts).where(eq(contracts.id, project.contract)) - : [], - - project.projecttype - ? server.db.select().from(projecttypes).where(eq(projecttypes.id, project.projecttype)) - : [], - - // Tasks - server.db - .select() - .from(tasks) - .where(eq(tasks.project, pid)), - - // Files - server.db - .select() - .from(files) - .where(eq(files.project, pid)), - - // Documents - server.db - .select() - .from(createddocuments) - .where(eq(createddocuments.project, pid)), - - // Events - server.db - .select() - .from(events) - .where(eq(events.project, pid)), - - ]) - - return { - ...project, - customer: customerRecord[0] ?? null, - plant: plantRecord[0] ?? null, - contract: contractRecord[0] ?? null, - projecttype: projectTypeRecord[0] ?? null, - tasks: projectTasks, - files: projectFiles, - createddocuments: projectDocuments, - events: projectEvents, + let ids = {} + let lists = {} + let maps = {} + let data = { + ...projRows[0] } + if(resourceConfig[resource].mtoLoad) { + for await (const relation of resourceConfig[resource].mtoLoad ) { + if(data[relation]) { + data[relation] = await server.db.select().from(resourceConfig[relation + "s"].table).where(eq(resourceConfig[relation + "s"].table.id, data[relation])) + } + } + + for await (const relation of resourceConfig[resource].mtmLoad ) { + console.log(relation) + data[relation] = await server.db.select().from(resourceConfig[relation].table).where(eq(resourceConfig[relation].table[resource.substring(0,resource.length - 1)],id)) + } + + + } + + return data + } catch (err) { console.error("ERROR /resource/projects/:id", err) return reply.code(500).send({ error: "Internal Server Error" }) } - })*/ + }) } diff --git a/src/routes/resources/productsServices.ts b/src/routes/resources/productsServices.ts deleted file mode 100644 index dd25bb5..0000000 --- a/src/routes/resources/productsServices.ts +++ /dev/null @@ -1,858 +0,0 @@ -import { FastifyInstance } from "fastify" -import { - eq, - ilike, - asc, - desc, - and, - count, - inArray, -} from "drizzle-orm" - -import { - products, - productcategories, - services, - servicecategories, -} from "../../../db/schema" - -// ----------------------------------------------------------------------------- -// PRODUCTS -// ----------------------------------------------------------------------------- -export default async function productsAndServicesRoutes(server: FastifyInstance) { - // ------------------------------------------------------------- - // LIST: /resource/products - // ------------------------------------------------------------- - server.get("/resource/products", 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 baseQuery = server.db - .select() - .from(products) - .where(eq(products.tenant, tenantId)) - - if (search) { - baseQuery = server.db - .select() - .from(products) - .where( - and( - eq(products.tenant, tenantId), - ilike(products.name, `%${search}%`) - ) - ) - } - - if (sort) { - const field = (products as any)[sort] - if (field) { - // @ts-ignore - baseQuery = baseQuery.orderBy( - ascQuery === "true" ? asc(field) : desc(field) - ) - } - } - - const list = await baseQuery - return list - }) - - // ------------------------------------------------------------- - // PAGINATED: /resource/products/paginated - // ------------------------------------------------------------- - server.get("/resource/products/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, // aktuell ignoriert, wie bei customers - search, - searchColumns, - distinctColumns, - } = req.query as { - select?: string - search?: string - searchColumns?: string - distinctColumns?: string - } - - let whereCond: any = eq(products.tenant, tenantId) - - if (filters) { - for (const [key, val] of Object.entries(filters)) { - const col = (products 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 as any)) - } else { - whereCond = and(whereCond, eq(col, val as any)) - } - } - } - - if (search && search.trim().length > 0) { - const searchTerm = `%${search.trim().toLowerCase()}%` - whereCond = and( - whereCond, - ilike(products.name, searchTerm) - ) - } - - const totalRes = await server.db - .select({ value: count(products.id) }) - .from(products) - .where(whereCond) - - const total = Number(totalRes[0]?.value ?? 0) - - const distinctValues: Record = {} - - if (distinctColumns) { - for (const colName of distinctColumns.split(",").map(v => v.trim())) { - const col = (products as any)[colName] - if (!col) continue - - const rows = await server.db - .select({ v: col }) - .from(products) - .where(eq(products.tenant, tenantId)) - - const values = rows - .map(r => r.v) - .filter(v => v != null && v !== "") - - distinctValues[colName] = [...new Set(values)].sort() - } - } - - let offset = 0 - let limit = 999999 - - if (!paginationDisabled && pagination) { - offset = pagination.offset - limit = pagination.limit - } - - let orderField: any = null - let orderDirection: "asc" | "desc" = "asc" - - if (sort?.length > 0) { - const s = sort[0] - const col = (products as any)[s.field] - if (col) { - orderField = col - orderDirection = s.direction === "asc" ? "asc" : "desc" - } - } - - let dataQuery = server.db - .select() - .from(products) - .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 (e) { - server.log.error(e) - return reply.code(500).send({ error: "Internal Server Error" }) - } - }) - - // ------------------------------------------------------------- - // DETAIL: /resource/products/:id - // (aktuell ohne weitere Joins) - // ------------------------------------------------------------- - server.get("/resource/products/: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 rows = await server.db - .select() - .from(products) - .where( - and( - eq(products.id, Number(id)), - eq(products.tenant, tenantId) - ) - ) - .limit(1) - - if (!rows.length) { - return reply.code(404).send({ error: "Product not found" }) - } - - return rows[0] - }) - - - // --------------------------------------------------------------------------- - // PRODUCTCATEGORIES - // --------------------------------------------------------------------------- - server.get("/resource/productcategories", 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 baseQuery = server.db - .select() - .from(productcategories) - .where(eq(productcategories.tenant, tenantId)) - - if (search) { - const searchTerm = `%${search}%` - baseQuery = server.db - .select() - .from(productcategories) - .where( - and( - eq(productcategories.tenant, tenantId), - ilike(productcategories.name, searchTerm) - ) - ) - } - - if (sort) { - const field = (productcategories as any)[sort] - if (field) { - // @ts-ignore - baseQuery = baseQuery.orderBy( - ascQuery === "true" ? asc(field) : desc(field) - ) - } - } - - const list = await baseQuery - return list - }) - - server.get("/resource/productcategories/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 - } - - let whereCond: any = eq(productcategories.tenant, tenantId) - - if (filters) { - for (const [key, val] of Object.entries(filters)) { - const col = (productcategories 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 as any)) - } else { - whereCond = and(whereCond, eq(col, val as any)) - } - } - } - - if (search && search.trim().length > 0) { - const searchTerm = `%${search.trim().toLowerCase()}%` - whereCond = and( - whereCond, - ilike(productcategories.name, searchTerm) - ) - } - - const totalRes = await server.db - .select({ value: count(productcategories.id) }) - .from(productcategories) - .where(whereCond) - - const total = Number(totalRes[0]?.value ?? 0) - - const distinctValues: Record = {} - - if (distinctColumns) { - for (const colName of distinctColumns.split(",").map(v => v.trim())) { - const col = (productcategories as any)[colName] - if (!col) continue - - const rows = await server.db - .select({ v: col }) - .from(productcategories) - .where(eq(productcategories.tenant, tenantId)) - - const values = rows - .map(r => r.v) - .filter(v => v != null && v !== "") - - distinctValues[colName] = [...new Set(values)].sort() - } - } - - let offset = 0 - let limit = 999999 - - if (!paginationDisabled && pagination) { - offset = pagination.offset - limit = pagination.limit - } - - let orderField: any = null - let orderDirection: "asc" | "desc" = "asc" - - if (sort?.length > 0) { - const s = sort[0] - const col = (productcategories as any)[s.field] - if (col) { - orderField = col - orderDirection = s.direction === "asc" ? "asc" : "desc" - } - } - - let dataQuery = server.db - .select() - .from(productcategories) - .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 (e) { - server.log.error(e) - return reply.code(500).send({ error: "Internal Server Error" }) - } - }) - - server.get("/resource/productcategories/: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 rows = await server.db - .select() - .from(productcategories) - .where( - and( - eq(productcategories.id, Number(id)), - eq(productcategories.tenant, tenantId) - ) - ) - .limit(1) - - if (!rows.length) { - return reply.code(404).send({ error: "Product category not found" }) - } - - // Später hier: products mit Join-Tabelle - return rows[0] - }) - - - // --------------------------------------------------------------------------- - // SERVICES - // --------------------------------------------------------------------------- - server.get("/resource/services", 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 baseQuery = server.db - .select() - .from(services) - .where(eq(services.tenant, tenantId)) - - if (search) { - const searchTerm = `%${search}%` - baseQuery = server.db - .select() - .from(services) - .where( - and( - eq(services.tenant, tenantId), - ilike(services.name, searchTerm) - ) - ) - } - - if (sort) { - const field = (services as any)[sort] - if (field) { - // @ts-ignore - baseQuery = baseQuery.orderBy( - ascQuery === "true" ? asc(field) : desc(field) - ) - } - } - - const list = await baseQuery - return list - }) - - server.get("/resource/services/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 - } - - let whereCond: any = eq(services.tenant, tenantId) - - if (filters) { - for (const [key, val] of Object.entries(filters)) { - const col = (services 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 as any)) - } else { - whereCond = and(whereCond, eq(col, val as any)) - } - } - } - - if (search && search.trim().length > 0) { - const searchTerm = `%${search.trim().toLowerCase()}%` - whereCond = and( - whereCond, - ilike(services.name, searchTerm) - ) - } - - const totalRes = await server.db - .select({ value: count(services.id) }) - .from(services) - .where(whereCond) - - const total = Number(totalRes[0]?.value ?? 0) - - const distinctValues: Record = {} - - if (distinctColumns) { - for (const colName of distinctColumns.split(",").map(v => v.trim())) { - const col = (services as any)[colName] - if (!col) continue - - const rows = await server.db - .select({ v: col }) - .from(services) - .where(eq(services.tenant, tenantId)) - - const values = rows - .map(r => r.v) - .filter(v => v != null && v !== "") - - distinctValues[colName] = [...new Set(values)].sort() - } - } - - let offset = 0 - let limit = 999999 - - if (!paginationDisabled && pagination) { - offset = pagination.offset - limit = pagination.limit - } - - let orderField: any = null - let orderDirection: "asc" | "desc" = "asc" - - if (sort?.length > 0) { - const s = sort[0] - const col = (services as any)[s.field] - if (col) { - orderField = col - orderDirection = s.direction === "asc" ? "asc" : "desc" - } - } - - let dataQuery = server.db - .select() - .from(services) - .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 (e) { - server.log.error(e) - return reply.code(500).send({ error: "Internal Server Error" }) - } - }) - - server.get("/resource/services/: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 rows = await server.db - .select() - .from(services) - .where( - and( - eq(services.id, Number(id)), - eq(services.tenant, tenantId) - ) - ) - .limit(1) - - if (!rows.length) { - return reply.code(404).send({ error: "Service not found" }) - } - - // Später: Unit, Kategorien, etc. als Joins - return rows[0] - }) - - - // --------------------------------------------------------------------------- - // SERVICECATEGORIES - // --------------------------------------------------------------------------- - server.get("/resource/servicecategories", 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 baseQuery = server.db - .select() - .from(servicecategories) - .where(eq(servicecategories.tenant, tenantId)) - - if (search) { - const searchTerm = `%${search}%` - baseQuery = server.db - .select() - .from(servicecategories) - .where( - and( - eq(servicecategories.tenant, tenantId), - ilike(servicecategories.name, searchTerm) - ) - ) - } - - if (sort) { - const field = (servicecategories as any)[sort] - if (field) { - // @ts-ignore - baseQuery = baseQuery.orderBy( - ascQuery === "true" ? asc(field) : desc(field) - ) - } - } - - const list = await baseQuery - return list - }) - - server.get("/resource/servicecategories/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 - } - - let whereCond: any = eq(servicecategories.tenant, tenantId) - - if (filters) { - for (const [key, val] of Object.entries(filters)) { - const col = (servicecategories 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 as any)) - } else { - whereCond = and(whereCond, eq(col, val as any)) - } - } - } - - if (search && search.trim().length > 0) { - const searchTerm = `%${search.trim().toLowerCase()}%` - whereCond = and( - whereCond, - ilike(servicecategories.name, searchTerm) - ) - } - - const totalRes = await server.db - .select({ value: count(servicecategories.id) }) - .from(servicecategories) - .where(whereCond) - - const total = Number(totalRes[0]?.value ?? 0) - - const distinctValues: Record = {} - - if (distinctColumns) { - for (const colName of distinctColumns.split(",").map(v => v.trim())) { - const col = (servicecategories as any)[colName] - if (!col) continue - - const rows = await server.db - .select({ v: col }) - .from(servicecategories) - .where(eq(servicecategories.tenant, tenantId)) - - const values = rows - .map(r => r.v) - .filter(v => v != null && v !== "") - - distinctValues[colName] = [...new Set(values)].sort() - } - } - - let offset = 0 - let limit = 999999 - - if (!paginationDisabled && pagination) { - offset = pagination.offset - limit = pagination.limit - } - - let orderField: any = null - let orderDirection: "asc" | "desc" = "asc" - - if (sort?.length > 0) { - const s = sort[0] - const col = (servicecategories as any)[s.field] - if (col) { - orderField = col - orderDirection = s.direction === "asc" ? "asc" : "desc" - } - } - - let dataQuery = server.db - .select() - .from(servicecategories) - .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 (e) { - server.log.error(e) - return reply.code(500).send({ error: "Internal Server Error" }) - } - }) - - server.get("/resource/servicecategories/: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 rows = await server.db - .select() - .from(servicecategories) - .where( - and( - eq(servicecategories.id, Number(id)), - eq(servicecategories.tenant, tenantId) - ) - ) - .limit(1) - - if (!rows.length) { - return reply.code(404).send({ error: "Service category not found" }) - } - - // Später: zugehörige Services über Join-Tabelle - return rows[0] - }) -}