863 lines
33 KiB
TypeScript
863 lines
33 KiB
TypeScript
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
import { and, eq, inArray, isNull } from "drizzle-orm";
|
|
|
|
import {
|
|
authTenantUsers,
|
|
authProfiles,
|
|
authRoles,
|
|
authUserRoles,
|
|
authUsers,
|
|
filetags,
|
|
folders,
|
|
tenants,
|
|
} from "../../db/schema";
|
|
import { generateRandomPassword, hashPassword } from "../utils/password";
|
|
|
|
export default async function adminRoutes(server: FastifyInstance) {
|
|
const deriveNameFromEmail = (email: string) => {
|
|
const localPart = email.split("@")[0] || "Benutzer";
|
|
const normalized = localPart.replace(/[._-]+/g, " ").trim();
|
|
const parts = normalized.split(/\s+/).filter(Boolean);
|
|
const firstName = parts[0]
|
|
? parts[0].charAt(0).toUpperCase() + parts[0].slice(1)
|
|
: "Neuer";
|
|
const lastName = parts.length > 1
|
|
? parts.slice(1).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ")
|
|
: "Benutzer";
|
|
|
|
return { first_name: firstName, last_name: lastName };
|
|
};
|
|
|
|
const createTenantSeeds = async (tenantId: number, createdBy: string) => {
|
|
const currentYear = new Date().getFullYear();
|
|
const timestamp = new Date();
|
|
|
|
const insertedTags = await server.db
|
|
.insert(filetags)
|
|
.values([
|
|
{
|
|
tenant: tenantId,
|
|
name: "Rechnungen",
|
|
color: "#16a34a",
|
|
createdDocumentType: "invoices",
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Angebote",
|
|
color: "#2563eb",
|
|
createdDocumentType: "quotes",
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Auftragsbestätigungen",
|
|
color: "#7c3aed",
|
|
createdDocumentType: "confirmationOrders",
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Lieferscheine",
|
|
color: "#ea580c",
|
|
createdDocumentType: "deliveryNotes",
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Eingangsrechnungen",
|
|
color: "#dc2626",
|
|
incomingDocumentType: "invoices",
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Mahnungen",
|
|
color: "#b91c1c",
|
|
incomingDocumentType: "reminders",
|
|
},
|
|
])
|
|
.returning({
|
|
id: filetags.id,
|
|
name: filetags.name,
|
|
createdDocumentType: filetags.createdDocumentType,
|
|
incomingDocumentType: filetags.incomingDocumentType,
|
|
});
|
|
|
|
const invoiceTag = insertedTags.find((tag) => tag.createdDocumentType === "invoices");
|
|
const quoteTag = insertedTags.find((tag) => tag.createdDocumentType === "quotes");
|
|
const confirmationTag = insertedTags.find((tag) => tag.createdDocumentType === "confirmationOrders");
|
|
const deliveryTag = insertedTags.find((tag) => tag.createdDocumentType === "deliveryNotes");
|
|
const incomingInvoiceTag = insertedTags.find((tag) => tag.incomingDocumentType === "invoices");
|
|
|
|
const insertedFolders = await server.db
|
|
.insert(folders)
|
|
.values([
|
|
{
|
|
tenant: tenantId,
|
|
name: "Ausgangsrechnungen",
|
|
function: "yearSubCategory",
|
|
icon: "i-heroicons-document-text",
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Angebote",
|
|
function: "yearSubCategory",
|
|
icon: "i-heroicons-document-duplicate",
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Auftragsbestätigungen",
|
|
function: "yearSubCategory",
|
|
icon: "i-heroicons-clipboard-document-check",
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Lieferscheine",
|
|
function: "yearSubCategory",
|
|
icon: "i-heroicons-truck",
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Eingangsrechnungen",
|
|
function: "yearSubCategory",
|
|
icon: "i-heroicons-inbox-arrow-down",
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: "Belege Bankeinzahlung",
|
|
function: "yearSubCategory",
|
|
icon: "i-heroicons-banknotes",
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
])
|
|
.returning({
|
|
id: folders.id,
|
|
name: folders.name,
|
|
});
|
|
|
|
const folderByName = new Map(insertedFolders.map((folder) => [folder.name, folder.id]));
|
|
|
|
await server.db
|
|
.insert(folders)
|
|
.values([
|
|
{
|
|
tenant: tenantId,
|
|
name: String(currentYear),
|
|
parent: folderByName.get("Ausgangsrechnungen"),
|
|
function: "invoices",
|
|
year: currentYear,
|
|
icon: "i-heroicons-document-text",
|
|
standardFiletype: invoiceTag?.id,
|
|
standardFiletypeIsOptional: false,
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: String(currentYear),
|
|
parent: folderByName.get("Angebote"),
|
|
function: "quotes",
|
|
year: currentYear,
|
|
icon: "i-heroicons-document-duplicate",
|
|
standardFiletype: quoteTag?.id,
|
|
standardFiletypeIsOptional: false,
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: String(currentYear),
|
|
parent: folderByName.get("Auftragsbestätigungen"),
|
|
function: "confirmationOrders",
|
|
year: currentYear,
|
|
icon: "i-heroicons-clipboard-document-check",
|
|
standardFiletype: confirmationTag?.id,
|
|
standardFiletypeIsOptional: false,
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: String(currentYear),
|
|
parent: folderByName.get("Lieferscheine"),
|
|
function: "deliveryNotes",
|
|
year: currentYear,
|
|
icon: "i-heroicons-truck",
|
|
standardFiletype: deliveryTag?.id,
|
|
standardFiletypeIsOptional: false,
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: String(currentYear),
|
|
parent: folderByName.get("Eingangsrechnungen"),
|
|
function: "incomingInvoices",
|
|
year: currentYear,
|
|
icon: "i-heroicons-inbox-arrow-down",
|
|
standardFiletype: incomingInvoiceTag?.id,
|
|
standardFiletypeIsOptional: false,
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
{
|
|
tenant: tenantId,
|
|
name: String(currentYear),
|
|
parent: folderByName.get("Belege Bankeinzahlung"),
|
|
function: "deposit",
|
|
year: currentYear,
|
|
icon: "i-heroicons-banknotes",
|
|
isSystemUsed: true,
|
|
updatedAt: timestamp,
|
|
updatedBy: createdBy,
|
|
},
|
|
]);
|
|
};
|
|
|
|
const requireAdmin = async (req: FastifyRequest, reply: FastifyReply) => {
|
|
if (!req.user?.user_id) {
|
|
reply.code(401).send({ error: "Unauthorized" });
|
|
return null;
|
|
}
|
|
|
|
const [currentUser] = await server.db
|
|
.select({
|
|
id: authUsers.id,
|
|
is_admin: authUsers.is_admin,
|
|
})
|
|
.from(authUsers)
|
|
.where(eq(authUsers.id, req.user.user_id))
|
|
.limit(1);
|
|
|
|
if (!currentUser?.is_admin) {
|
|
reply.code(403).send({ error: "Admin access required" });
|
|
return null;
|
|
}
|
|
|
|
return currentUser;
|
|
};
|
|
|
|
// -------------------------------------------------------------
|
|
// GET /admin/overview
|
|
// -------------------------------------------------------------
|
|
server.get("/admin/overview", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const [tenantRows, userRows, profileRows, membershipRows, roleRows, roleAssignmentRows] = await Promise.all([
|
|
server.db
|
|
.select({
|
|
id: tenants.id,
|
|
name: tenants.name,
|
|
short: tenants.short,
|
|
createdAt: tenants.createdAt,
|
|
locked: tenants.locked,
|
|
})
|
|
.from(tenants),
|
|
server.db
|
|
.select({
|
|
id: authUsers.id,
|
|
email: authUsers.email,
|
|
created_at: authUsers.created_at,
|
|
multiTenant: authUsers.multiTenant,
|
|
must_change_password: authUsers.must_change_password,
|
|
is_admin: authUsers.is_admin,
|
|
})
|
|
.from(authUsers),
|
|
server.db
|
|
.select({
|
|
id: authProfiles.id,
|
|
user_id: authProfiles.user_id,
|
|
tenant_id: authProfiles.tenant_id,
|
|
first_name: authProfiles.first_name,
|
|
last_name: authProfiles.last_name,
|
|
full_name: authProfiles.full_name,
|
|
email: authProfiles.email,
|
|
active: authProfiles.active,
|
|
})
|
|
.from(authProfiles),
|
|
server.db
|
|
.select()
|
|
.from(authTenantUsers),
|
|
server.db
|
|
.select({
|
|
id: authRoles.id,
|
|
name: authRoles.name,
|
|
description: authRoles.description,
|
|
tenant_id: authRoles.tenant_id,
|
|
})
|
|
.from(authRoles),
|
|
server.db
|
|
.select({
|
|
user_id: authUserRoles.user_id,
|
|
role_id: authUserRoles.role_id,
|
|
tenant_id: authUserRoles.tenant_id,
|
|
})
|
|
.from(authUserRoles),
|
|
]);
|
|
|
|
const users = userRows.map((user) => {
|
|
const profiles = profileRows.filter((profile) => profile.user_id === user.id);
|
|
const memberships = membershipRows.filter((membership) => membership.user_id === user.id);
|
|
const roleAssignments = roleAssignmentRows.filter((assignment) => assignment.user_id === user.id);
|
|
const preferredProfile = profiles.find((profile) => profile.active) || profiles[0];
|
|
const fallbackName = deriveNameFromEmail(user.email);
|
|
|
|
return {
|
|
...user,
|
|
display_name: preferredProfile?.full_name || user.email,
|
|
profile_defaults: {
|
|
first_name: preferredProfile?.first_name || fallbackName.first_name,
|
|
last_name: preferredProfile?.last_name || fallbackName.last_name,
|
|
},
|
|
profiles,
|
|
tenant_ids: memberships.map((membership) => membership.tenant_id),
|
|
role_assignments: roleAssignments,
|
|
};
|
|
});
|
|
|
|
const tenantsWithCounts = tenantRows.map((tenant) => ({
|
|
...tenant,
|
|
user_count: membershipRows.filter((membership) => membership.tenant_id === tenant.id).length,
|
|
}));
|
|
|
|
return {
|
|
users,
|
|
tenants: tenantsWithCounts,
|
|
roles: roleRows,
|
|
unassignedProfiles: profileRows.filter((profile) => !profile.user_id),
|
|
memberships: membershipRows,
|
|
roleAssignments: roleAssignmentRows,
|
|
};
|
|
} catch (err) {
|
|
console.error("ERROR /admin/overview:", err);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------
|
|
// POST /admin/users
|
|
// -------------------------------------------------------------
|
|
server.post("/admin/users", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const body = req.body as {
|
|
email?: string;
|
|
password?: string;
|
|
is_admin?: boolean;
|
|
multiTenant?: boolean;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
};
|
|
|
|
const email = body.email?.trim().toLowerCase();
|
|
if (!email) {
|
|
return reply.code(400).send({ error: "email required" });
|
|
}
|
|
|
|
const existingUsers = await server.db
|
|
.select({ id: authUsers.id })
|
|
.from(authUsers)
|
|
.where(eq(authUsers.email, email))
|
|
.limit(1);
|
|
|
|
if (existingUsers.length) {
|
|
return reply.code(409).send({ error: "User with this email already exists" });
|
|
}
|
|
|
|
const initialPassword = body.password?.trim() || generateRandomPassword(14);
|
|
const passwordHash = await hashPassword(initialPassword);
|
|
|
|
const [createdUser] = await server.db
|
|
.insert(authUsers)
|
|
.values({
|
|
email,
|
|
passwordHash,
|
|
is_admin: Boolean(body.is_admin),
|
|
multiTenant: typeof body.multiTenant === "boolean" ? body.multiTenant : true,
|
|
must_change_password: true,
|
|
updatedAt: new Date(),
|
|
})
|
|
.returning({
|
|
id: authUsers.id,
|
|
email: authUsers.email,
|
|
created_at: authUsers.created_at,
|
|
multiTenant: authUsers.multiTenant,
|
|
must_change_password: authUsers.must_change_password,
|
|
is_admin: authUsers.is_admin,
|
|
});
|
|
|
|
return {
|
|
user: createdUser,
|
|
initialPassword,
|
|
profile_defaults: {
|
|
first_name: body.first_name?.trim() || deriveNameFromEmail(email).first_name,
|
|
last_name: body.last_name?.trim() || deriveNameFromEmail(email).last_name,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
console.error("ERROR /admin/users:", err);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------
|
|
// POST /admin/tenants
|
|
// -------------------------------------------------------------
|
|
server.post("/admin/tenants", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const body = req.body as {
|
|
name?: string;
|
|
short?: string;
|
|
};
|
|
|
|
const name = body.name?.trim();
|
|
const short = body.short?.trim();
|
|
|
|
if (!name || !short) {
|
|
return reply.code(400).send({ error: "name and short required" });
|
|
}
|
|
|
|
const [createdTenant] = await server.db
|
|
.insert(tenants)
|
|
.values({
|
|
name,
|
|
short,
|
|
updatedAt: new Date(),
|
|
updatedBy: currentUser.id,
|
|
})
|
|
.returning({
|
|
id: tenants.id,
|
|
name: tenants.name,
|
|
short: tenants.short,
|
|
createdAt: tenants.createdAt,
|
|
locked: tenants.locked,
|
|
});
|
|
|
|
await createTenantSeeds(createdTenant.id, currentUser.id);
|
|
|
|
return { tenant: createdTenant };
|
|
} catch (err) {
|
|
console.error("ERROR /admin/tenants:", err);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------
|
|
// PUT /admin/users/:user_id
|
|
// -------------------------------------------------------------
|
|
server.put("/admin/users/:user_id", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const { user_id } = req.params as { user_id: string };
|
|
const body = req.body as {
|
|
email?: string;
|
|
multiTenant?: boolean;
|
|
must_change_password?: boolean;
|
|
is_admin?: boolean;
|
|
};
|
|
|
|
const updateData: Record<string, any> = {
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
if (typeof body.email === "string") updateData.email = body.email.trim().toLowerCase();
|
|
if (typeof body.multiTenant === "boolean") updateData.multiTenant = body.multiTenant;
|
|
if (typeof body.must_change_password === "boolean") updateData.must_change_password = body.must_change_password;
|
|
if (typeof body.is_admin === "boolean") updateData.is_admin = body.is_admin;
|
|
|
|
const [updatedUser] = await server.db
|
|
.update(authUsers)
|
|
.set(updateData)
|
|
.where(eq(authUsers.id, user_id))
|
|
.returning({
|
|
id: authUsers.id,
|
|
email: authUsers.email,
|
|
created_at: authUsers.created_at,
|
|
multiTenant: authUsers.multiTenant,
|
|
must_change_password: authUsers.must_change_password,
|
|
is_admin: authUsers.is_admin,
|
|
});
|
|
|
|
if (!updatedUser) {
|
|
return reply.code(404).send({ error: "User not found" });
|
|
}
|
|
|
|
return { user: updatedUser };
|
|
} catch (err) {
|
|
console.error("ERROR /admin/users/:user_id:", err);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------
|
|
// PUT /admin/tenants/:tenant_id
|
|
// -------------------------------------------------------------
|
|
server.put("/admin/tenants/:tenant_id", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const { tenant_id } = req.params as { tenant_id: string };
|
|
const body = req.body as {
|
|
name?: string;
|
|
short?: string;
|
|
};
|
|
|
|
const updateData: Record<string, any> = {
|
|
updatedAt: new Date(),
|
|
updatedBy: currentUser.id,
|
|
};
|
|
|
|
if (typeof body.name === "string") updateData.name = body.name.trim();
|
|
if (typeof body.short === "string") updateData.short = body.short.trim();
|
|
|
|
const [updatedTenant] = await server.db
|
|
.update(tenants)
|
|
.set(updateData)
|
|
.where(eq(tenants.id, Number(tenant_id)))
|
|
.returning({
|
|
id: tenants.id,
|
|
name: tenants.name,
|
|
short: tenants.short,
|
|
createdAt: tenants.createdAt,
|
|
locked: tenants.locked,
|
|
});
|
|
|
|
if (!updatedTenant) {
|
|
return reply.code(404).send({ error: "Tenant not found" });
|
|
}
|
|
|
|
return { tenant: updatedTenant };
|
|
} catch (err) {
|
|
console.error("ERROR /admin/tenants/:tenant_id:", err);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------
|
|
// PUT /admin/users/:user_id/access
|
|
// -------------------------------------------------------------
|
|
server.put("/admin/users/:user_id/access", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const { user_id } = req.params as { user_id: string };
|
|
const body = req.body as {
|
|
tenant_ids?: number[];
|
|
role_assignments?: { tenant_id: number; role_id: string }[];
|
|
profile_defaults?: { first_name?: string; last_name?: string };
|
|
profile_assignments?: { tenant_id: number; profile_id?: string | null }[];
|
|
};
|
|
|
|
const tenantIds = Array.from(new Set((body.tenant_ids || []).map((tenantId) => Number(tenantId)).filter(Boolean)));
|
|
const requestedAssignments = (body.role_assignments || [])
|
|
.map((assignment) => ({
|
|
tenant_id: Number(assignment.tenant_id),
|
|
role_id: assignment.role_id,
|
|
}))
|
|
.filter((assignment) => assignment.tenant_id && assignment.role_id && tenantIds.includes(assignment.tenant_id));
|
|
|
|
const [targetUser] = await server.db
|
|
.select({ id: authUsers.id, email: authUsers.email })
|
|
.from(authUsers)
|
|
.where(eq(authUsers.id, user_id))
|
|
.limit(1);
|
|
|
|
if (!targetUser) {
|
|
return reply.code(404).send({ error: "User not found" });
|
|
}
|
|
|
|
const availableRoles = requestedAssignments.length
|
|
? await server.db
|
|
.select({
|
|
id: authRoles.id,
|
|
tenant_id: authRoles.tenant_id,
|
|
})
|
|
.from(authRoles)
|
|
.where(
|
|
inArray(
|
|
authRoles.id,
|
|
requestedAssignments.map((assignment) => assignment.role_id)
|
|
)
|
|
)
|
|
: [];
|
|
|
|
const validRoleIds = new Set(
|
|
availableRoles
|
|
.filter((role) =>
|
|
role.tenant_id === null ||
|
|
requestedAssignments.some((assignment) => assignment.role_id === role.id && assignment.tenant_id === role.tenant_id)
|
|
)
|
|
.map((role) => role.id)
|
|
);
|
|
|
|
const validAssignments = requestedAssignments.filter((assignment) => validRoleIds.has(assignment.role_id));
|
|
const existingMemberships = await server.db
|
|
.select()
|
|
.from(authTenantUsers)
|
|
.where(eq(authTenantUsers.user_id, user_id));
|
|
const removedTenantIds = existingMemberships
|
|
.map((membership) => membership.tenant_id)
|
|
.filter((tenantId) => !tenantIds.includes(tenantId));
|
|
|
|
const existingUserProfiles = await server.db
|
|
.select({
|
|
id: authProfiles.id,
|
|
tenant_id: authProfiles.tenant_id,
|
|
})
|
|
.from(authProfiles)
|
|
.where(eq(authProfiles.user_id, user_id));
|
|
|
|
const unassignedProfiles = tenantIds.length
|
|
? await server.db
|
|
.select({
|
|
id: authProfiles.id,
|
|
tenant_id: authProfiles.tenant_id,
|
|
})
|
|
.from(authProfiles)
|
|
.where(
|
|
and(
|
|
inArray(authProfiles.tenant_id, tenantIds),
|
|
isNull(authProfiles.user_id)
|
|
)
|
|
)
|
|
: [];
|
|
|
|
const fallbackName = deriveNameFromEmail(targetUser.email);
|
|
const profileDefaults = {
|
|
first_name: body.profile_defaults?.first_name?.trim() || fallbackName.first_name,
|
|
last_name: body.profile_defaults?.last_name?.trim() || fallbackName.last_name,
|
|
};
|
|
const requestedProfileAssignments = new Map<number, string>(
|
|
(body.profile_assignments || [])
|
|
.filter((assignment) => assignment?.tenant_id && assignment.profile_id)
|
|
.map((assignment) => [Number(assignment.tenant_id), String(assignment.profile_id)])
|
|
);
|
|
|
|
await server.db
|
|
.delete(authUserRoles)
|
|
.where(eq(authUserRoles.user_id, user_id));
|
|
|
|
await server.db
|
|
.delete(authTenantUsers)
|
|
.where(eq(authTenantUsers.user_id, user_id));
|
|
|
|
if (tenantIds.length) {
|
|
await server.db
|
|
.insert(authTenantUsers)
|
|
.values(
|
|
tenantIds.map((tenantId) => ({
|
|
tenant_id: tenantId,
|
|
user_id,
|
|
created_by: currentUser.id,
|
|
}))
|
|
);
|
|
}
|
|
|
|
if (validAssignments.length) {
|
|
await server.db
|
|
.insert(authUserRoles)
|
|
.values(
|
|
validAssignments.map((assignment) => ({
|
|
user_id,
|
|
tenant_id: assignment.tenant_id,
|
|
role_id: assignment.role_id,
|
|
created_by: currentUser.id,
|
|
}))
|
|
);
|
|
}
|
|
|
|
if (removedTenantIds.length) {
|
|
await server.db
|
|
.update(authProfiles)
|
|
.set({ user_id: null })
|
|
.where(
|
|
and(
|
|
eq(authProfiles.user_id, user_id),
|
|
inArray(authProfiles.tenant_id, removedTenantIds)
|
|
)
|
|
);
|
|
}
|
|
|
|
const existingProfileTenantIds = new Set(existingUserProfiles.map((profile) => profile.tenant_id));
|
|
|
|
for (const tenantId of tenantIds) {
|
|
if (existingProfileTenantIds.has(tenantId)) continue;
|
|
|
|
const requestedProfileId = requestedProfileAssignments.get(tenantId);
|
|
const freeProfile = requestedProfileId
|
|
? unassignedProfiles.find((profile) => profile.id === requestedProfileId && profile.tenant_id === tenantId)
|
|
: null;
|
|
|
|
if (freeProfile) {
|
|
await server.db
|
|
.update(authProfiles)
|
|
.set({ user_id })
|
|
.where(eq(authProfiles.id, freeProfile.id));
|
|
continue;
|
|
}
|
|
|
|
await server.db
|
|
.insert(authProfiles)
|
|
.values({
|
|
user_id,
|
|
tenant_id: tenantId,
|
|
first_name: profileDefaults.first_name,
|
|
last_name: profileDefaults.last_name,
|
|
email: targetUser.email,
|
|
active: true,
|
|
});
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
tenant_ids: tenantIds,
|
|
role_assignments: validAssignments,
|
|
};
|
|
} catch (err) {
|
|
console.error("ERROR /admin/users/:user_id/access:", err);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------
|
|
// POST /admin/add-user-to-tenant
|
|
// -------------------------------------------------------------
|
|
server.post("/admin/add-user-to-tenant", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const body = req.body as {
|
|
user_id: string;
|
|
tenant_id: number;
|
|
role?: string;
|
|
mode?: "single" | "multi";
|
|
};
|
|
|
|
if (!body.user_id || !body.tenant_id) {
|
|
return reply.code(400).send({
|
|
error: "user_id and tenant_id required"
|
|
});
|
|
}
|
|
|
|
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)
|
|
.values({
|
|
user_id: body.user_id,
|
|
tenant_id: body.tenant_id,
|
|
created_by: currentUser.id,
|
|
});
|
|
|
|
return { success: true, mode };
|
|
|
|
} catch (err) {
|
|
console.error("ERROR /admin/add-user-to-tenant:", err);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
});
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
// GET /admin/user-tenants/:user_id
|
|
// -------------------------------------------------------------
|
|
server.get("/admin/user-tenants/:user_id", async (req, reply) => {
|
|
try {
|
|
const currentUser = await requireAdmin(req, reply);
|
|
if (!currentUser) return;
|
|
|
|
const { user_id } = req.params as { user_id: string };
|
|
|
|
if (!user_id) {
|
|
return reply.code(400).send({ error: "user_id required" });
|
|
}
|
|
|
|
// ----------------------------
|
|
// 1) User existiert?
|
|
// ----------------------------
|
|
const [user] = await server.db
|
|
.select()
|
|
.from(authUsers)
|
|
.where(eq(authUsers.id, user_id))
|
|
.limit(1);
|
|
|
|
if (!user) {
|
|
return reply.code(400).send({ error: "faulty user_id presented" });
|
|
}
|
|
|
|
// ----------------------------
|
|
// 2) Tenants Join über auth_tenant_users
|
|
// ----------------------------
|
|
const tenantRecords = await server.db
|
|
.select({
|
|
id: tenants.id,
|
|
name: tenants.name,
|
|
short: tenants.short,
|
|
locked: tenants.locked,
|
|
numberRanges: tenants.numberRanges,
|
|
accountChart: tenants.accountChart,
|
|
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" });
|
|
}
|
|
});
|
|
|
|
}
|