New Admin Dashboard
This commit is contained in:
@@ -1,19 +1,689 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { eq } from "drizzle-orm";
|
||||
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 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");
|
||||
|
||||
await server.db
|
||||
.insert(folders)
|
||||
.values([
|
||||
{
|
||||
tenant: tenantId,
|
||||
name: "Ausgangsrechnungen",
|
||||
function: "invoices",
|
||||
year: currentYear,
|
||||
icon: "i-heroicons-document-text",
|
||||
standardFiletype: invoiceTag?.id,
|
||||
standardFiletypeIsOptional: false,
|
||||
isSystemUsed: true,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: createdBy,
|
||||
},
|
||||
{
|
||||
tenant: tenantId,
|
||||
name: "Angebote",
|
||||
function: "quotes",
|
||||
year: currentYear,
|
||||
icon: "i-heroicons-document-duplicate",
|
||||
standardFiletype: quoteTag?.id,
|
||||
standardFiletypeIsOptional: false,
|
||||
isSystemUsed: true,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: createdBy,
|
||||
},
|
||||
{
|
||||
tenant: tenantId,
|
||||
name: "Auftragsbestätigungen",
|
||||
function: "confirmationOrders",
|
||||
year: currentYear,
|
||||
icon: "i-heroicons-clipboard-document-check",
|
||||
standardFiletype: confirmationTag?.id,
|
||||
standardFiletypeIsOptional: false,
|
||||
isSystemUsed: true,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: createdBy,
|
||||
},
|
||||
{
|
||||
tenant: tenantId,
|
||||
name: "Lieferscheine",
|
||||
function: "deliveryNotes",
|
||||
year: currentYear,
|
||||
icon: "i-heroicons-truck",
|
||||
standardFiletype: deliveryTag?.id,
|
||||
standardFiletypeIsOptional: false,
|
||||
isSystemUsed: true,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: createdBy,
|
||||
},
|
||||
{
|
||||
tenant: tenantId,
|
||||
name: "Eingangsrechnungen",
|
||||
function: "incomingInvoices",
|
||||
year: currentYear,
|
||||
icon: "i-heroicons-inbox-arrow-down",
|
||||
standardFiletype: incomingInvoiceTag?.id,
|
||||
standardFiletypeIsOptional: false,
|
||||
isSystemUsed: true,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: createdBy,
|
||||
},
|
||||
{
|
||||
tenant: tenantId,
|
||||
name: "Belege Bankeinzahlung",
|
||||
function: "deposit",
|
||||
year: currentYear,
|
||||
icon: "i-heroicons-banknotes",
|
||||
isSystemUsed: true,
|
||||
updatedAt: new Date(),
|
||||
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;
|
||||
@@ -44,11 +714,10 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
|
||||
await server.db
|
||||
.insert(authTenantUsers)
|
||||
// @ts-ignore
|
||||
.values({
|
||||
user_id: body.user_id,
|
||||
tenantId: body.tenant_id,
|
||||
role: body.role ?? "member",
|
||||
tenant_id: body.tenant_id,
|
||||
created_by: currentUser.id,
|
||||
});
|
||||
|
||||
return { success: true, mode };
|
||||
@@ -65,6 +734,9 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
// -------------------------------------------------------------
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user