feat(openapi): Started working on Openapi generation

- Updated to Typebox 1.0.0 to better support Openapi type generation
- Changed dockerfile to fetch depedencies only once
- Fixed Routes to properly handle openapi
- Fixed Routes to respond with multiples status code (no more only 200)
- Fixed Schemas so the auth-gated endpoint properly reflect that
- Added Makefile rule to generate openapi client (none working due to
  missing files)
This commit is contained in:
Maieul BOYER 2025-11-09 02:44:18 +01:00 committed by Maix0
parent 1bd2b4594b
commit b7c2a3dff9
36 changed files with 5472 additions and 833 deletions

178
src/user/openapi.json Normal file
View file

@ -0,0 +1,178 @@
{
"openapi": "3.1.0",
"info": {
"version": "9.6.0",
"title": "@fastify/swagger"
},
"components": {
"schemas": {}
},
"paths": {
"/api/user/info/{user}": {
"get": {
"operationId": "getUser",
"parameters": [
{
"schema": {
"anyOf": [
{
"enum": [
"me"
],
"description": "the current logged in user"
},
{
"type": "string",
"format": "uuid",
"description": "A user uuid"
}
]
},
"in": "path",
"name": "user",
"required": true
}
],
"responses": {
"200": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg",
"payload"
],
"properties": {
"kind": {
"enum": [
"success"
]
},
"msg": {
"enum": [
"userinfo.success"
]
},
"payload": {
"type": "object",
"required": [
"name",
"id",
"guest"
],
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string"
},
"guest": {
"type": "boolean"
}
}
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"notLoggedIn"
]
},
"msg": {
"enum": [
"auth.noCookie",
"auth.invalidKind",
"auth.noUser",
"auth.invalid"
]
}
}
}
}
}
},
"403": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"userinfo.failure.notLoggedIn"
]
}
}
}
}
}
},
"404": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"userinfo.failure.unknownUser"
]
}
}
}
}
}
}
}
}
}
},
"servers": [
{
"url": "https://local.maix.me:8888",
"description": "direct from docker"
},
{
"url": "https://local.maix.me:8000",
"description": "using fnginx"
}
]
}

View file

@ -11,7 +11,8 @@
"scripts": {
"start": "npm run build && node dist/run.js",
"build": "vite build",
"build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false"
"build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false",
"build:openapi": "VITE_ENTRYPOINT=src/openapi.ts vite build && node dist/openapi.cjs >openapi.json"
},
"keywords": [],
"author": "",
@ -22,15 +23,15 @@
"@fastify/multipart": "^9.3.0",
"@fastify/sensible": "^6.0.3",
"@fastify/static": "^8.3.0",
"@sinclair/typebox": "^0.34.41",
"typebox": "^1.0.51",
"fastify": "^5.6.1",
"fastify-cli": "^7.4.0",
"fastify-cli": "^7.4.1",
"fastify-plugin": "^5.1.0"
},
"devDependencies": {
"@types/node": "^22.18.13",
"rollup-plugin-node-externals": "^8.1.1",
"vite": "^7.1.12",
"@types/node": "^22.19.0",
"rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.2",
"vite-tsconfig-paths": "^5.1.4"
}
}

View file

@ -3,21 +3,20 @@ import fastifyFormBody from '@fastify/formbody';
import fastifyMultipart from '@fastify/multipart';
import * as db from '@shared/database';
import * as auth from '@shared/auth';
import * as swagger from '@shared/swagger';
import * as utils from '@shared/utils';
declare const __SERVICE_NAME: string;
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
// When using .decorate you have to specify added properties for Typescript
declare module 'fastify' {
export interface FastifyInstance {
image_store: string;
}
}
const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
void opts;
await fastify.register(utils.useMakeResponse);
await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME });
await fastify.register(db.useDatabase as FastifyPluginAsync, {});
await fastify.register(auth.jwtPlugin as FastifyPluginAsync, {});
await fastify.register(auth.authPlugin as FastifyPluginAsync, {});

21
src/user/src/openapi.ts Normal file
View file

@ -0,0 +1,21 @@
import f, { FastifyPluginAsync } from 'fastify';
import * as swagger from '@shared/swagger';
import * as auth from '@shared/auth';
declare const __SERVICE_NAME: string;
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
async function start() {
const fastify = f({ logger: false });
await fastify.register(auth.authPlugin, { onlySchema: true });
await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME });
for (const route of Object.values(routes)) {
await fastify.register(route as FastifyPluginAsync, {});
}
await fastify.ready();
console.log(JSON.stringify(fastify.swagger(), undefined, 4));
}
start();

View file

@ -1,26 +1,37 @@
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from '@sinclair/typebox';
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
import { Static, Type } from 'typebox';
import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
export const UserInfoRes = Type.Union([
typeResponse('success', 'userinfo.success', { name: Type.String(), id: Type.String(), guest: Type.Boolean() }),
typeResponse('failure', ['userinfo.failure.generic', 'userinfo.failure.unknownUser', 'userinfo.failure.notLoggedIn']),
]);
export const UserInfoRes = {
'200': typeResponse('success', 'userinfo.success', {
name: Type.String(), id: Type.String(), guest: Type.Boolean(),
}),
'403': typeResponse('failure', 'userinfo.failure.notLoggedIn'),
'404': typeResponse('failure', 'userinfo.failure.unknownUser'),
};
export type UserInfoRes = Static<typeof UserInfoRes>;
export type UserInfoRes = MakeStaticResponse<typeof UserInfoRes>;
export const UserInfoParams = Type.Object({
user: Type.Union([
Type.Enum(['me'], { description: 'the current logged in user' }),
Type.String({ format: 'uuid', description: 'A user uuid' }),
]),
});
export type UserInfoParams = Static<typeof UserInfoParams>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.get<{ Params: { user: string } }>(
fastify.get<{ Params: UserInfoParams }>(
'/api/user/info/:user',
{ schema: { response: { '2xx': UserInfoRes } }, config: { requireAuth: true } },
async function(req, _res) {
void _res;
if (isNullish(req.authUser)) { return makeResponse('failure', 'userinfo.failure.notLoggedIn'); }
{ schema: { params: UserInfoParams, response: UserInfoRes, operationId: 'getUser' }, config: { requireAuth: true } },
async function(req, res) {
if (isNullish(req.authUser)) { return res.makeResponse(403, 'failure', 'userinfo.failure.notLoggedIn'); }
if (isNullish(req.params.user) || req.params.user.length === 0) {
return makeResponse('failure', 'userinfo.failure.unknownUser');
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
}
// if the param is the special value `me`, then just get the id from the currently auth'ed user
@ -30,7 +41,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
const user = this.db.getUser(req.params.user);
if (isNullish(user)) {
return makeResponse('failure', 'userinfo.failure.unknownUser');
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
}
@ -48,7 +59,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
guest: !!user.guest,
};
return makeResponse('success', 'userinfo.success', payload);
return res.makeResponse(200, 'success', 'userinfo.success', payload);
},
);
};

View file

@ -26,6 +26,9 @@ const externals = collectDeps(
export default defineConfig({
root: __dirname,
define: {
__SERVICE_NAME: '"user"',
},
// service root
plugins: [tsconfigPaths(), nodeExternals()],
build: {
@ -33,7 +36,7 @@ export default defineConfig({
outDir: 'dist',
emptyOutDir: true,
lib: {
entry: path.resolve(__dirname, 'src/run.ts'),
entry: path.resolve(__dirname, process.env.VITE_ENTRYPOINT ?? 'src/run.ts'),
// adjust main entry
formats: ['cjs'],
// CommonJS for Node.js