feat: Add browser documentation
This commit is contained in:
@@ -6,6 +6,7 @@ import { issueToken, consumeToken, revokeToken, IssuedToken } from './auth-store
|
||||
import { buildOpenApiDocument } from './openapi';
|
||||
import { HttpError, RequestContext, readJsonBody } from './http-helpers';
|
||||
import { getDocsHtml, getScalarApiReferenceBundlePath } from './docs-html';
|
||||
import { resolveDocusaurusRoute } from './docusaurus-static';
|
||||
import { LocalApiSettings } from '../desktop-settings';
|
||||
|
||||
export interface RouteResponse {
|
||||
@@ -20,7 +21,7 @@ export interface RouteContext {
|
||||
request: RequestContext;
|
||||
settings: LocalApiSettings;
|
||||
baseUrl: string;
|
||||
dataSource: DataSource;
|
||||
dataSource: DataSource | null;
|
||||
bodyBuffer: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ interface RouteMatch {
|
||||
handler: RouteHandler;
|
||||
params: Record<string, string>;
|
||||
requiresAuth: boolean;
|
||||
requiresDatabase: boolean;
|
||||
}
|
||||
|
||||
interface RouteDefinition {
|
||||
@@ -38,6 +40,7 @@ interface RouteDefinition {
|
||||
paramKeys: string[];
|
||||
handler: RouteHandler;
|
||||
requiresAuth: boolean;
|
||||
requiresDatabase: boolean;
|
||||
}
|
||||
|
||||
function compilePattern(template: string): { pattern: RegExp; paramKeys: string[] } {
|
||||
@@ -56,10 +59,10 @@ function compilePattern(template: string): { pattern: RegExp; paramKeys: string[
|
||||
return { pattern: new RegExp(`^${source}$`), paramKeys };
|
||||
}
|
||||
|
||||
function defineRoute(method: string, template: string, handler: RouteHandler, requiresAuth: boolean): RouteDefinition {
|
||||
function defineRoute(method: string, template: string, handler: RouteHandler, requiresAuth: boolean, requiresDatabase = true): RouteDefinition {
|
||||
const compiled = compilePattern(template);
|
||||
|
||||
return { method: method.toUpperCase(), pattern: compiled.pattern, paramKeys: compiled.paramKeys, handler, requiresAuth };
|
||||
return { method: method.toUpperCase(), pattern: compiled.pattern, paramKeys: compiled.paramKeys, handler, requiresAuth, requiresDatabase };
|
||||
}
|
||||
|
||||
function runQuery<T>(dataSource: DataSource, query: Query): Promise<T> {
|
||||
@@ -73,6 +76,14 @@ function runQuery<T>(dataSource: DataSource, query: Query): Promise<T> {
|
||||
return handler(query) as Promise<T>;
|
||||
}
|
||||
|
||||
function requireDataSource(dataSource: DataSource | null): DataSource {
|
||||
if (!dataSource) {
|
||||
throw new HttpError(503, 'Database not initialised', 'DB_UNAVAILABLE');
|
||||
}
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
function clampInt(value: unknown, min: number, max: number, fallback: number): number {
|
||||
const parsed = typeof value === 'string' ? Number(value) : NaN;
|
||||
|
||||
@@ -86,12 +97,12 @@ const ROUTES: RouteDefinition[] = [
|
||||
defineRoute('GET', '/api/health', async (ctx): Promise<RouteResponse> => ({
|
||||
status: 200,
|
||||
body: { status: 'ok', version: app.getVersion(), timestamp: Date.now(), exposeOnLan: ctx.settings.exposeOnLan }
|
||||
}), false),
|
||||
}), false, false),
|
||||
|
||||
defineRoute('GET', '/api/openapi.json', async (ctx): Promise<RouteResponse> => ({
|
||||
status: 200,
|
||||
body: buildOpenApiDocument({ baseUrl: ctx.baseUrl, appVersion: app.getVersion() })
|
||||
}), false),
|
||||
}), false, false),
|
||||
|
||||
defineRoute('GET', '/docs', async (ctx): Promise<RouteResponse> => {
|
||||
if (!ctx.settings.scalarEnabled) {
|
||||
@@ -109,7 +120,27 @@ const ROUTES: RouteDefinition[] = [
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
rawBody: getDocsHtml(`${ctx.baseUrl}/api/openapi.json`)
|
||||
};
|
||||
}, false),
|
||||
}, false, false),
|
||||
|
||||
defineRoute('GET', '/docusaurus(?:/.*)?', async (ctx): Promise<RouteResponse> => {
|
||||
if (!ctx.settings.docusaurusEnabled) {
|
||||
return {
|
||||
status: 404,
|
||||
body: null,
|
||||
contentType: 'text/plain; charset=utf-8',
|
||||
rawBody: 'Docusaurus documentation is disabled. Open documentation from the desktop app to activate it.'
|
||||
};
|
||||
}
|
||||
|
||||
const docsRoute = await resolveDocusaurusRoute(ctx.request.pathname);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: null,
|
||||
contentType: docsRoute.contentType,
|
||||
filePath: docsRoute.filePath
|
||||
};
|
||||
}, false, false),
|
||||
|
||||
defineRoute('GET', '/scalar/api-reference.js', async (ctx): Promise<RouteResponse> => {
|
||||
if (!ctx.settings.scalarEnabled) {
|
||||
@@ -133,7 +164,7 @@ const ROUTES: RouteDefinition[] = [
|
||||
contentType: 'application/javascript; charset=utf-8',
|
||||
filePath: bundlePath
|
||||
};
|
||||
}, false),
|
||||
}, false, false),
|
||||
|
||||
defineRoute('POST', '/api/auth/login', async (ctx): Promise<RouteResponse> => {
|
||||
const body = await ctx.bodyBuffer() as { username?: unknown; password?: unknown; serverUrl?: unknown };
|
||||
@@ -213,7 +244,7 @@ const ROUTES: RouteDefinition[] = [
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/profile', async (ctx): Promise<RouteResponse> => {
|
||||
const user = await runQuery<unknown>(ctx.dataSource, {
|
||||
const user = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetCurrentUser,
|
||||
payload: {}
|
||||
});
|
||||
@@ -226,7 +257,7 @@ const ROUTES: RouteDefinition[] = [
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/rooms', async (ctx): Promise<RouteResponse> => {
|
||||
const rooms = await runQuery<unknown[]>(ctx.dataSource, {
|
||||
const rooms = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetAllRooms,
|
||||
payload: {}
|
||||
});
|
||||
@@ -243,7 +274,7 @@ const ROUTES: RouteDefinition[] = [
|
||||
const limit = clampInt(ctx.request.url.searchParams.get('limit'), 1, 500, 100);
|
||||
const offset = clampInt(ctx.request.url.searchParams.get('offset'), 0, Number.MAX_SAFE_INTEGER, 0);
|
||||
|
||||
const messages = await runQuery<unknown[]>(ctx.dataSource, {
|
||||
const messages = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetMessages,
|
||||
payload: { roomId: decodeURIComponent(roomId), limit, offset }
|
||||
});
|
||||
@@ -278,7 +309,7 @@ export function matchRoute(method: string, pathname: string): RoutingResult {
|
||||
}
|
||||
|
||||
return {
|
||||
match: { handler: route.handler, params, requiresAuth: route.requiresAuth },
|
||||
match: { handler: route.handler, params, requiresAuth: route.requiresAuth, requiresDatabase: route.requiresDatabase },
|
||||
methodNotAllowed: false
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user