redone routes
This commit is contained in:
@@ -17,7 +17,6 @@ import resourceRoutesSpecial from "./routes/resourcesSpecial";
|
|||||||
import fastifyCookie from "@fastify/cookie";
|
import fastifyCookie from "@fastify/cookie";
|
||||||
import historyRoutes from "./routes/history";
|
import historyRoutes from "./routes/history";
|
||||||
import fileRoutes from "./routes/files";
|
import fileRoutes from "./routes/files";
|
||||||
import userRoutes from "./routes/auth/user"
|
|
||||||
import functionRoutes from "./routes/functions";
|
import functionRoutes from "./routes/functions";
|
||||||
import bankingRoutes from "./routes/banking";
|
import bankingRoutes from "./routes/banking";
|
||||||
import exportRoutes from "./routes/exports"
|
import exportRoutes from "./routes/exports"
|
||||||
@@ -30,8 +29,6 @@ import staffTimeRoutes from "./routes/staff/time";
|
|||||||
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
||||||
|
|
||||||
//Resources
|
//Resources
|
||||||
import productsAndServicesRoutes from "./routes/resources/productsServices";
|
|
||||||
|
|
||||||
import resourceRoutes from "./routes/resources/main";
|
import resourceRoutes from "./routes/resources/main";
|
||||||
|
|
||||||
//M2M
|
//M2M
|
||||||
@@ -106,7 +103,6 @@ async function main() {
|
|||||||
await subApp.register(resourceRoutesSpecial);
|
await subApp.register(resourceRoutesSpecial);
|
||||||
await subApp.register(historyRoutes);
|
await subApp.register(historyRoutes);
|
||||||
await subApp.register(fileRoutes);
|
await subApp.register(fileRoutes);
|
||||||
await subApp.register(userRoutes);
|
|
||||||
await subApp.register(functionRoutes);
|
await subApp.register(functionRoutes);
|
||||||
await subApp.register(bankingRoutes);
|
await subApp.register(bankingRoutes);
|
||||||
await subApp.register(exportRoutes);
|
await subApp.register(exportRoutes);
|
||||||
@@ -118,7 +114,6 @@ async function main() {
|
|||||||
await subApp.register(staffTimeConnectRoutes);
|
await subApp.register(staffTimeConnectRoutes);
|
||||||
|
|
||||||
|
|
||||||
await subApp.register(productsAndServicesRoutes);
|
|
||||||
await subApp.register(resourceRoutes);
|
await subApp.register(resourceRoutes);
|
||||||
|
|
||||||
},{prefix: "/api"})
|
},{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 { FastifyInstance } from "fastify";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
import {
|
||||||
|
authTenantUsers,
|
||||||
|
authUsers,
|
||||||
|
tenants,
|
||||||
|
} from "../../db/schema";
|
||||||
|
|
||||||
export default async function adminRoutes(server: FastifyInstance) {
|
export default async function adminRoutes(server: FastifyInstance) {
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// POST /admin/add-user-to-tenant
|
||||||
|
// -------------------------------------------------------------
|
||||||
server.post("/admin/add-user-to-tenant", async (req, reply) => {
|
server.post("/admin/add-user-to-tenant", async (req, reply) => {
|
||||||
const body = req.body as {
|
try {
|
||||||
user_id: string;
|
const body = req.body as {
|
||||||
tenant_id: string;
|
user_id: string;
|
||||||
role?: string;
|
tenant_id: number;
|
||||||
mode?: "single" | "multi";
|
role?: string;
|
||||||
};
|
mode?: "single" | "multi";
|
||||||
|
};
|
||||||
|
|
||||||
if (!body.user_id || !body.tenant_id) {
|
if (!body.user_id || !body.tenant_id) {
|
||||||
return reply.code(400).send({ error: "user_id and tenant_id required" });
|
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) => {
|
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) {
|
if (!user_id) {
|
||||||
return reply.code(400).send({ error: "user_id required" });
|
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 { FastifyInstance } from "fastify"
|
||||||
import bcrypt from "bcrypt";
|
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) {
|
export default async function authRoutesAuthenticated(server: FastifyInstance) {
|
||||||
|
|
||||||
server.post("/auth/password/change", {
|
server.post("/auth/password/change", {
|
||||||
schema: {
|
schema: {
|
||||||
tags: ["Auth"],
|
tags: ["Auth"],
|
||||||
summary: "Reset Password after forced change",
|
summary: "Change password (after login or forced reset)",
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["old_password", "new_password"],
|
required: ["old_password", "new_password"],
|
||||||
@@ -25,54 +28,69 @@ export default async function authRoutesAuthenticated(server: FastifyInstance) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, async (req, reply) => {
|
}, 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
|
const userId = req.user?.user_id
|
||||||
if (!user_id) {
|
if (!userId) {
|
||||||
// @ts-ignore
|
//@ts-ignore
|
||||||
return reply.code(401).send({ error: "Unauthorized" });
|
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 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
|
// @ts-ignore
|
||||||
import MailComposer from 'nodemailer/lib/mail-composer/index.js'
|
import MailComposer from "nodemailer/lib/mail-composer/index.js"
|
||||||
|
import { ImapFlow } from "imapflow"
|
||||||
import {ImapFlow} from "imapflow"
|
|
||||||
|
|
||||||
export default async function emailAsUserRoutes(server: FastifyInstance) {
|
export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||||
|
|
||||||
// Create E-Mail Account
|
|
||||||
|
// ======================================================================
|
||||||
|
// CREATE OR UPDATE EMAIL ACCOUNT
|
||||||
|
// ======================================================================
|
||||||
server.post("/email/accounts/:id?", async (req, reply) => {
|
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 {
|
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
|
email: string
|
||||||
password: string
|
password: string
|
||||||
smtp_host: string
|
smtp_host: string
|
||||||
@@ -173,32 +35,173 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
|||||||
imap_port: number
|
imap_port: number
|
||||||
imap_ssl: boolean
|
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" });
|
// UPDATE EXISTING
|
||||||
} else {
|
// -----------------------------
|
||||||
Object.keys(data).forEach((key) => {
|
if (id) {
|
||||||
if(key.includes("encrypted")){
|
const saveData = {
|
||||||
accountData[key.substring(0,key.length-10)] = decrypt(data[key])
|
emailEncrypted: body.email ? encrypt(body.email) : undefined,
|
||||||
} else {
|
passwordEncrypted: body.password ? encrypt(body.password) : undefined,
|
||||||
accountData[key] = data[key]
|
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({
|
const transporter = nodemailer.createTransport({
|
||||||
host: accountData.smtp_host,
|
host: accountData.smtpHost,
|
||||||
port: accountData.smtp_port,
|
port: accountData.smtpPort,
|
||||||
secure: accountData.smtp_ssl,
|
secure: accountData.smtpSsl,
|
||||||
auth: {
|
auth: {
|
||||||
user: accountData.email,
|
user: accountData.email,
|
||||||
pass: accountData.password,
|
pass: accountData.password,
|
||||||
@@ -208,62 +211,48 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
|||||||
const message = {
|
const message = {
|
||||||
from: accountData.email,
|
from: accountData.email,
|
||||||
to: body.to,
|
to: body.to,
|
||||||
cc: body.cc ? body.cc : undefined,
|
cc: body.cc,
|
||||||
bcc: body.bcc ? body.bcc : undefined,
|
bcc: body.bcc,
|
||||||
subject: body.subject,
|
subject: body.subject,
|
||||||
html: body.html ? body.html : undefined,
|
html: body.html,
|
||||||
text: body.text,
|
text: body.text,
|
||||||
attachments: body.attachments ? body.attachments : undefined,
|
attachments: body.attachments,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const info = await transporter.sendMail(message)
|
const info = await transporter.sendMail(message)
|
||||||
|
|
||||||
const imapClient = new ImapFlow({
|
// -------------------------
|
||||||
host: accountData.imap_host,
|
// SAVE TO IMAP SENT FOLDER
|
||||||
port: accountData.imap_port,
|
// -------------------------
|
||||||
secure: accountData.imap_ssl,
|
const imap = new ImapFlow({
|
||||||
|
host: accountData.imapHost,
|
||||||
|
port: accountData.imapPort,
|
||||||
|
secure: accountData.imapSsl,
|
||||||
auth: {
|
auth: {
|
||||||
user: accountData.email,
|
user: accountData.email,
|
||||||
pass: accountData.password,
|
pass: accountData.password,
|
||||||
},
|
},
|
||||||
logger: false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await imapClient.connect()
|
await imap.connect()
|
||||||
|
|
||||||
const mail = new MailComposer(message)
|
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 imap.list()) {
|
||||||
|
if (mailbox.specialUse === "\\Sent") {
|
||||||
|
await imap.mailboxOpen(mailbox.path)
|
||||||
for await (const mailbox of await imapClient.list()) {
|
await imap.append(mailbox.path, raw, ["\\Seen"])
|
||||||
// mailbox.flags enthält z. B. ['\\Sent', '\\HasChildren']
|
await imap.logout()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(info.response.includes("OK")){
|
return reply.send({ success: true })
|
||||||
reply.send({success: true})
|
|
||||||
}{
|
|
||||||
reply.status(500)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.error("POST /email/send error:", err)
|
||||||
reply.code(500).send({ error: "Failed to send E-Mail as User" })
|
return reply.code(500).send({ error: "Failed to send email" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,184 +1,200 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import multipart from "@fastify/multipart"
|
import multipart from "@fastify/multipart"
|
||||||
import { s3 } from "../utils/s3"
|
import { s3 } from "../utils/s3"
|
||||||
import {GetObjectCommand, PutObjectCommand} from "@aws-sdk/client-s3"
|
import {
|
||||||
import {getSignedUrl} from "@aws-sdk/s3-request-presigner";
|
GetObjectCommand,
|
||||||
|
PutObjectCommand
|
||||||
|
} from "@aws-sdk/client-s3"
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||||
import archiver from "archiver"
|
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) {
|
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) => {
|
server.post("/files/upload", async (req, reply) => {
|
||||||
const tenantId = req.user?.tenant_id
|
try {
|
||||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
const tenantId = req.user?.tenant_id
|
||||||
|
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||||
|
|
||||||
const data:any = await req.file()
|
const data: any = await req.file()
|
||||||
const fileBuffer = await data.toBuffer()
|
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" })
|
// 2️⃣ Datei in S3 speichern
|
||||||
|
const fileKey = `${tenantId}/filesbyid/${created.id}/${data.filename}`
|
||||||
|
|
||||||
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}`
|
|
||||||
|
|
||||||
await s3.send(new PutObjectCommand({
|
await s3.send(new PutObjectCommand({
|
||||||
Bucket: secrets.S3_BUCKET,
|
Bucket: secrets.S3_BUCKET,
|
||||||
Key: fileKey,
|
Key: fileKey,
|
||||||
Body: fileBuffer,
|
Body: fileBuffer,
|
||||||
ContentType: data.mimetype,
|
ContentType: data.mimetype
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//Update File with Corresponding Path
|
// 3️⃣ DB updaten: meta + path
|
||||||
const {data:updateFileData, error:updateFileError} = await server.supabase
|
await server.db
|
||||||
.from("files")
|
.update(files)
|
||||||
.update({
|
.set({
|
||||||
...meta,
|
...meta,
|
||||||
path: fileKey,
|
path: fileKey
|
||||||
})
|
})
|
||||||
.eq("id", createdFileData.id)
|
.where(eq(files.id, created.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 }
|
|
||||||
|
|
||||||
|
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) => {
|
server.get("/files/:id?", async (req, reply) => {
|
||||||
const { id } = req.params as { id?: string }
|
try {
|
||||||
|
const { id } = req.params as { id?: string }
|
||||||
|
|
||||||
if(id) {
|
// 🔹 EINZELNE DATEI
|
||||||
try {
|
if (id) {
|
||||||
const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single()
|
const rows = await server.db
|
||||||
|
.select()
|
||||||
|
.from(files)
|
||||||
|
.where(eq(files.id, id))
|
||||||
|
|
||||||
return {...data}
|
const file = rows[0]
|
||||||
} catch (err) {
|
if (!file) return reply.code(404).send({ error: "Not found" })
|
||||||
req.log.error(err);
|
|
||||||
reply.code(500).send({ error: "Could not generate presigned URL" });
|
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 }
|
return { files: list }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
req.log.error(err)
|
console.error(err)
|
||||||
reply.code(500).send({ error: "Could not generate presigned URLs" })
|
return reply.code(500).send({ error: "Could not load files" })
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// DOWNLOAD (SINGLE OR MULTI ZIP)
|
||||||
|
// -------------------------------------------------------------
|
||||||
server.post("/files/download/:id?", async (req, reply) => {
|
server.post("/files/download/:id?", async (req, reply) => {
|
||||||
const { id } = req.params as { id?: string }
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const ids = req.body?.ids || []
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (id) {
|
const { id } = req.params as { id?: string }
|
||||||
// 🔹 Einzeldownload
|
const ids = req.body?.ids || []
|
||||||
const { data, error } = await server.supabase
|
|
||||||
.from("files")
|
|
||||||
.select("*")
|
|
||||||
.eq("id", id)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
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({
|
const command = new GetObjectCommand({
|
||||||
Bucket: secrets.S3_BUCKET,
|
Bucket: secrets.S3_BUCKET,
|
||||||
Key: data.path,
|
Key: file.path!
|
||||||
})
|
})
|
||||||
|
|
||||||
const { Body, ContentType } = await s3.send(command)
|
const { Body, ContentType } = await s3.send(command)
|
||||||
|
|
||||||
const chunks: any[] = []
|
const chunks: any[] = []
|
||||||
// @ts-ignore
|
for await (const chunk of Body as any) chunks.push(chunk)
|
||||||
for await (const chunk of Body) {
|
|
||||||
chunks.push(chunk)
|
|
||||||
}
|
|
||||||
const buffer = Buffer.concat(chunks)
|
const buffer = Buffer.concat(chunks)
|
||||||
|
|
||||||
reply.header("Content-Type", ContentType || "application/octet-stream")
|
reply.header("Content-Type", ContentType || "application/octet-stream")
|
||||||
reply.header(
|
reply.header("Content-Disposition", `attachment; filename="${file.path?.split("/").pop()}"`)
|
||||||
"Content-Disposition",
|
|
||||||
`attachment; filename="${data.path.split("/").pop()}"`
|
|
||||||
)
|
|
||||||
return reply.send(buffer)
|
return reply.send(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(ids)
|
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// 2️⃣ MULTI DOWNLOAD → ZIP
|
||||||
|
// -------------------------------------------------
|
||||||
if (Array.isArray(ids) && ids.length > 0) {
|
if (Array.isArray(ids) && ids.length > 0) {
|
||||||
// 🔹 Multi-Download → ZIP zurückgeben
|
const rows = await server.db
|
||||||
const { data: supabaseFiles, error } = await server.supabase
|
.select()
|
||||||
.from("files")
|
.from(files)
|
||||||
.select("*")
|
.where(inArray(files.id, ids))
|
||||||
.in("id", ids)
|
|
||||||
|
|
||||||
if (error || !supabaseFiles?.length) {
|
if (!rows.length) return reply.code(404).send({ error: "Files not found" })
|
||||||
return reply.code(404).send({ error: "Files not found" })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(supabaseFiles)
|
|
||||||
|
|
||||||
reply.header("Content-Type", "application/zip")
|
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 } })
|
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||||
archive.on("warning", console.warn)
|
|
||||||
|
|
||||||
for (const entry of supabaseFiles) {
|
for (const entry of rows) {
|
||||||
const command = new GetObjectCommand({
|
const cmd = new GetObjectCommand({
|
||||||
Bucket: secrets.S3_BUCKET,
|
Bucket: secrets.S3_BUCKET,
|
||||||
Key: entry.path,
|
Key: entry.path!
|
||||||
})
|
})
|
||||||
|
const { Body } = await s3.send(cmd)
|
||||||
|
|
||||||
const { Body } = await s3.send(command)
|
archive.append(Body as any, {
|
||||||
const filename = entry.path.split("/").pop() || entry.id
|
name: entry.path?.split("/").pop() || entry.id
|
||||||
console.log(filename)
|
})
|
||||||
archive.append(Body as any, { name: filename })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await archive.finalize()
|
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" })
|
return reply.code(400).send({ error: "No id or ids provided" })
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.error(err)
|
||||||
reply.code(500).send({ error: "Download failed" })
|
return reply.code(500).send({ error: "Download failed" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// GENERATE PRESIGNED URL(S)
|
||||||
|
// -------------------------------------------------------------
|
||||||
server.post("/files/presigned/:id?", async (req, reply) => {
|
server.post("/files/presigned/:id?", async (req, reply) => {
|
||||||
const { id } = req.params as { id: string };
|
try {
|
||||||
const { ids } = req.body as { ids: string[] }
|
const { id } = req.params as { id?: string }
|
||||||
|
const { ids } = req.body as { ids?: string[] }
|
||||||
|
const tenantId = req.user?.tenant_id
|
||||||
|
|
||||||
if(id) {
|
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||||
try {
|
|
||||||
const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single()
|
|
||||||
|
|
||||||
const command = new GetObjectCommand({
|
// -------------------------------------------------
|
||||||
Bucket: secrets.S3_BUCKET,
|
// SINGLE FILE PRESIGNED URL
|
||||||
Key: data.path,
|
// -------------------------------------------------
|
||||||
});
|
if (id) {
|
||||||
|
const rows = await server.db
|
||||||
|
.select()
|
||||||
|
.from(files)
|
||||||
|
.where(eq(files.id, id))
|
||||||
|
|
||||||
// URL für 15 Minuten gültig
|
const file = rows[0]
|
||||||
const url = await getSignedUrl(s3, command, { expiresIn: 900 });
|
if (!file) return reply.code(404).send({ error: "Not found" })
|
||||||
|
|
||||||
return { ...data, url };
|
const url = await getSignedUrl(
|
||||||
} catch (err) {
|
s3,
|
||||||
req.log.error(err);
|
new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }),
|
||||||
reply.code(500).send({ error: "Could not generate presigned URL" });
|
{ expiresIn: 900 }
|
||||||
}
|
)
|
||||||
} else {
|
|
||||||
if (!Array.isArray(ids) || ids.length === 0) {
|
|
||||||
return reply.code(400).send({ error: "No file keys provided" })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
return { ...file, url }
|
||||||
const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").eq("tenant",req.user.tenant_id).is("archived",false)
|
} 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))
|
const selected = rows.filter(f => ids.includes(f.id))
|
||||||
filteredFiles = filteredFiles.filter(i => i.path)
|
|
||||||
|
|
||||||
console.log(filteredFiles.filter(i => !i.path))
|
const output = await Promise.all(
|
||||||
|
selected.map(async (file) => {
|
||||||
|
const url = await getSignedUrl(
|
||||||
|
s3,
|
||||||
let urls = await Promise.all(
|
new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }),
|
||||||
ids.map(async (id) => {
|
{ expiresIn: 900 }
|
||||||
let file = filteredFiles.find(i => i.id === id)
|
)
|
||||||
|
return { ...file, url }
|
||||||
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}
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
urls = urls.filter(i => i)
|
return { files: output }
|
||||||
|
|
||||||
return { files: urls }
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
reply.code(500).send({ error: "Could not generate presigned URLs" })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} 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 duration from "dayjs/plugin/duration.js";
|
||||||
import timezone from "dayjs/plugin/timezone.js";
|
import timezone from "dayjs/plugin/timezone.js";
|
||||||
import {generateTimesEvaluation} from "../modules/time/evaluation.service";
|
import {generateTimesEvaluation} from "../modules/time/evaluation.service";
|
||||||
|
import {citys} from "../../db/schema";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(isoWeek)
|
dayjs.extend(isoWeek)
|
||||||
dayjs.extend(isBetween)
|
dayjs.extend(isBetween)
|
||||||
@@ -102,7 +104,11 @@ export default async function functionRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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')
|
.from('citys')
|
||||||
.select()
|
.select()
|
||||||
.eq('zip', zip)
|
.eq('zip', zip)
|
||||||
@@ -111,7 +117,7 @@ export default async function functionRoutes(server: FastifyInstance) {
|
|||||||
if (error) {
|
if (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
return reply.code(500).send({ error: 'Database error' })
|
return reply.code(500).send({ error: 'Database error' })
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return reply.code(404).send({ error: 'ZIP not found' })
|
return reply.code(404).send({ error: 'ZIP not found' })
|
||||||
|
|||||||
@@ -1,54 +1,120 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
|
||||||
|
import {
|
||||||
|
authProfiles,
|
||||||
|
} from "../../db/schema";
|
||||||
|
|
||||||
export default async function authProfilesRoutes(server: FastifyInstance) {
|
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) => {
|
server.get("/profiles/:id", async (req, reply) => {
|
||||||
const { id } = req.params as {id:string};
|
try {
|
||||||
const tenantId = (req.user as any)?.tenant_id;
|
const { id } = req.params as { id: string };
|
||||||
|
const tenantId = (req.user as any)?.tenant_id;
|
||||||
|
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
return reply.code(400).send({ error: "No tenant selected" });
|
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) => {
|
function sanitizeProfileUpdate(body: any) {
|
||||||
if (!req.user.tenant_id) {
|
const cleaned: any = { ...body }
|
||||||
return reply.code(400).send({ error: "No tenant selected" });
|
|
||||||
|
// ❌ 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 };
|
// ✅ Date-Felder sauber konvertieren, falls vorhanden
|
||||||
const body = req.body as any
|
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 PROFILE
|
||||||
.update(body)
|
// -------------------------------------------------------------
|
||||||
.eq("id", id)
|
server.put("/profiles/:id", async (req, reply) => {
|
||||||
.eq("tenant_id", req.user.tenant_id)
|
try {
|
||||||
.select("*")
|
const tenantId = req.user?.tenant_id
|
||||||
.single();
|
const userId = req.user?.user_id
|
||||||
|
|
||||||
if (error || !data) {
|
if (!tenantId) {
|
||||||
console.log(error)
|
return reply.code(400).send({ error: "No tenant selected" })
|
||||||
return reply.code(404).send({ error: "User not found or not in tenant" });
|
}
|
||||||
|
|
||||||
|
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"
|
} from "drizzle-orm"
|
||||||
|
|
||||||
|
|
||||||
import {
|
|
||||||
projects,
|
import {resourceConfig} from "../../resource.config";
|
||||||
customers,
|
|
||||||
plants,
|
|
||||||
contracts,
|
|
||||||
projecttypes,
|
|
||||||
createddocuments,
|
|
||||||
files,
|
|
||||||
events,
|
|
||||||
tasks, contacts, vendors
|
|
||||||
} from "../../../db/schema"
|
|
||||||
import * as sea from "node:sea";
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// SQL Volltextsuche auf mehreren Feldern
|
// SQL Volltextsuche auf mehreren Feldern
|
||||||
@@ -50,7 +40,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// LIST
|
// LIST
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
/*server.get("/resource/:resource", async (req, reply) => {
|
server.get("/resource/:resource", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const tenantId = req.user?.tenant_id
|
const tenantId = req.user?.tenant_id
|
||||||
if (!tenantId)
|
if (!tenantId)
|
||||||
@@ -62,34 +52,85 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
asc?: string
|
asc?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {resource} = req.params as {resource: string}
|
||||||
|
const table = resourceConfig[resource].table
|
||||||
|
|
||||||
// WHERE-Basis
|
// WHERE-Basis
|
||||||
let whereCond: any = eq(projects.tenant, tenantId)
|
let whereCond: any = eq(table.tenant, tenantId)
|
||||||
|
|
||||||
// 🔍 SQL Search
|
// 🔍 SQL Search
|
||||||
const searchCond = buildProjectSearch(search)
|
if(search) {
|
||||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
const searchCond = buildSearchCondition(
|
||||||
|
table,
|
||||||
|
resourceConfig[resource].searchColumns,
|
||||||
|
search.trim()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (searchCond) {
|
||||||
|
whereCond = and(whereCond, searchCond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Base Query
|
// Base Query
|
||||||
let q = server.db.select().from(projects).where(whereCond)
|
let q = server.db.select().from(table).where(whereCond)
|
||||||
|
|
||||||
// Sortierung
|
// Sortierung
|
||||||
if (sort) {
|
if (sort) {
|
||||||
const col = (projects as any)[sort]
|
const col = (table as any)[sort]
|
||||||
if (col) {
|
if (col) {
|
||||||
|
//@ts-ignore
|
||||||
q = ascQuery === "true"
|
q = ascQuery === "true"
|
||||||
? q.orderBy(asc(col))
|
? q.orderBy(asc(col))
|
||||||
: q.orderBy(desc(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
|
return data
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("ERROR /resource/projects", err)
|
console.error("ERROR /resource/:resource", err)
|
||||||
return reply.code(500).send({ error: "Internal Server Error" })
|
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);
|
let whereCond: any = eq(table.tenant, tenantId);
|
||||||
|
|
||||||
@@ -160,7 +170,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
if(search) {
|
if(search) {
|
||||||
const searchCond = buildSearchCondition(
|
const searchCond = buildSearchCondition(
|
||||||
table,
|
table,
|
||||||
config[resource].searchColumns,
|
resourceConfig[resource].searchColumns,
|
||||||
search.trim()
|
search.trim()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -225,7 +235,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
if (sort?.length > 0) {
|
if (sort?.length > 0) {
|
||||||
const s = sort[0];
|
const s = sort[0];
|
||||||
const col = (projects as any)[s.field];
|
const col = (table as any)[s.field];
|
||||||
if (col) {
|
if (col) {
|
||||||
orderField = col;
|
orderField = col;
|
||||||
direction = s.direction === "asc" ? "asc" : "desc";
|
direction = s.direction === "asc" ? "asc" : "desc";
|
||||||
@@ -268,17 +278,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
let maps = {}
|
let maps = {}
|
||||||
let data = []
|
let data = []
|
||||||
|
|
||||||
if(config[resource].mtoLoad) {
|
if(resourceConfig[resource].mtoLoad) {
|
||||||
config[resource].mtoLoad.forEach(relation => {
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))];
|
ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))];
|
||||||
})
|
})
|
||||||
|
|
||||||
for await (const relation of config[resource].mtoLoad ) {
|
for await (const relation of resourceConfig[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])) : []
|
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]));
|
maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i]));
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -287,12 +297,14 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
...row
|
...row
|
||||||
}
|
}
|
||||||
|
|
||||||
config[resource].mtoLoad.forEach(relation => {
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
|
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
|
||||||
})
|
})
|
||||||
|
|
||||||
return toReturn
|
return toReturn
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
data = rows
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------
|
// -----------------------------------------------
|
||||||
@@ -318,96 +330,55 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// DETAIL (mit JOINS)
|
// DETAIL (mit JOINS)
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
/*server.get("/resource/projects/:id", async (req, reply) => {
|
server.get("/resource/:resource/:id", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params as { id: string }
|
const { id } = req.params as { id: string }
|
||||||
const tenantId = req.user?.tenant_id
|
const tenantId = req.user?.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 pid = Number(id)
|
const {resource} = req.params as { resource: string }
|
||||||
|
const table = resourceConfig[resource].table
|
||||||
|
|
||||||
const projRows = await server.db
|
const projRows = await server.db
|
||||||
.select()
|
.select()
|
||||||
.from(projects)
|
.from(table)
|
||||||
.where(and(eq(projects.id, pid), eq(projects.tenant, tenantId)))
|
.where(and(eq(table.id, id), eq(table.tenant, tenantId)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!projRows.length)
|
if (!projRows.length)
|
||||||
return reply.code(404).send({ error: "Project not found" })
|
return reply.code(404).send({ error: "Resource not found" })
|
||||||
|
|
||||||
const project = projRows[0]
|
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// LOAD RELATIONS
|
// LOAD RELATIONS
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const [
|
|
||||||
customerRecord,
|
|
||||||
plantRecord,
|
|
||||||
contractRecord,
|
|
||||||
projectTypeRecord,
|
|
||||||
projectTasks,
|
|
||||||
projectFiles,
|
|
||||||
projectDocuments,
|
|
||||||
projectEvents,
|
|
||||||
] = await Promise.all([
|
|
||||||
|
|
||||||
project.customer
|
let ids = {}
|
||||||
? server.db.select().from(customers).where(eq(customers.id, project.customer))
|
let lists = {}
|
||||||
: [],
|
let maps = {}
|
||||||
|
let data = {
|
||||||
project.plant
|
...projRows[0]
|
||||||
? 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.error("ERROR /resource/projects/:id", err)
|
console.error("ERROR /resource/projects/:id", err)
|
||||||
return reply.code(500).send({ error: "Internal Server Error" })
|
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