Files
FEDEO/backend/src/routes/tenant.ts
2026-02-15 13:29:26 +01:00

421 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FastifyInstance } from "fastify"
import jwt from "jsonwebtoken"
import { secrets } from "../utils/secrets"
import { createHash, randomBytes } from "node:crypto"
import {
authTenantUsers,
authUsers,
authProfiles,
tenants,
m2mApiKeys
} from "../../db/schema"
import {and, desc, eq, inArray} from "drizzle-orm"
export default async function tenantRoutes(server: FastifyInstance) {
const generateApiKey = () => {
const raw = randomBytes(32).toString("base64url")
return `fedeo_m2m_${raw}`
}
const hashApiKey = (apiKey: string) =>
createHash("sha256").update(apiKey, "utf8").digest("hex")
// -------------------------------------------------------------
// GET CURRENT TENANT
// -------------------------------------------------------------
server.get("/tenant", async (req) => {
if (req.tenant) {
return {
message: `Hallo vom Tenant ${req.tenant?.name}`,
tenant_id: req.tenant?.id,
}
}
return {
message: "Server ist im MultiTenant-Modus es werden alle verfügbaren Tenants geladen."
}
})
// -------------------------------------------------------------
// SWITCH TENANT
// -------------------------------------------------------------
server.post("/tenant/switch", async (req, reply) => {
try {
if (!req.user) {
return reply.code(401).send({ error: "Unauthorized" })
}
const { tenant_id } = req.body as { tenant_id: string }
if (!tenant_id) return reply.code(400).send({ error: "tenant_id required" })
// prüfen ob der User zu diesem Tenant gehört
const membership = await server.db
.select()
.from(authTenantUsers)
.where(and(
eq(authTenantUsers.user_id, req.user.user_id),
eq(authTenantUsers.tenant_id, Number(tenant_id))
))
if (!membership.length) {
return reply.code(403).send({ error: "Not a member of this tenant" })
}
// JWT neu erzeugen
const token = jwt.sign(
{
user_id: req.user.user_id,
email: req.user.email,
tenant_id,
},
secrets.JWT_SECRET!,
{ expiresIn: "6h" }
)
reply.setCookie("token", token, {
path: "/",
httpOnly: true,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 6,
})
return { token }
} catch (err) {
console.error("TENANT SWITCH ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// TENANT USERS (auth_users + auth_profiles)
// -------------------------------------------------------------
server.get("/tenant/users", async (req, reply) => {
try {
const authUser = req.user
if (!authUser) return reply.code(401).send({ error: "Unauthorized" })
const tenantId = authUser.tenant_id
// 1) auth_tenant_users → user_ids
const tenantUsers = await server.db
.select()
.from(authTenantUsers)
.where(eq(authTenantUsers.tenant_id, tenantId))
const userIds = tenantUsers.map(u => u.user_id)
if (!userIds.length) {
return { tenant_id: tenantId, users: [] }
}
// 2) auth_users laden
const users = await server.db
.select()
.from(authUsers)
.where(inArray(authUsers.id, userIds))
// 3) auth_profiles pro Tenant laden
const profiles = await server.db
.select()
.from(authProfiles)
.where(
and(
eq(authProfiles.tenant_id, tenantId),
inArray(authProfiles.user_id, userIds)
))
const combined = users.map(u => {
const profile = profiles.find(p => p.user_id === u.id)
return {
id: u.id,
email: u.email,
profile,
full_name: profile?.full_name ?? null
}
})
return { tenant_id: tenantId, users: combined }
} catch (err) {
console.error("/tenant/users ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// TENANT PROFILES
// -------------------------------------------------------------
server.get("/tenant/profiles", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
const data = await server.db
.select()
.from(authProfiles)
.where(eq(authProfiles.tenant_id, tenantId))
return { data }
} catch (err) {
console.error("/tenant/profiles ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// UPDATE NUMBER RANGE
// -------------------------------------------------------------
server.put("/tenant/numberrange/:numberrange", async (req, reply) => {
try {
const user = req.user
if (!user) return reply.code(401).send({ error: "Unauthorized" })
const { numberrange } = req.params as { numberrange: string }
const { numberRange } = req.body as { numberRange: any }
if (!numberRange) {
return reply.code(400).send({ error: "numberRange required" })
}
const tenantId = Number(user.tenant_id)
const currentTenantRows = await server.db
.select()
.from(tenants)
.where(eq(tenants.id, tenantId))
const current = currentTenantRows[0]
if (!current) return reply.code(404).send({ error: "Tenant not found" })
const updatedRanges = {
//@ts-ignore
...current.numberRanges,
[numberrange]: numberRange
}
const updated = await server.db
.update(tenants)
.set({ numberRanges: updatedRanges })
.where(eq(tenants.id, tenantId))
.returning()
return updated[0]
} catch (err) {
console.error("/tenant/numberrange ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// UPDATE TENANT OTHER FIELDS
// -------------------------------------------------------------
server.put("/tenant/other/:id", async (req, reply) => {
try {
const user = req.user
if (!user) return reply.code(401).send({ error: "Unauthorized" })
const { id } = req.params as { id: string }
const { data } = req.body as { data: any }
if (!data) return reply.code(400).send({ error: "data required" })
const updated = await server.db
.update(tenants)
.set(data)
.where(eq(tenants.id, Number(user.tenant_id)))
.returning()
return updated[0]
} catch (err) {
console.error("/tenant/other ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// M2M API KEYS
// -------------------------------------------------------------
server.get("/tenant/api-keys", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
const keys = await server.db
.select({
id: m2mApiKeys.id,
name: m2mApiKeys.name,
tenant_id: m2mApiKeys.tenantId,
user_id: m2mApiKeys.userId,
active: m2mApiKeys.active,
key_prefix: m2mApiKeys.keyPrefix,
created_at: m2mApiKeys.createdAt,
updated_at: m2mApiKeys.updatedAt,
expires_at: m2mApiKeys.expiresAt,
last_used_at: m2mApiKeys.lastUsedAt,
})
.from(m2mApiKeys)
.where(eq(m2mApiKeys.tenantId, tenantId))
.orderBy(desc(m2mApiKeys.createdAt))
return keys
} catch (err) {
console.error("/tenant/api-keys GET ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
server.post("/tenant/api-keys", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
const creatorUserId = req.user?.user_id
if (!tenantId || !creatorUserId) {
return reply.code(401).send({ error: "Unauthorized" })
}
const { name, user_id, expires_at } = req.body as {
name: string
user_id: string
expires_at?: string | null
}
if (!name || !user_id) {
return reply.code(400).send({ error: "name and user_id are required" })
}
const userMembership = await server.db
.select()
.from(authTenantUsers)
.where(and(
eq(authTenantUsers.tenant_id, tenantId),
eq(authTenantUsers.user_id, user_id)
))
.limit(1)
if (!userMembership[0]) {
return reply.code(400).send({ error: "user_id is not assigned to this tenant" })
}
const plainApiKey = generateApiKey()
const keyPrefix = plainApiKey.slice(0, 16)
const keyHash = hashApiKey(plainApiKey)
const inserted = await server.db
.insert(m2mApiKeys)
.values({
tenantId,
userId: user_id,
createdBy: creatorUserId,
name,
keyPrefix,
keyHash,
expiresAt: expires_at ? new Date(expires_at) : null,
})
.returning({
id: m2mApiKeys.id,
name: m2mApiKeys.name,
tenant_id: m2mApiKeys.tenantId,
user_id: m2mApiKeys.userId,
key_prefix: m2mApiKeys.keyPrefix,
created_at: m2mApiKeys.createdAt,
expires_at: m2mApiKeys.expiresAt,
active: m2mApiKeys.active,
})
return reply.code(201).send({
...inserted[0],
api_key: plainApiKey, // only returned once
})
} catch (err) {
console.error("/tenant/api-keys POST ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
server.patch("/tenant/api-keys/:id", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
const { id } = req.params as { id: string }
const { name, active, expires_at } = req.body as {
name?: string
active?: boolean
expires_at?: string | null
}
const updateData: any = {
updatedAt: new Date()
}
if (name !== undefined) updateData.name = name
if (active !== undefined) updateData.active = active
if (expires_at !== undefined) updateData.expiresAt = expires_at ? new Date(expires_at) : null
const updated = await server.db
.update(m2mApiKeys)
.set(updateData)
.where(and(
eq(m2mApiKeys.id, id),
eq(m2mApiKeys.tenantId, tenantId)
))
.returning({
id: m2mApiKeys.id,
name: m2mApiKeys.name,
tenant_id: m2mApiKeys.tenantId,
user_id: m2mApiKeys.userId,
active: m2mApiKeys.active,
key_prefix: m2mApiKeys.keyPrefix,
updated_at: m2mApiKeys.updatedAt,
expires_at: m2mApiKeys.expiresAt,
last_used_at: m2mApiKeys.lastUsedAt,
})
if (!updated[0]) {
return reply.code(404).send({ error: "API key not found" })
}
return updated[0]
} catch (err) {
console.error("/tenant/api-keys PATCH ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
server.delete("/tenant/api-keys/:id", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
const { id } = req.params as { id: string }
await server.db
.delete(m2mApiKeys)
.where(and(
eq(m2mApiKeys.id, id),
eq(m2mApiKeys.tenantId, tenantId)
))
return { success: true }
} catch (err) {
console.error("/tenant/api-keys DELETE ERROR:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
}