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

View file

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