Redone
This commit is contained in:
@@ -27,6 +27,7 @@ import helpdeskInboundRoutes from "./routes/helpdesk.inbound";
|
|||||||
import notificationsRoutes from "./routes/notifications";
|
import notificationsRoutes from "./routes/notifications";
|
||||||
import staffTimeRoutes from "./routes/staff/time";
|
import staffTimeRoutes from "./routes/staff/time";
|
||||||
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
||||||
|
import userRoutes from "./routes/auth/user";
|
||||||
|
|
||||||
//Resources
|
//Resources
|
||||||
import resourceRoutes from "./routes/resources/main";
|
import resourceRoutes from "./routes/resources/main";
|
||||||
@@ -113,6 +114,7 @@ async function main() {
|
|||||||
await subApp.register(notificationsRoutes);
|
await subApp.register(notificationsRoutes);
|
||||||
await subApp.register(staffTimeRoutes);
|
await subApp.register(staffTimeRoutes);
|
||||||
await subApp.register(staffTimeConnectRoutes);
|
await subApp.register(staffTimeConnectRoutes);
|
||||||
|
await subApp.register(userRoutes);
|
||||||
|
|
||||||
|
|
||||||
await subApp.register(resourceRoutes);
|
await subApp.register(resourceRoutes);
|
||||||
|
|||||||
129
src/routes/auth/user.ts
Normal file
129
src/routes/auth/user.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
import { eq, and } from "drizzle-orm"
|
||||||
|
|
||||||
|
import {
|
||||||
|
authUsers,
|
||||||
|
authProfiles,
|
||||||
|
} from "../../../db/schema"
|
||||||
|
|
||||||
|
export default async function userRoutes(server: FastifyInstance) {
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// GET /user/:id
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
server.get("/user/:id", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const authUser = req.user
|
||||||
|
const { id } = req.params as { id: string }
|
||||||
|
|
||||||
|
if (!authUser) {
|
||||||
|
return reply.code(401).send({ error: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1️⃣ User laden
|
||||||
|
const [user] = await server.db
|
||||||
|
.select({
|
||||||
|
id: authUsers.id,
|
||||||
|
email: authUsers.email,
|
||||||
|
created_at: authUsers.created_at,
|
||||||
|
must_change_password: authUsers.must_change_password,
|
||||||
|
})
|
||||||
|
.from(authUsers)
|
||||||
|
.where(eq(authUsers.id, id))
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return reply.code(404).send({ error: "User not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2️⃣ Profil im Tenant
|
||||||
|
let profile = null
|
||||||
|
|
||||||
|
if (authUser.tenant_id) {
|
||||||
|
const [profileRow] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(authProfiles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(authProfiles.user_id, id),
|
||||||
|
eq(authProfiles.tenant_id, authUser.tenant_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
profile = profileRow || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, profile }
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("/user/:id ERROR", err)
|
||||||
|
return reply.code(500).send({ error: err.message || "Internal error" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// PUT /user/:id/profile
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
server.put("/user/:id/profile", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params as { id: string }
|
||||||
|
const { data } = req.body as { data?: Record<string, any> }
|
||||||
|
|
||||||
|
if (!req.user?.tenant_id) {
|
||||||
|
return reply.code(401).send({ error: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || typeof data !== "object") {
|
||||||
|
return reply.code(400).send({ error: "data object required" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1️⃣ Profil für diesen Tenant laden (damit wir die ID kennen)
|
||||||
|
const [profile] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(authProfiles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(authProfiles.user_id, id),
|
||||||
|
eq(authProfiles.tenant_id, req.user.tenant_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
return reply.code(404).send({ error: "Profile not found in tenant" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2️⃣ Timestamp-Felder normalisieren (falls welche drin sind)
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
if (!val) return null
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: any = { ...data }
|
||||||
|
|
||||||
|
// bekannte Date-Felder prüfen
|
||||||
|
if (data.entry_date !== undefined)
|
||||||
|
updateData.entry_date = normalizeDate(data.entry_date)
|
||||||
|
|
||||||
|
if (data.birthday !== undefined)
|
||||||
|
updateData.birthday = normalizeDate(data.birthday)
|
||||||
|
|
||||||
|
if (data.created_at !== undefined)
|
||||||
|
updateData.created_at = normalizeDate(data.created_at)
|
||||||
|
|
||||||
|
updateData.updated_at = new Date()
|
||||||
|
|
||||||
|
// 3️⃣ Update durchführen
|
||||||
|
const [updatedProfile] = await server.db
|
||||||
|
.update(authProfiles)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(authProfiles.id, profile.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return { profile: updatedProfile }
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("PUT /user/:id/profile ERROR", err)
|
||||||
|
return reply.code(500).send({ error: err.message || "Internal server error" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
|
|
||||||
|
|
||||||
import {resourceConfig} from "../../utils/resource.config";
|
import {resourceConfig} from "../../utils/resource.config";
|
||||||
|
import {useNextNumberRangeNumber} from "../../utils/functions";
|
||||||
|
import {stafftimeentries} from "../../../db/schema";
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// SQL Volltextsuche auf mehreren Feldern
|
// SQL Volltextsuche auf mehreren Feldern
|
||||||
@@ -422,4 +424,112 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
return reply.code(500).send({ error: "Internal Server Error" })
|
return reply.code(500).send({ error: "Internal Server Error" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Create
|
||||||
|
server.post("/resource/:resource", async (req, reply) => {
|
||||||
|
if (!req.user?.tenant_id) {
|
||||||
|
return reply.code(400).send({error: "No tenant selected"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {resource} = req.params as { resource: string };
|
||||||
|
const body = req.body as Record<string, any>;
|
||||||
|
|
||||||
|
const table = resourceConfig[resource].table
|
||||||
|
|
||||||
|
let createData = {
|
||||||
|
...body,
|
||||||
|
tenant: req.user.tenant_id,
|
||||||
|
archived: false, // Standardwert
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceConfig[resource].numberRangeHolder && !body[resourceConfig[resource]]) {
|
||||||
|
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
|
||||||
|
createData[resourceConfig[resource]] = result.usedNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
const [created] = await server.db
|
||||||
|
.insert(table)
|
||||||
|
.values(createData)
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
|
||||||
|
/*await insertHistoryItem(server, {
|
||||||
|
entity: resource,
|
||||||
|
entityId: data.id,
|
||||||
|
action: "created",
|
||||||
|
created_by: req.user.user_id,
|
||||||
|
tenant_id: req.user.tenant_id,
|
||||||
|
oldVal: null,
|
||||||
|
newVal: data,
|
||||||
|
text: `${dataType.labelSingle} erstellt`,
|
||||||
|
});*/
|
||||||
|
|
||||||
|
return created;
|
||||||
|
});
|
||||||
|
|
||||||
|
// UPDATE (inkl. Soft-Delete/Archive)
|
||||||
|
server.put("/resource/:resource/:id", async (req, reply) => {
|
||||||
|
const {resource, id} = req.params as { resource: string; id: string }
|
||||||
|
const body = req.body as Record<string, any>
|
||||||
|
|
||||||
|
const tenantId = (req.user as any)?.tenant_id
|
||||||
|
const userId = (req.user as any)?.user_id
|
||||||
|
|
||||||
|
if (!tenantId || !userId) {
|
||||||
|
return reply.code(401).send({error: "Unauthorized"})
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = resourceConfig[resource].table
|
||||||
|
|
||||||
|
//TODO: HISTORY
|
||||||
|
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(body)
|
||||||
|
|
||||||
|
Object.keys(body).forEach((key) => {
|
||||||
|
if(key.includes("_at") || key.includes("At")) {
|
||||||
|
body[key] = normalizeDate(body[key])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// vorherige Version für History laden
|
||||||
|
/*const {data: oldItem} = await server.supabase
|
||||||
|
.from(resource)
|
||||||
|
.select("*")
|
||||||
|
.eq("id", id)
|
||||||
|
.eq("tenant", tenantId)
|
||||||
|
.single()*/
|
||||||
|
|
||||||
|
const [updated] = await server.db
|
||||||
|
.update(table)
|
||||||
|
.set({...body, updated_at: new Date().toISOString(), updated_by: userId})
|
||||||
|
.where(and(
|
||||||
|
eq(table.id, id),
|
||||||
|
eq(table.tenant, tenantId)))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
//const diffs = diffObjects(oldItem, newItem);
|
||||||
|
|
||||||
|
|
||||||
|
/*for (const d of diffs) {
|
||||||
|
await insertHistoryItem(server, {
|
||||||
|
entity: resource,
|
||||||
|
entityId: id,
|
||||||
|
action: d.type,
|
||||||
|
created_by: userId,
|
||||||
|
tenant_id: tenantId,
|
||||||
|
oldVal: d.oldValue ? String(d.oldValue) : null,
|
||||||
|
newVal: d.newValue ? String(d.newValue) : null,
|
||||||
|
text: `Feld "${d.label}" ${d.typeLabel}: ${d.oldValue ?? ""} → ${d.newValue ?? ""}`,
|
||||||
|
});
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
tenants
|
tenants
|
||||||
} from "../../db/schema"
|
} from "../../db/schema"
|
||||||
|
|
||||||
import { eq } from "drizzle-orm"
|
import {and, eq, inArray} from "drizzle-orm"
|
||||||
|
|
||||||
|
|
||||||
export default async function tenantRoutes(server: FastifyInstance) {
|
export default async function tenantRoutes(server: FastifyInstance) {
|
||||||
@@ -48,12 +48,10 @@ export default async function tenantRoutes(server: FastifyInstance) {
|
|||||||
const membership = await server.db
|
const membership = await server.db
|
||||||
.select()
|
.select()
|
||||||
.from(authTenantUsers)
|
.from(authTenantUsers)
|
||||||
.where(
|
.where(and(
|
||||||
eq(authTenantUsers.user_id, req.user.user_id)
|
eq(authTenantUsers.user_id, req.user.user_id),
|
||||||
)
|
eq(authTenantUsers.tenant_id, Number(tenant_id))
|
||||||
.where(
|
))
|
||||||
eq(authTenantUsers.tenant_id, Number(tenant_id))
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!membership.length) {
|
if (!membership.length) {
|
||||||
return reply.code(403).send({ error: "Not a member of this tenant" })
|
return reply.code(403).send({ error: "Not a member of this tenant" })
|
||||||
@@ -120,8 +118,11 @@ export default async function tenantRoutes(server: FastifyInstance) {
|
|||||||
const profiles = await server.db
|
const profiles = await server.db
|
||||||
.select()
|
.select()
|
||||||
.from(authProfiles)
|
.from(authProfiles)
|
||||||
.where(eq(authProfiles.tenant_id, tenantId))
|
.where(
|
||||||
.where(inArray(authProfiles.user_id, userIds))
|
and(
|
||||||
|
eq(authProfiles.tenant_id, tenantId),
|
||||||
|
inArray(authProfiles.user_id, userIds)
|
||||||
|
))
|
||||||
|
|
||||||
const combined = users.map(u => {
|
const combined = users.map(u => {
|
||||||
const profile = profiles.find(p => p.user_id === u.id)
|
const profile = profiles.find(p => p.user_id === u.id)
|
||||||
@@ -192,6 +193,7 @@ export default async function tenantRoutes(server: FastifyInstance) {
|
|||||||
if (!current) return reply.code(404).send({ error: "Tenant not found" })
|
if (!current) return reply.code(404).send({ error: "Tenant not found" })
|
||||||
|
|
||||||
const updatedRanges = {
|
const updatedRanges = {
|
||||||
|
//@ts-ignore
|
||||||
...current.numberRanges,
|
...current.numberRanges,
|
||||||
[numberrange]: numberRange
|
[numberrange]: numberRange
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ export const resourceConfig = {
|
|||||||
searchColumns: ["name"],
|
searchColumns: ["name"],
|
||||||
mtoLoad: ["customer","plant","contract","projecttype"],
|
mtoLoad: ["customer","plant","contract","projecttype"],
|
||||||
mtmLoad: ["tasks", "files"],
|
mtmLoad: ["tasks", "files"],
|
||||||
table: projects
|
table: projects,
|
||||||
|
numberRangeHolder: "projectNumber"
|
||||||
},
|
},
|
||||||
customers: {
|
customers: {
|
||||||
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
||||||
mtmLoad: ["contacts","projects"],
|
mtmLoad: ["contacts","projects"],
|
||||||
table: customers,
|
table: customers,
|
||||||
|
numberRangeHolder: "customerNumber",
|
||||||
},
|
},
|
||||||
contacts: {
|
contacts: {
|
||||||
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
|
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
|
||||||
@@ -30,7 +32,8 @@ export const resourceConfig = {
|
|||||||
},
|
},
|
||||||
contracts: {
|
contracts: {
|
||||||
table: contracts,
|
table: contracts,
|
||||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"]
|
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
|
||||||
|
numberRangeHolder: "contractNumber",
|
||||||
},
|
},
|
||||||
plants: {
|
plants: {
|
||||||
table: plants,
|
table: plants,
|
||||||
@@ -43,6 +46,7 @@ export const resourceConfig = {
|
|||||||
vendors: {
|
vendors: {
|
||||||
table: vendors,
|
table: vendors,
|
||||||
searchColumns: ["name","vendorNumber","notes","defaultPaymentType"],
|
searchColumns: ["name","vendorNumber","notes","defaultPaymentType"],
|
||||||
|
numberRangeHolder: "vendorNumber",
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
table: files
|
table: files
|
||||||
@@ -54,7 +58,8 @@ export const resourceConfig = {
|
|||||||
table: filetags
|
table: filetags
|
||||||
},
|
},
|
||||||
inventoryitems: {
|
inventoryitems: {
|
||||||
table: inventoryitems
|
table: inventoryitems,
|
||||||
|
numberRangeHolder: "articleNumber",
|
||||||
},
|
},
|
||||||
inventoryitemgroups: {
|
inventoryitemgroups: {
|
||||||
table: inventoryitemgroups
|
table: inventoryitemgroups
|
||||||
@@ -88,6 +93,7 @@ export const resourceConfig = {
|
|||||||
spaces: {
|
spaces: {
|
||||||
table: spaces,
|
table: spaces,
|
||||||
searchColumns: ["name","space_number","type","info_data"],
|
searchColumns: ["name","space_number","type","info_data"],
|
||||||
|
numberRangeHolder: "spaceNumber",
|
||||||
},
|
},
|
||||||
ownaccounts: {
|
ownaccounts: {
|
||||||
table: ownaccounts,
|
table: ownaccounts,
|
||||||
@@ -96,7 +102,8 @@ export const resourceConfig = {
|
|||||||
costcentres: {
|
costcentres: {
|
||||||
table: costcentres,
|
table: costcentres,
|
||||||
searchColumns: ["name","number","description"],
|
searchColumns: ["name","number","description"],
|
||||||
mtoLoad: ["vehicle","project","inventoryitem"]
|
mtoLoad: ["vehicle","project","inventoryitem"],
|
||||||
|
numberRangeHolder: "number",
|
||||||
},
|
},
|
||||||
tasks: {
|
tasks: {
|
||||||
table: tasks,
|
table: tasks,
|
||||||
|
|||||||
Reference in New Issue
Block a user