redone routes
This commit is contained in:
@@ -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"})
|
||||
|
||||
114
src/resource.config.ts
Normal file
114
src/resource.config.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
});*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
})*/
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<string, any[]> = {}
|
||||
|
||||
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<string, any[]> = {}
|
||||
|
||||
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<string, any[]> = {}
|
||||
|
||||
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<string, any[]> = {}
|
||||
|
||||
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]
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user