M2M Api
This commit is contained in:
@@ -1,18 +1,26 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import jwt from "jsonwebtoken"
|
||||
import { secrets } from "../utils/secrets"
|
||||
import { createHash, randomBytes } from "node:crypto"
|
||||
|
||||
import {
|
||||
authTenantUsers,
|
||||
authUsers,
|
||||
authProfiles,
|
||||
tenants
|
||||
tenants,
|
||||
m2mApiKeys
|
||||
} from "../../db/schema"
|
||||
|
||||
import {and, eq, inArray} from "drizzle-orm"
|
||||
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")
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -73,7 +81,7 @@ export default async function tenantRoutes(server: FastifyInstance) {
|
||||
httpOnly: true,
|
||||
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
maxAge: 60 * 60 * 3,
|
||||
maxAge: 60 * 60 * 6,
|
||||
})
|
||||
|
||||
return { token }
|
||||
@@ -241,4 +249,172 @@ export default async function tenantRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 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" })
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user