feat: add initial API

This commit is contained in:
Morten Olsen
2025-10-16 20:54:31 +02:00
parent 5cf0a3612a
commit 11828da073
17 changed files with 647 additions and 89 deletions

View File

@@ -1,8 +1,34 @@
import { type FastifyPluginAsync } from 'fastify';
import { manageEndpoints } from './endpoints/endpoints.manage.ts';
import { authPlugin } from './plugins/plugins.auth.ts';
import { messageEndpoints } from './endpoints/endpoints.message.ts';
import { z } from 'zod';
const api: FastifyPluginAsync = async (fastify) => {
fastify.get('/healthz', () => {
return { status: 'ok' };
fastify.route({
method: 'get',
url: '/health',
schema: {
operationId: 'health.get',
summary: 'Get health status',
tags: ['system'],
response: {
200: z.object({
status: z.literal('ok'),
}),
},
},
handler: () => {
return { status: 'ok' };
},
});
await authPlugin(fastify, {});
await fastify.register(manageEndpoints, {
prefix: '/manage',
});
await fastify.register(messageEndpoints, {
prefix: '/message',
});
};

View File

@@ -0,0 +1,45 @@
import { JwtAuth } from '#root/auth/auth.jwt.ts';
import { statementSchema } from '#root/auth/auth.schemas.ts';
import { Config } from '#root/config/config.ts';
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
import { z } from 'zod';
const manageEndpoints: FastifyPluginAsyncZod = async (fastify) => {
const config = fastify.services.get(Config);
if (config.jwtSecret) {
fastify.route({
method: 'post',
url: '/jwt',
schema: {
operationId: 'manage.jwt.post',
summary: 'Generate a JWT',
tags: ['manage'],
body: z.object({
exp: z.number().optional(),
statements: z.array(statementSchema),
}),
response: {
200: z.object({
jwt: z.string(),
}),
},
},
handler: async (req, reply) => {
if (
!req.session.validate({
action: 'mgmt:generate-jwt',
resource: 'mgmt/',
})
) {
throw reply.unauthorized('not allowed');
}
const jwtAuth = fastify.services.get(JwtAuth);
const jwt = jwtAuth.generate(req.body);
reply.send({ jwt });
},
});
}
};
export { manageEndpoints };

View File

@@ -0,0 +1,62 @@
import { Config } from '#root/config/config.ts';
import { MqttServer } from '#root/server/server.ts';
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
import { z } from 'zod';
const messageEndpoints: FastifyPluginAsyncZod = async (fastify) => {
const config = fastify.services.get(Config);
if (config.jwtSecret) {
fastify.route({
method: 'post',
url: '',
schema: {
summary: 'Post a message to the bus',
operationId: 'message.post',
tags: ['message'],
body: z.object({
topic: z.string(),
dup: z.boolean(),
qos: z.union([z.literal(0), z.literal(1), z.literal(2)]),
retain: z.boolean(),
payload: z.string(),
}),
response: {
200: z.object({
success: z.literal(true),
}),
},
},
handler: async (req, reply) => {
if (
!req.session.validate({
action: 'mqtt:publish',
resource: 'mgmt:',
})
) {
throw reply.unauthorized('not allowed');
}
const server = fastify.services.get(MqttServer);
await new Promise<void>((resolve, reject) => {
server.bus.publish(
{
...req.body,
cmd: 'publish',
payload: Buffer.from(req.body.payload, 'base64'),
},
(err) => {
if (err) {
return reject(err);
}
resolve();
},
);
});
reply.send({ success: true });
},
});
}
};
export { messageEndpoints };

14
src/api/extensions.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import type { Session } from '#root/services/sessions/sessions.session.ts';
import type { Services } from '#root/utils/services.ts';
import 'fastify';
declare module 'fastify' {
// eslint-disable-next-line
export interface FastifyInstance {
services: Services;
}
// eslint-disable-next-line
export interface FastifyRequest {
session: Session;
}
}

View File

@@ -0,0 +1,27 @@
import { SessionProvider } from '#root/services/sessions/sessions.provider.ts';
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
const authPlugin: FastifyPluginAsyncZod = async (fastify) => {
fastify.addHook('onRequest', async (req, reply) => {
const authProvider = req.headers['x-auth-provider'];
if (!authProvider || Array.isArray(authProvider)) {
throw reply.unauthorized('missing x-auth-provider header');
}
const authorization = req.headers.authorization;
if (!authorization) {
throw reply.unauthorized('missing authorization header');
}
const [type, token] = authorization.split(' ');
if (type.toLowerCase() !== 'bearer') {
throw reply.unauthorized('only bearer tokens are allowed');
}
if (!token) {
throw reply.unauthorized('missing token');
}
const sessionProvider = fastify.services.get(SessionProvider);
const session = await sessionProvider.get(authProvider, token);
req.session = session;
});
};
export { authPlugin };