feat: better endpoint descriptions (#20439)

This commit is contained in:
Jason Rasmussen 2025-07-30 12:29:36 -04:00 committed by GitHub
parent d5a01c0310
commit 749f999f2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1918 additions and 428 deletions

View file

@ -413,6 +413,11 @@ export enum LogLevel {
Fatal = 'fatal',
}
export enum ApiCustomExtension {
Permission = 'x-immich-permission',
AdminOnly = 'x-immich-admin-only',
}
export enum MetadataKey {
AuthRoute = 'auth_route',
AdminRoute = 'admin_route',

View file

@ -10,7 +10,7 @@ import { Reflector } from '@nestjs/core';
import { ApiBearerAuth, ApiCookieAuth, ApiExtension, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
import { Request } from 'express';
import { AuthDto } from 'src/dtos/auth.dto';
import { ImmichQuery, MetadataKey, Permission } from 'src/enum';
import { ApiCustomExtension, ImmichQuery, MetadataKey, Permission } from 'src/enum';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { UAParser } from 'ua-parser-js';
@ -19,16 +19,20 @@ type AdminRoute = { admin?: true };
type SharedLinkRoute = { sharedLink?: true };
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => {
export const Authenticated = (options: AuthenticatedOptions = {}): MethodDecorator => {
const decorators: MethodDecorator[] = [
ApiBearerAuth(),
ApiCookieAuth(),
ApiSecurity(MetadataKey.ApiKeySecurity),
SetMetadata(MetadataKey.AuthRoute, options || {}),
SetMetadata(MetadataKey.AuthRoute, options),
];
if ((options as AdminRoute).admin) {
decorators.push(ApiExtension(ApiCustomExtension.AdminOnly, true));
}
if (options?.permission) {
decorators.push(ApiExtension('x-immich-permission', options.permission));
decorators.push(ApiExtension(ApiCustomExtension.Permission, options.permission ?? Permission.All));
}
if ((options as SharedLinkRoute)?.sharedLink) {

View file

@ -6,7 +6,11 @@ import {
SwaggerDocumentOptions,
SwaggerModule,
} from '@nestjs/swagger';
import { ReferenceObject, SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
import {
OperationObject,
ReferenceObject,
SchemaObject,
} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
import _ from 'lodash';
import { writeFileSync } from 'node:fs';
import path from 'node:path';
@ -15,7 +19,7 @@ import parse from 'picomatch/lib/parse';
import { SystemConfig } from 'src/config';
import { CLIP_MODEL_INFO, serverVersion } from 'src/constants';
import { extraSyncModels } from 'src/dtos/sync.dto';
import { ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
import { LoggingRepository } from 'src/repositories/logging.repository';
export class ImmichStartupError extends Error {}
@ -198,7 +202,12 @@ const patchOpenAPI = (document: OpenAPIObject) => {
trace: path.trace,
};
for (const operation of Object.values(operations)) {
for (const operation of Object.values(operations) as Array<
OperationObject & {
[ApiCustomExtension.AdminOnly]?: boolean;
[ApiCustomExtension.Permission]?: string;
}
>) {
if (!operation) {
continue;
}
@ -211,12 +220,21 @@ const patchOpenAPI = (document: OpenAPIObject) => {
// console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`);
}
if (operation.description === '') {
delete operation.description;
}
const adminOnly = operation[ApiCustomExtension.AdminOnly] ?? false;
const permission = operation[ApiCustomExtension.Permission];
if (permission) {
let description = (operation.description || '').trim();
if (description && !description.endsWith('.')) {
description += '. ';
}
if (operation.parameters) {
operation.parameters = _.orderBy(operation.parameters, 'name');
operation.description =
description +
`This endpoint ${adminOnly ? 'is an admin-only route, and ' : ''}requires the \`${permission}\` permission.`;
if (operation.parameters) {
operation.parameters = _.orderBy(operation.parameters, 'name');
}
}
}
}