feat: add manage api for projects, clients and references

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-20 05:51:56 +01:00
parent c63578b35b
commit 470ddbe8e7
13 changed files with 2330 additions and 6 deletions

View File

@@ -8,6 +8,7 @@
"start": "dotenv -e ../../.env node dist/index.js",
"build": "rm -rf dist && tsdown",
"gen:bots": "jiti scripts/get-bots.ts && biome format --write src/bots/bots.ts",
"test:manage": "jiti scripts/test-manage-api.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {

View File

@@ -0,0 +1,340 @@
/**
* One-off script to test all /manage/ API endpoints
*
* Usage:
* pnpm test:manage
* or
* pnpm jiti scripts/test-manage-api.ts
*
* Set API_URL environment variable to test against a different server:
* API_URL=http://localhost:3000 pnpm test:manage
*/
const CLIENT_ID = process.env.CLIENT_ID!;
const CLIENT_SECRET = process.env.CLIENT_SECRET!;
const API_BASE_URL = process.env.API_URL || 'http://localhost:3333';
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error('CLIENT_ID and CLIENT_SECRET must be set');
process.exit(1);
}
interface TestResult {
name: string;
method: string;
url: string;
status: number;
success: boolean;
error?: string;
data?: any;
}
const results: TestResult[] = [];
async function makeRequest(
method: string,
path: string,
body?: any,
): Promise<TestResult> {
const url = `${API_BASE_URL}${path}`;
const headers: Record<string, string> = {
'openpanel-client-id': CLIENT_ID,
'openpanel-client-secret': CLIENT_SECRET,
};
// Only set Content-Type if there's a body
if (body) {
headers['Content-Type'] = 'application/json';
}
try {
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const data = await response.json().catch(() => ({}));
return {
name: `${method} ${path}`,
method,
url,
status: response.status,
success: response.ok,
error: response.ok ? undefined : data.message || 'Request failed',
data: response.ok ? data : undefined,
};
} catch (error) {
return {
name: `${method} ${path}`,
method,
url,
status: 0,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async function testProjects() {
console.log('\n📁 Testing Projects endpoints...\n');
// Create project
const createResult = await makeRequest('POST', '/manage/projects', {
name: `Test Project ${Date.now()}`,
domain: 'https://example.com',
cors: ['https://example.com', 'https://www.example.com'],
crossDomain: false,
types: ['website'],
});
results.push(createResult);
console.log(
`✓ POST /manage/projects: ${createResult.success ? '✅' : '❌'} ${createResult.status}`,
);
if (createResult.error) console.log(` Error: ${createResult.error}`);
const projectId = createResult.data?.data?.id;
const clientId = createResult.data?.data?.client?.id;
const clientSecret = createResult.data?.data?.client?.secret;
if (projectId) {
console.log(` Created project: ${projectId}`);
if (clientId) console.log(` Created client: ${clientId}`);
if (clientSecret) console.log(` Client secret: ${clientSecret}`);
}
// List projects
const listResult = await makeRequest('GET', '/manage/projects');
results.push(listResult);
console.log(
`✓ GET /manage/projects: ${listResult.success ? '✅' : '❌'} ${listResult.status}`,
);
if (listResult.data?.data?.length) {
console.log(` Found ${listResult.data.data.length} projects`);
}
if (projectId) {
// Get project
const getResult = await makeRequest('GET', `/manage/projects/${projectId}`);
results.push(getResult);
console.log(
`✓ GET /manage/projects/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`,
);
// Update project
const updateResult = await makeRequest(
'PATCH',
`/manage/projects/${projectId}`,
{
name: 'Updated Test Project',
crossDomain: true,
},
);
results.push(updateResult);
console.log(
`✓ PATCH /manage/projects/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`,
);
// Delete project (soft delete)
const deleteResult = await makeRequest(
'DELETE',
`/manage/projects/${projectId}`,
);
results.push(deleteResult);
console.log(
`✓ DELETE /manage/projects/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`,
);
}
return { projectId, clientId };
}
async function testClients(projectId?: string) {
console.log('\n🔑 Testing Clients endpoints...\n');
// Create client
const createResult = await makeRequest('POST', '/manage/clients', {
name: `Test Client ${Date.now()}`,
projectId: projectId || undefined,
type: 'read',
});
results.push(createResult);
console.log(
`✓ POST /manage/clients: ${createResult.success ? '✅' : '❌'} ${createResult.status}`,
);
if (createResult.error) console.log(` Error: ${createResult.error}`);
const clientId = createResult.data?.data?.id;
const clientSecret = createResult.data?.data?.secret;
if (clientId) {
console.log(` Created client: ${clientId}`);
if (clientSecret) console.log(` Client secret: ${clientSecret}`);
}
// List clients
const listResult = await makeRequest(
'GET',
projectId ? `/manage/clients?projectId=${projectId}` : '/manage/clients',
);
results.push(listResult);
console.log(
`✓ GET /manage/clients: ${listResult.success ? '✅' : '❌'} ${listResult.status}`,
);
if (listResult.data?.data?.length) {
console.log(` Found ${listResult.data.data.length} clients`);
}
if (clientId) {
// Get client
const getResult = await makeRequest('GET', `/manage/clients/${clientId}`);
results.push(getResult);
console.log(
`✓ GET /manage/clients/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`,
);
// Update client
const updateResult = await makeRequest(
'PATCH',
`/manage/clients/${clientId}`,
{
name: 'Updated Test Client',
},
);
results.push(updateResult);
console.log(
`✓ PATCH /manage/clients/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`,
);
// Delete client
const deleteResult = await makeRequest(
'DELETE',
`/manage/clients/${clientId}`,
);
results.push(deleteResult);
console.log(
`✓ DELETE /manage/clients/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`,
);
}
}
async function testReferences(projectId?: string) {
console.log('\n📚 Testing References endpoints...\n');
if (!projectId) {
console.log(' ⚠️ Skipping references tests - no project ID available');
return;
}
// Create reference
const createResult = await makeRequest('POST', '/manage/references', {
projectId,
title: `Test Reference ${Date.now()}`,
description: 'This is a test reference',
datetime: new Date().toISOString(),
});
results.push(createResult);
console.log(
`✓ POST /manage/references: ${createResult.success ? '✅' : '❌'} ${createResult.status}`,
);
if (createResult.error) console.log(` Error: ${createResult.error}`);
const referenceId = createResult.data?.data?.id;
if (referenceId) {
console.log(` Created reference: ${referenceId}`);
}
// List references
const listResult = await makeRequest(
'GET',
`/manage/references?projectId=${projectId}`,
);
results.push(listResult);
console.log(
`✓ GET /manage/references: ${listResult.success ? '✅' : '❌'} ${listResult.status}`,
);
if (listResult.data?.data?.length) {
console.log(` Found ${listResult.data.data.length} references`);
}
if (referenceId) {
// Get reference
const getResult = await makeRequest(
'GET',
`/manage/references/${referenceId}`,
);
results.push(getResult);
console.log(
`✓ GET /manage/references/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`,
);
// Update reference
const updateResult = await makeRequest(
'PATCH',
`/manage/references/${referenceId}`,
{
title: 'Updated Test Reference',
description: 'Updated description',
datetime: new Date().toISOString(),
},
);
results.push(updateResult);
console.log(
`✓ PATCH /manage/references/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`,
);
// Delete reference
const deleteResult = await makeRequest(
'DELETE',
`/manage/references/${referenceId}`,
);
results.push(deleteResult);
console.log(
`✓ DELETE /manage/references/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`,
);
}
}
async function main() {
console.log('🚀 Testing Manage API Endpoints\n');
console.log(`API Base URL: ${API_BASE_URL}`);
console.log(`Client ID: ${CLIENT_ID}\n`);
try {
// Test projects first (creates a project we can use for other tests)
const { projectId } = await testProjects();
// Test clients
await testClients(projectId);
// Test references (requires a project)
await testReferences(projectId);
// Summary
console.log(`\n${'='.repeat(60)}`);
console.log('📊 Test Summary\n');
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
console.log(`Total tests: ${results.length}`);
console.log(`✅ Successful: ${successful}`);
console.log(`❌ Failed: ${failed}\n`);
if (failed > 0) {
console.log('Failed tests:');
results
.filter((r) => !r.success)
.forEach((r) => {
console.log(`${r.name} (${r.status})`);
if (r.error) console.log(` Error: ${r.error}`);
});
}
} catch (error) {
console.error('Fatal error:', error);
process.exit(1);
}
}
main().catch(console.error);

View File

@@ -0,0 +1,649 @@
import crypto from 'node:crypto';
import { HttpError } from '@/utils/errors';
import { stripTrailingSlash } from '@openpanel/common';
import { hashPassword } from '@openpanel/common/server';
import {
db,
getClientByIdCached,
getId,
getProjectByIdCached,
} from '@openpanel/db';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { z } from 'zod';
// Validation schemas
const zCreateProject = z.object({
name: z.string().min(1),
domain: z.string().url().or(z.literal('')).or(z.null()).optional(),
cors: z.array(z.string()).default([]),
crossDomain: z.boolean().optional().default(false),
types: z
.array(z.enum(['website', 'app', 'backend']))
.optional()
.default([]),
});
const zUpdateProject = z.object({
name: z.string().min(1).optional(),
domain: z.string().url().or(z.literal('')).or(z.null()).optional(),
cors: z.array(z.string()).optional(),
crossDomain: z.boolean().optional(),
allowUnsafeRevenueTracking: z.boolean().optional(),
});
const zCreateClient = z.object({
name: z.string().min(1),
projectId: z.string().optional(),
type: z.enum(['read', 'write', 'root']).optional().default('write'),
});
const zUpdateClient = z.object({
name: z.string().min(1).optional(),
});
const zCreateReference = z.object({
projectId: z.string(),
title: z.string().min(1),
description: z.string().optional(),
datetime: z.string(),
});
const zUpdateReference = z.object({
title: z.string().min(1).optional(),
description: z.string().optional(),
datetime: z.string().optional(),
});
// Projects CRUD
export async function listProjects(
request: FastifyRequest,
reply: FastifyReply,
) {
const projects = await db.project.findMany({
where: {
organizationId: request.client!.organizationId,
deleteAt: null,
},
orderBy: {
createdAt: 'desc',
},
});
reply.send({ data: projects });
}
export async function getProject(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply,
) {
const project = await db.project.findFirst({
where: {
id: request.params.id,
organizationId: request.client!.organizationId,
},
});
if (!project) {
throw new HttpError('Project not found', { status: 404 });
}
reply.send({ data: project });
}
export async function createProject(
request: FastifyRequest<{ Body: z.infer<typeof zCreateProject> }>,
reply: FastifyReply,
) {
const parsed = zCreateProject.safeParse(request.body);
if (parsed.success === false) {
return reply.status(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: parsed.error.errors,
});
}
const { name, domain, cors, crossDomain, types } = parsed.data;
// Generate a default client secret
const secret = `sec_${crypto.randomBytes(10).toString('hex')}`;
const clientData = {
organizationId: request.client!.organizationId,
name: 'First client',
type: 'write' as const,
secret: await hashPassword(secret),
};
const project = await db.project.create({
data: {
id: await getId('project', name),
organizationId: request.client!.organizationId,
name,
domain: domain ? stripTrailingSlash(domain) : null,
cors: cors.map((c) => stripTrailingSlash(c)),
crossDomain: crossDomain ?? false,
allowUnsafeRevenueTracking: false,
filters: [],
types,
clients: {
create: clientData,
},
},
include: {
clients: {
select: {
id: true,
},
},
},
});
// Clear cache
await Promise.all([
getProjectByIdCached.clear(project.id),
project.clients.map((client) => {
getClientByIdCached.clear(client.id);
}),
]);
reply.send({
data: {
...project,
client: project.clients[0]
? {
id: project.clients[0].id,
secret,
}
: null,
},
});
}
export async function updateProject(
request: FastifyRequest<{
Params: { id: string };
Body: z.infer<typeof zUpdateProject>;
}>,
reply: FastifyReply,
) {
const parsed = zUpdateProject.safeParse(request.body);
if (parsed.success === false) {
return reply.status(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: parsed.error.errors,
});
}
// Verify project exists and belongs to organization
const existing = await db.project.findFirst({
where: {
id: request.params.id,
organizationId: request.client!.organizationId,
},
include: {
clients: {
select: {
id: true,
},
},
},
});
if (!existing) {
throw new HttpError('Project not found', { status: 404 });
}
const updateData: any = {};
if (parsed.data.name !== undefined) {
updateData.name = parsed.data.name;
}
if (parsed.data.domain !== undefined) {
updateData.domain = parsed.data.domain
? stripTrailingSlash(parsed.data.domain)
: null;
}
if (parsed.data.cors !== undefined) {
updateData.cors = parsed.data.cors.map((c) => stripTrailingSlash(c));
}
if (parsed.data.crossDomain !== undefined) {
updateData.crossDomain = parsed.data.crossDomain;
}
if (parsed.data.allowUnsafeRevenueTracking !== undefined) {
updateData.allowUnsafeRevenueTracking =
parsed.data.allowUnsafeRevenueTracking;
}
const project = await db.project.update({
where: {
id: request.params.id,
},
data: updateData,
});
// Clear cache
await Promise.all([
getProjectByIdCached.clear(project.id),
existing.clients.map((client) => {
getClientByIdCached.clear(client.id);
}),
]);
reply.send({ data: project });
}
export async function deleteProject(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply,
) {
const project = await db.project.findFirst({
where: {
id: request.params.id,
organizationId: request.client!.organizationId,
},
});
if (!project) {
throw new HttpError('Project not found', { status: 404 });
}
await db.project.update({
where: {
id: request.params.id,
},
data: {
deleteAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
},
});
await getProjectByIdCached.clear(request.params.id);
reply.send({ success: true });
}
// Clients CRUD
export async function listClients(
request: FastifyRequest<{ Querystring: { projectId?: string } }>,
reply: FastifyReply,
) {
const where: any = {
organizationId: request.client!.organizationId,
};
if (request.query.projectId) {
// Verify project belongs to organization
const project = await db.project.findFirst({
where: {
id: request.query.projectId,
organizationId: request.client!.organizationId,
},
});
if (!project) {
throw new HttpError('Project not found', { status: 404 });
}
where.projectId = request.query.projectId;
}
const clients = await db.client.findMany({
where,
orderBy: {
createdAt: 'desc',
},
});
reply.send({ data: clients });
}
export async function getClient(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply,
) {
const client = await db.client.findFirst({
where: {
id: request.params.id,
organizationId: request.client!.organizationId,
},
});
if (!client) {
throw new HttpError('Client not found', { status: 404 });
}
reply.send({ data: client });
}
export async function createClient(
request: FastifyRequest<{ Body: z.infer<typeof zCreateClient> }>,
reply: FastifyReply,
) {
const parsed = zCreateClient.safeParse(request.body);
if (parsed.success === false) {
return reply.status(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: parsed.error.errors,
});
}
const { name, projectId, type } = parsed.data;
// If projectId is provided, verify it belongs to organization
if (projectId) {
const project = await db.project.findFirst({
where: {
id: projectId,
organizationId: request.client!.organizationId,
},
});
if (!project) {
throw new HttpError('Project not found', { status: 404 });
}
}
// Generate secret
const secret = `sec_${crypto.randomBytes(10).toString('hex')}`;
const client = await db.client.create({
data: {
organizationId: request.client!.organizationId,
projectId: projectId || null,
name,
type: type || 'write',
secret: await hashPassword(secret),
},
});
await getClientByIdCached.clear(client.id);
reply.send({
data: {
...client,
secret, // Return plain secret only once
},
});
}
export async function updateClient(
request: FastifyRequest<{
Params: { id: string };
Body: z.infer<typeof zUpdateClient>;
}>,
reply: FastifyReply,
) {
const parsed = zUpdateClient.safeParse(request.body);
if (parsed.success === false) {
return reply.status(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: parsed.error.errors,
});
}
// Verify client exists and belongs to organization
const existing = await db.client.findFirst({
where: {
id: request.params.id,
organizationId: request.client!.organizationId,
},
});
if (!existing) {
throw new HttpError('Client not found', { status: 404 });
}
const updateData: any = {};
if (parsed.data.name !== undefined) {
updateData.name = parsed.data.name;
}
const client = await db.client.update({
where: {
id: request.params.id,
},
data: updateData,
});
await getClientByIdCached.clear(client.id);
reply.send({ data: client });
}
export async function deleteClient(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply,
) {
const client = await db.client.findFirst({
where: {
id: request.params.id,
organizationId: request.client!.organizationId,
},
});
if (!client) {
throw new HttpError('Client not found', { status: 404 });
}
await db.client.delete({
where: {
id: request.params.id,
},
});
await getClientByIdCached.clear(request.params.id);
reply.send({ success: true });
}
// References CRUD
export async function listReferences(
request: FastifyRequest<{ Querystring: { projectId?: string } }>,
reply: FastifyReply,
) {
const where: any = {};
if (request.query.projectId) {
// Verify project belongs to organization
const project = await db.project.findFirst({
where: {
id: request.query.projectId,
organizationId: request.client!.organizationId,
},
});
if (!project) {
throw new HttpError('Project not found', { status: 404 });
}
where.projectId = request.query.projectId;
} else {
// If no projectId, get all projects in org and filter references
const projects = await db.project.findMany({
where: {
organizationId: request.client!.organizationId,
},
select: { id: true },
});
where.projectId = {
in: projects.map((p) => p.id),
};
}
const references = await db.reference.findMany({
where,
orderBy: {
createdAt: 'desc',
},
});
reply.send({ data: references });
}
export async function getReference(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply,
) {
const reference = await db.reference.findUnique({
where: {
id: request.params.id,
},
include: {
project: {
select: {
organizationId: true,
},
},
},
});
if (!reference) {
throw new HttpError('Reference not found', { status: 404 });
}
if (reference.project.organizationId !== request.client!.organizationId) {
throw new HttpError('Reference not found', { status: 404 });
}
reply.send({ data: reference });
}
export async function createReference(
request: FastifyRequest<{ Body: z.infer<typeof zCreateReference> }>,
reply: FastifyReply,
) {
const parsed = zCreateReference.safeParse(request.body);
if (parsed.success === false) {
return reply.status(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: parsed.error.errors,
});
}
const { projectId, title, description, datetime } = parsed.data;
// Verify project belongs to organization
const project = await db.project.findFirst({
where: {
id: projectId,
organizationId: request.client!.organizationId,
},
});
if (!project) {
throw new HttpError('Project not found', { status: 404 });
}
const reference = await db.reference.create({
data: {
projectId,
title,
description: description || null,
date: new Date(datetime),
},
});
reply.send({ data: reference });
}
export async function updateReference(
request: FastifyRequest<{
Params: { id: string };
Body: z.infer<typeof zUpdateReference>;
}>,
reply: FastifyReply,
) {
const parsed = zUpdateReference.safeParse(request.body);
if (parsed.success === false) {
return reply.status(400).send({
error: 'Bad Request',
message: 'Invalid request body',
details: parsed.error.errors,
});
}
// Verify reference exists and belongs to organization
const existing = await db.reference.findUnique({
where: {
id: request.params.id,
},
include: {
project: {
select: {
organizationId: true,
},
},
},
});
if (!existing) {
throw new HttpError('Reference not found', { status: 404 });
}
if (existing.project.organizationId !== request.client!.organizationId) {
throw new HttpError('Reference not found', { status: 404 });
}
const updateData: any = {};
if (parsed.data.title !== undefined) {
updateData.title = parsed.data.title;
}
if (parsed.data.description !== undefined) {
updateData.description = parsed.data.description ?? null;
}
if (parsed.data.datetime !== undefined) {
updateData.date = new Date(parsed.data.datetime);
}
const reference = await db.reference.update({
where: {
id: request.params.id,
},
data: updateData,
});
reply.send({ data: reference });
}
export async function deleteReference(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply,
) {
const reference = await db.reference.findUnique({
where: {
id: request.params.id,
},
include: {
project: {
select: {
organizationId: true,
},
},
},
});
if (!reference) {
throw new HttpError('Reference not found', { status: 404 });
}
if (reference.project.organizationId !== request.client!.organizationId) {
throw new HttpError('Reference not found', { status: 404 });
}
await db.reference.delete({
where: {
id: request.params.id,
},
});
reply.send({ success: true });
}

