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 948 additions and 222 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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');
}
}
}
}