View File

@@ -38,6 +38,7 @@ import exportRouter from './routes/export.router';
import importRouter from './routes/import.router';
import insightsRouter from './routes/insights.router';
import liveRouter from './routes/live.router';
import manageRouter from './routes/manage.router';
import miscRouter from './routes/misc.router';
import oauthRouter from './routes/oauth-callback.router';
import profileRouter from './routes/profile.router';
@@ -194,6 +195,7 @@ const startServer = async () => {
instance.register(importRouter, { prefix: '/import' });
instance.register(insightsRouter, { prefix: '/insights' });
instance.register(trackRouter, { prefix: '/track' });
instance.register(manageRouter, { prefix: '/manage' });
// Keep existing endpoints for backward compatibility
instance.get('/healthcheck', healthcheck);
// New Kubernetes-style health endpoints

View File

@@ -0,0 +1,132 @@
import * as controller from '@/controllers/manage.controller';
import { validateManageRequest } from '@/utils/auth';
import { activateRateLimiter } from '@/utils/rate-limiter';
import { Prisma } from '@openpanel/db';
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
const manageRouter: FastifyPluginCallback = async (fastify) => {
await activateRateLimiter({
fastify,
max: 20,
timeWindow: '10 seconds',
});
fastify.addHook('preHandler', async (req: FastifyRequest, reply) => {
try {
const client = await validateManageRequest(req.headers);
req.client = client;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
return reply.status(401).send({
error: 'Unauthorized',
message: 'Client ID seems to be malformed',
});
}
if (e instanceof Error) {
return reply
.status(401)
.send({ error: 'Unauthorized', message: e.message });
}
return reply
.status(401)
.send({ error: 'Unauthorized', message: 'Unexpected error' });
}
});
// Projects routes
fastify.route({
method: 'GET',
url: '/projects',
handler: controller.listProjects,
});
fastify.route({
method: 'GET',
url: '/projects/:id',
handler: controller.getProject,
});
fastify.route({
method: 'POST',
url: '/projects',
handler: controller.createProject,
});
fastify.route({
method: 'PATCH',
url: '/projects/:id',
handler: controller.updateProject,
});
fastify.route({
method: 'DELETE',
url: '/projects/:id',
handler: controller.deleteProject,
});
// Clients routes
fastify.route({
method: 'GET',
url: '/clients',
handler: controller.listClients,
});
fastify.route({
method: 'GET',
url: '/clients/:id',
handler: controller.getClient,
});
fastify.route({
method: 'POST',
url: '/clients',
handler: controller.createClient,
});
fastify.route({
method: 'PATCH',
url: '/clients/:id',
handler: controller.updateClient,
});
fastify.route({
method: 'DELETE',
url: '/clients/:id',
handler: controller.deleteClient,
});
// References routes
fastify.route({
method: 'GET',
url: '/references',
handler: controller.listReferences,
});
fastify.route({
method: 'GET',
url: '/references/:id',
handler: controller.getReference,
});
fastify.route({
method: 'POST',
url: '/references',
handler: controller.createReference,
});
fastify.route({
method: 'PATCH',
url: '/references/:id',
handler: controller.updateReference,
});
fastify.route({
method: 'DELETE',
url: '/references/:id',
handler: controller.deleteReference,
});
};
export default manageRouter;

View File

@@ -236,3 +236,40 @@ export async function validateImportRequest(
return client;
}
export async function validateManageRequest(
headers: RawRequestDefaultExpression['headers'],
): Promise<IServiceClientWithProject> {
const clientId = headers['openpanel-client-id'] as string;
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
if (
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
clientId,
)
) {
throw new Error('Manage: Client ID must be a valid UUIDv4');
}
const client = await getClientByIdCached(clientId);
if (!client) {
throw new Error('Manage: Invalid client id');
}
if (!client.secret) {
throw new Error('Manage: Client has no secret');
}
if (client.type !== ClientType.root) {
throw new Error(
'Manage: Only root clients are allowed to manage resources',
);
}
if (!(await verifyPassword(clientSecret, client.secret))) {
throw new Error('Manage: Invalid client secret');
}
return client;
}