feat: endpoint descriptions (#23813)

This commit is contained in:
Jason Rasmussen 2025-11-11 17:01:14 -05:00 committed by GitHub
parent 896665bca9
commit edde0f93ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 2138 additions and 763 deletions

View file

@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
final folderApiRepositoryProvider = Provider((ref) => FolderApiRepository(ref.watch(apiServiceProvider).viewApi)); final folderApiRepositoryProvider = Provider((ref) => FolderApiRepository(ref.watch(apiServiceProvider).viewApi));
class FolderApiRepository extends ApiRepository { class FolderApiRepository extends ApiRepository {
final ViewApi _api; final ViewsApi _api;
final Logger _log = Logger("FolderApiRepository"); final Logger _log = Logger("FolderApiRepository");
FolderApiRepository(this._api); FolderApiRepository(this._api);

View file

@ -17,7 +17,7 @@ class ApiService implements Authentication {
late UsersApi usersApi; late UsersApi usersApi;
late AuthenticationApi authenticationApi; late AuthenticationApi authenticationApi;
late OAuthApi oAuthApi; late AuthenticationApi oAuthApi;
late AlbumsApi albumsApi; late AlbumsApi albumsApi;
late AssetsApi assetsApi; late AssetsApi assetsApi;
late SearchApi searchApi; late SearchApi searchApi;
@ -32,7 +32,7 @@ class ApiService implements Authentication {
late DownloadApi downloadApi; late DownloadApi downloadApi;
late TrashApi trashApi; late TrashApi trashApi;
late StacksApi stacksApi; late StacksApi stacksApi;
late ViewApi viewApi; late ViewsApi viewApi;
late MemoriesApi memoriesApi; late MemoriesApi memoriesApi;
late SessionsApi sessionsApi; late SessionsApi sessionsApi;
@ -56,7 +56,7 @@ class ApiService implements Authentication {
} }
usersApi = UsersApi(_apiClient); usersApi = UsersApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient);
oAuthApi = OAuthApi(_apiClient); oAuthApi = AuthenticationApi(_apiClient);
albumsApi = AlbumsApi(_apiClient); albumsApi = AlbumsApi(_apiClient);
assetsApi = AssetsApi(_apiClient); assetsApi = AssetsApi(_apiClient);
serverInfoApi = ServerApi(_apiClient); serverInfoApi = ServerApi(_apiClient);
@ -71,7 +71,7 @@ class ApiService implements Authentication {
downloadApi = DownloadApi(_apiClient); downloadApi = DownloadApi(_apiClient);
trashApi = TrashApi(_apiClient); trashApi = TrashApi(_apiClient);
stacksApi = StacksApi(_apiClient); stacksApi = StacksApi(_apiClient);
viewApi = ViewApi(_apiClient); viewApi = ViewsApi(_apiClient);
memoriesApi = MemoriesApi(_apiClient); memoriesApi = MemoriesApi(_apiClient);
sessionsApi = SessionsApi(_apiClient); sessionsApi = SessionsApi(_apiClient);
} }

BIN
mobile/openapi/README.md generated

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.

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

@ -2,7 +2,7 @@ import { Duration } from 'luxon';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { SemVer } from 'semver'; import { SemVer } from 'semver';
import { DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum'; import { ApiTag, DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum';
export const POSTGRES_VERSION_RANGE = '>=14.0.0'; export const POSTGRES_VERSION_RANGE = '>=14.0.0';
export const VECTORCHORD_VERSION_RANGE = '>=0.3 <0.6'; export const VECTORCHORD_VERSION_RANGE = '>=0.3 <0.6';
@ -138,3 +138,56 @@ export const ORIENTATION_TO_SHARP_ROTATION: Record<ExifOrientation, SharpRotatio
[ExifOrientation.MirrorHorizontalRotate90CW]: { angle: 90, flip: true }, [ExifOrientation.MirrorHorizontalRotate90CW]: { angle: 90, flip: true },
[ExifOrientation.Rotate270CW]: { angle: 270 }, [ExifOrientation.Rotate270CW]: { angle: 270 },
} as const; } as const;
export const endpointTags: Record<ApiTag, string> = {
[ApiTag.Activities]: 'An activity is a like or a comment made by a user on an asset or album.',
[ApiTag.Albums]: 'An album is a collection of assets that can be shared with other users or via shared links.',
[ApiTag.ApiKeys]: 'An api key can be used to programmatically access the Immich API.',
[ApiTag.Assets]: 'An asset is an image or video that has been uploaded to Immich.',
[ApiTag.Authentication]: 'Endpoints related to user authentication, including OAuth.',
[ApiTag.AuthenticationAdmin]: 'Administrative endpoints related to authentication.',
[ApiTag.Deprecated]: 'Deprecated endpoints that are planned for removal in the next major release.',
[ApiTag.Download]: 'Endpoints for downloading assets or collections of assets.',
[ApiTag.Duplicates]: 'Endpoints for managing and identifying duplicate assets.',
[ApiTag.Faces]:
'A face is a detected human face within an asset, which can be associated with a person. Faces are normally detected via machine learning, but can also be created via manually.',
[ApiTag.Jobs]:
'Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed.',
[ApiTag.Libraries]:
'An external library is made up of input file paths or expressions that are scanned for asset files. Discovered files are automatically imported. Assets much be unique within a library, but can be duplicated across libraries. Each user has a default upload library, and can have one or more external libraries.',
[ApiTag.Map]:
'Map endpoints include supplemental functionality related to geolocation, such as reverse geocoding and retrieving map markers for assets with geolocation data.',
[ApiTag.Memories]:
'A memory is a specialized collection of assets with dedicated viewing implementations in the web and mobile clients. A memory includes fields related to visibility and are automatically generated per user via a background job.',
[ApiTag.Notifications]:
'A notification is a specialized message sent to users to inform them of important events. Currently, these notifications are only shown in the Immich web application.',
[ApiTag.NotificationsAdmin]: 'Notification administrative endpoints.',
[ApiTag.Partners]: 'A partner is a link with another user that allows sharing of assets between two users.',
[ApiTag.People]:
'A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job.',
[ApiTag.Search]:
'Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting.',
[ApiTag.Server]:
'Information about the current server deployment, including version and build information, available features, supported media types, and more.',
[ApiTag.Sessions]:
'A session represents an authenticated login session for a user. Sessions also appear in the web application as "Authorized devices".',
[ApiTag.SharedLinks]:
'A shared link is a public url that provides access to a specific album, asset, or collection of assets. A shared link can be protected with a password, include a specific slug, allow or disallow downloads, and optionally include an expiration date.',
[ApiTag.Stacks]:
'A stack is a group of related assets. One asset is the "primary" asset, and the rest are "child" assets. On the main timeline, stack parents are included by default, while child assets are hidden.',
[ApiTag.Sync]: 'A collection of endpoints for the new mobile synchronization implementation.',
[ApiTag.SystemConfig]: 'Endpoints to view, modify, and validate the system configuration settings.',
[ApiTag.SystemMetadata]:
'Endpoints to view, modify, and validate the system metadata, which includes information about things like admin onboarding status.',
[ApiTag.Tags]:
'A tag is a user-defined label that can be applied to assets for organizational purposes. Tags can also be hierarchical, allowing for parent-child relationships between tags.',
[ApiTag.Timeline]:
'Specialized endpoints related to the timeline implementation used in the web application. External applications or tools should not use or rely on these endpoints, as they are subject to change without notice.',
[ApiTag.Trash]:
'Endpoints for managing the trash can, which includes assets that have been discarded. Items in the trash are automatically deleted after a configured amount of time.',
[ApiTag.UsersAdmin]:
'Administrative endpoints for managing users, including creating, updating, deleting, and restoring users. Also includes endpoints for resetting passwords and PIN codes.',
[ApiTag.Users]:
'Endpoints for viewing and updating the current users, including product key information, profile picture data, onboarding progress, and more.',
[ApiTag.Views]: 'Endpoints for specialized views, such as the folder view.',
};

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Response } from 'express'; import { Response } from 'express';
import { import {
ActivityCreateDto, ActivityCreateDto,
@ -9,24 +9,33 @@ import {
ActivityStatisticsResponseDto, ActivityStatisticsResponseDto,
} from 'src/dtos/activity.dto'; } from 'src/dtos/activity.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { ActivityService } from 'src/services/activity.service'; import { ActivityService } from 'src/services/activity.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Activities') @ApiTags(ApiTag.Activities)
@Controller('activities') @Controller('activities')
export class ActivityController { export class ActivityController {
constructor(private service: ActivityService) {} constructor(private service: ActivityService) {}
@Get() @Get()
@Authenticated({ permission: Permission.ActivityRead }) @Authenticated({ permission: Permission.ActivityRead })
@ApiOperation({
summary: 'List all activities',
description:
'Returns a list of activities for the selected asset or album. The activities are returned in sorted order, with the oldest activities appearing first.',
})
getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
return this.service.getAll(auth, dto); return this.service.getAll(auth, dto);
} }
@Post() @Post()
@Authenticated({ permission: Permission.ActivityCreate }) @Authenticated({ permission: Permission.ActivityCreate })
@ApiOperation({
summary: 'Create an activity',
description: 'Create a like or a comment for an album, or an asset in an album.',
})
async createActivity( async createActivity(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: ActivityCreateDto, @Body() dto: ActivityCreateDto,
@ -41,6 +50,10 @@ export class ActivityController {
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.ActivityStatistics }) @Authenticated({ permission: Permission.ActivityStatistics })
@ApiOperation({
summary: 'Retrieve activity statistics',
description: 'Returns the number of likes and comments for a given album or asset in an album.',
})
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
return this.service.getStatistics(auth, dto); return this.service.getStatistics(auth, dto);
} }
@ -48,6 +61,10 @@ export class ActivityController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.ActivityDelete }) @Authenticated({ permission: Permission.ActivityDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete an activity',
description: 'Removes a like or comment from a given album or asset in an album.',
})
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { import {
AddUsersDto, AddUsersDto,
AlbumInfoDto, AlbumInfoDto,
@ -14,36 +14,52 @@ import {
} from 'src/dtos/album.dto'; } from 'src/dtos/album.dto';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AlbumService } from 'src/services/album.service'; import { AlbumService } from 'src/services/album.service';
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
@ApiTags('Albums') @ApiTags(ApiTag.Albums)
@Controller('albums') @Controller('albums')
export class AlbumController { export class AlbumController {
constructor(private service: AlbumService) {} constructor(private service: AlbumService) {}
@Get() @Get()
@Authenticated({ permission: Permission.AlbumRead }) @Authenticated({ permission: Permission.AlbumRead })
@ApiOperation({
summary: 'List all albums',
description: 'Retrieve a list of albums available to the authenticated user.',
})
getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> { getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(auth, query); return this.service.getAll(auth, query);
} }
@Post() @Post()
@Authenticated({ permission: Permission.AlbumCreate }) @Authenticated({ permission: Permission.AlbumCreate })
@ApiOperation({
summary: 'Create an album',
description: 'Create a new album. The album can also be created with initial users and assets.',
})
createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> { createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.AlbumStatistics }) @Authenticated({ permission: Permission.AlbumStatistics })
@ApiOperation({
summary: 'Retrieve album statistics',
description: 'Returns statistics about the albums available to the authenticated user.',
})
getAlbumStatistics(@Auth() auth: AuthDto): Promise<AlbumStatisticsResponseDto> { getAlbumStatistics(@Auth() auth: AuthDto): Promise<AlbumStatisticsResponseDto> {
return this.service.getStatistics(auth); return this.service.getStatistics(auth);
} }
@Authenticated({ permission: Permission.AlbumRead, sharedLink: true }) @Authenticated({ permission: Permission.AlbumRead, sharedLink: true })
@Get(':id') @Get(':id')
@ApiOperation({
summary: 'Retrieve an album',
description: 'Retrieve information about a specific album by its ID.',
})
getAlbumInfo( getAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -54,6 +70,11 @@ export class AlbumController {
@Patch(':id') @Patch(':id')
@Authenticated({ permission: Permission.AlbumUpdate }) @Authenticated({ permission: Permission.AlbumUpdate })
@ApiOperation({
summary: 'Update an album',
description:
'Update the information of a specific album by its ID. This endpoint can be used to update the album name, description, sort order, etc. However, it is not used to add or remove assets or users from the album.',
})
updateAlbumInfo( updateAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -65,12 +86,21 @@ export class AlbumController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.AlbumDelete }) @Authenticated({ permission: Permission.AlbumDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete an album',
description:
'Delete a specific album by its ID. Note the album is initially trashed and then immediately scheduled for deletion, but relies on a background job to complete the process.',
})
deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
@Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true })
@ApiOperation({
summary: 'Add assets to an album',
description: 'Add multiple assets to a specific album by its ID.',
})
addAssetsToAlbum( addAssetsToAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -81,12 +111,20 @@ export class AlbumController {
@Put('assets') @Put('assets')
@Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true }) @Authenticated({ permission: Permission.AlbumAssetCreate, sharedLink: true })
@ApiOperation({
summary: 'Add assets to albums',
description: 'Send a list of asset IDs and album IDs to add each asset to each album.',
})
addAssetsToAlbums(@Auth() auth: AuthDto, @Body() dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> { addAssetsToAlbums(@Auth() auth: AuthDto, @Body() dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> {
return this.service.addAssetsToAlbums(auth, dto); return this.service.addAssetsToAlbums(auth, dto);
} }
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated({ permission: Permission.AlbumAssetDelete }) @Authenticated({ permission: Permission.AlbumAssetDelete })
@ApiOperation({
summary: 'Remove assets from an album',
description: 'Remove multiple assets from a specific album by its ID.',
})
removeAssetFromAlbum( removeAssetFromAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: BulkIdsDto, @Body() dto: BulkIdsDto,
@ -97,6 +135,10 @@ export class AlbumController {
@Put(':id/users') @Put(':id/users')
@Authenticated({ permission: Permission.AlbumUserCreate }) @Authenticated({ permission: Permission.AlbumUserCreate })
@ApiOperation({
summary: 'Share album with users',
description: 'Share an album with multiple users. Each user can be given a specific role in the album.',
})
addUsersToAlbum( addUsersToAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -108,6 +150,10 @@ export class AlbumController {
@Put(':id/user/:userId') @Put(':id/user/:userId')
@Authenticated({ permission: Permission.AlbumUserUpdate }) @Authenticated({ permission: Permission.AlbumUserUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Update user role',
description: 'Change the role for a specific user in a specific album.',
})
updateAlbumUser( updateAlbumUser(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -120,6 +166,10 @@ export class AlbumController {
@Delete(':id/user/:userId') @Delete(':id/user/:userId')
@Authenticated({ permission: Permission.AlbumUserDelete }) @Authenticated({ permission: Permission.AlbumUserDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Remove user from album',
description: 'Remove a user from an album. Use an ID of "me" to leave a shared album.',
})
removeUserFromAlbum( removeUserFromAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,

View file

@ -1,43 +1,60 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { ApiKeyService } from 'src/services/api-key.service'; import { ApiKeyService } from 'src/services/api-key.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('API Keys') @ApiTags(ApiTag.ApiKeys)
@Controller('api-keys') @Controller('api-keys')
export class ApiKeyController { export class ApiKeyController {
constructor(private service: ApiKeyService) {} constructor(private service: ApiKeyService) {}
@Post() @Post()
@Authenticated({ permission: Permission.ApiKeyCreate }) @Authenticated({ permission: Permission.ApiKeyCreate })
@ApiOperation({
summary: 'Create an API key',
description: 'Creates a new API key. It will be limited to the permissions specified.',
})
createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.ApiKeyRead }) @Authenticated({ permission: Permission.ApiKeyRead })
@ApiOperation({ summary: 'List all API keys', description: 'Retrieve all API keys of the current user.' })
getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> { getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Get('me') @Get('me')
@Authenticated({ permission: false }) @Authenticated({ permission: false })
@ApiOperation({
summary: 'Retrieve the current API key',
description: 'Retrieve the API key that is used to access this endpoint.',
})
async getMyApiKey(@Auth() auth: AuthDto): Promise<APIKeyResponseDto> { async getMyApiKey(@Auth() auth: AuthDto): Promise<APIKeyResponseDto> {
return this.service.getMine(auth); return this.service.getMine(auth);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.ApiKeyRead }) @Authenticated({ permission: Permission.ApiKeyRead })
@ApiOperation({
summary: 'Retrieve an API key',
description: 'Retrieve an API key by its ID. The current user must own this API key.',
})
getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> { getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(auth, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.ApiKeyUpdate }) @Authenticated({ permission: Permission.ApiKeyUpdate })
@ApiOperation({
summary: 'Update an API key',
description: 'Updates the name and permissions of an API key by its ID. The current user must own this API key.',
})
updateApiKey( updateApiKey(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -49,6 +66,10 @@ export class ApiKeyController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.ApiKeyDelete }) @Authenticated({ permission: Permission.ApiKeyDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete an API key',
description: 'Deletes an API key identified by its ID. The current user must own this API key.',
})
deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View file

@ -34,7 +34,7 @@ import {
UploadFieldName, UploadFieldName,
} from 'src/dtos/asset-media.dto'; } from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ImmichHeader, Permission, RouteKey } from 'src/enum'; import { ApiTag, ImmichHeader, Permission, RouteKey } from 'src/enum';
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.interceptor'; import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.interceptor';
@ -44,7 +44,7 @@ import { UploadFiles } from 'src/types';
import { ImmichFileResponse, sendFile } from 'src/utils/file'; import { ImmichFileResponse, sendFile } from 'src/utils/file';
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation'; import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
@ApiTags('Assets') @ApiTags(ApiTag.Assets)
@Controller(RouteKey.Asset) @Controller(RouteKey.Asset)
export class AssetMediaController { export class AssetMediaController {
constructor( constructor(
@ -53,6 +53,7 @@ export class AssetMediaController {
) {} ) {}
@Post() @Post()
@Authenticated({ permission: Permission.AssetUpload, sharedLink: true })
@UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor) @UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiHeader({ @ApiHeader({
@ -61,7 +62,10 @@ export class AssetMediaController {
required: false, required: false,
}) })
@ApiBody({ description: 'Asset Upload Information', type: AssetMediaCreateDto }) @ApiBody({ description: 'Asset Upload Information', type: AssetMediaCreateDto })
@Authenticated({ permission: Permission.AssetUpload, sharedLink: true }) @ApiOperation({
summary: 'Upload asset',
description: 'Uploads a new asset to the server.',
})
async uploadAsset( async uploadAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles, @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles,
@ -81,6 +85,10 @@ export class AssetMediaController {
@Get(':id/original') @Get(':id/original')
@FileResponse() @FileResponse()
@Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) @Authenticated({ permission: Permission.AssetDownload, sharedLink: true })
@ApiOperation({
summary: 'Download original asset',
description: 'Downloads the original file of the specified asset.',
})
async downloadAsset( async downloadAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -90,17 +98,14 @@ export class AssetMediaController {
await sendFile(res, next, () => this.service.downloadOriginal(auth, id), this.logger); await sendFile(res, next, () => this.service.downloadOriginal(auth, id), this.logger);
} }
/**
* Replace the asset with new file, without changing its id
*/
@Put(':id/original') @Put(':id/original')
@UseInterceptors(FileUploadInterceptor) @UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@EndpointLifecycle({ @EndpointLifecycle({
addedAt: 'v1.106.0', addedAt: 'v1.106.0',
deprecatedAt: 'v1.142.0', deprecatedAt: 'v1.142.0',
summary: 'replaceAsset', summary: 'Replace asset',
description: 'Replace the asset with new file, without changing its id', description: 'Replace the asset with new file, without changing its id.',
}) })
@Authenticated({ permission: Permission.AssetReplace, sharedLink: true }) @Authenticated({ permission: Permission.AssetReplace, sharedLink: true })
async replaceAsset( async replaceAsset(
@ -122,6 +127,10 @@ export class AssetMediaController {
@Get(':id/thumbnail') @Get(':id/thumbnail')
@FileResponse() @FileResponse()
@Authenticated({ permission: Permission.AssetView, sharedLink: true }) @Authenticated({ permission: Permission.AssetView, sharedLink: true })
@ApiOperation({
summary: 'View asset thumbnail',
description: 'Retrieve the thumbnail image for the specified asset.',
})
async viewAsset( async viewAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -159,6 +168,10 @@ export class AssetMediaController {
@Get(':id/video/playback') @Get(':id/video/playback')
@FileResponse() @FileResponse()
@Authenticated({ permission: Permission.AssetView, sharedLink: true }) @Authenticated({ permission: Permission.AssetView, sharedLink: true })
@ApiOperation({
summary: 'Play asset video',
description: 'Streams the video file for the specified asset. This endpoint also supports byte range requests.',
})
async playAssetVideo( async playAssetVideo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -174,7 +187,7 @@ export class AssetMediaController {
@Post('exist') @Post('exist')
@Authenticated() @Authenticated()
@ApiOperation({ @ApiOperation({
summary: 'checkExistingAssets', summary: 'Check existing assets',
description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup', description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup',
}) })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ -191,8 +204,8 @@ export class AssetMediaController {
@Post('bulk-upload-check') @Post('bulk-upload-check')
@Authenticated({ permission: Permission.AssetUpload }) @Authenticated({ permission: Permission.AssetUpload })
@ApiOperation({ @ApiOperation({
summary: 'checkBulkUpload', summary: 'Check bulk upload',
description: 'Checks if assets exist by checksums', description: 'Determine which assets have already been uploaded to the server based on their SHA1 checksums.',
}) })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
checkBulkUpload( checkBulkUpload(

View file

@ -18,29 +18,31 @@ import {
} from 'src/dtos/asset.dto'; } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto';
import { Permission, RouteKey } from 'src/enum'; import { ApiTag, Permission, RouteKey } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AssetService } from 'src/services/asset.service'; import { AssetService } from 'src/services/asset.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Assets') @ApiTags(ApiTag.Assets)
@Controller(RouteKey.Asset) @Controller(RouteKey.Asset)
export class AssetController { export class AssetController {
constructor(private service: AssetService) {} constructor(private service: AssetService) {}
@Get('random') @Get('random')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@EndpointLifecycle({ deprecatedAt: 'v1.116.0' }) @EndpointLifecycle({
deprecatedAt: 'v1.116.0',
summary: 'Get random assets',
description: 'Retrieve a specified number of random assets for the authenticated user.',
})
getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> { getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
return this.service.getRandom(auth, dto.count ?? 1); return this.service.getRandom(auth, dto.count ?? 1);
} }
/**
* Get all asset of a device that are in the database, ID only.
*/
@Get('/device/:deviceId') @Get('/device/:deviceId')
@ApiOperation({ @EndpointLifecycle({
summary: 'getAllUserAssetsByDeviceId', deprecatedAt: 'v2.0.0',
summary: 'Retrieve assets by device ID',
description: 'Get all asset of a device that are in the database, ID only.', description: 'Get all asset of a device that are in the database, ID only.',
}) })
@Authenticated() @Authenticated()
@ -50,6 +52,10 @@ export class AssetController {
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.AssetStatistics }) @Authenticated({ permission: Permission.AssetStatistics })
@ApiOperation({
summary: 'Get asset statistics',
description: 'Retrieve various statistics about the assets owned by the authenticated user.',
})
getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> { getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(auth, dto); return this.service.getStatistics(auth, dto);
} }
@ -57,6 +63,10 @@ export class AssetController {
@Post('jobs') @Post('jobs')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Run an asset job',
description: 'Run a specific job on a set of assets.',
})
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> { runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
return this.service.run(auth, dto); return this.service.run(auth, dto);
} }
@ -64,6 +74,10 @@ export class AssetController {
@Put() @Put()
@Authenticated({ permission: Permission.AssetUpdate }) @Authenticated({ permission: Permission.AssetUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Update assets',
description: 'Updates multiple assets at the same time.',
})
updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> { updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(auth, dto); return this.service.updateAll(auth, dto);
} }
@ -71,12 +85,20 @@ export class AssetController {
@Delete() @Delete()
@Authenticated({ permission: Permission.AssetDelete }) @Authenticated({ permission: Permission.AssetDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete assets',
description: 'Deletes multiple assets at the same time.',
})
deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> { deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.AssetRead, sharedLink: true }) @Authenticated({ permission: Permission.AssetRead, sharedLink: true })
@ApiOperation({
summary: 'Retrieve an asset',
description: 'Retrieve detailed information about a specific asset.',
})
getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> { getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.service.get(auth, id) as Promise<AssetResponseDto>; return this.service.get(auth, id) as Promise<AssetResponseDto>;
} }
@ -84,12 +106,20 @@ export class AssetController {
@Put('copy') @Put('copy')
@Authenticated({ permission: Permission.AssetCopy }) @Authenticated({ permission: Permission.AssetCopy })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Copy asset',
description: 'Copy asset information like albums, tags, etc. from one asset to another.',
})
copyAsset(@Auth() auth: AuthDto, @Body() dto: AssetCopyDto): Promise<void> { copyAsset(@Auth() auth: AuthDto, @Body() dto: AssetCopyDto): Promise<void> {
return this.service.copy(auth, dto); return this.service.copy(auth, dto);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.AssetUpdate }) @Authenticated({ permission: Permission.AssetUpdate })
@ApiOperation({
summary: 'Update an asset',
description: 'Update information of a specific asset.',
})
updateAsset( updateAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -100,18 +130,30 @@ export class AssetController {
@Get(':id/metadata') @Get(':id/metadata')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ApiOperation({
summary: 'Get asset metadata',
description: 'Retrieve all metadata key-value pairs associated with the specified asset.',
})
getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetMetadataResponseDto[]> { getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetMetadataResponseDto[]> {
return this.service.getMetadata(auth, id); return this.service.getMetadata(auth, id);
} }
@Get(':id/ocr') @Get(':id/ocr')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ApiOperation({
summary: 'Retrieve asset OCR data',
description: 'Retrieve all OCR (Optical Character Recognition) data associated with the specified asset.',
})
getAssetOcr(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetOcrResponseDto[]> { getAssetOcr(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetOcrResponseDto[]> {
return this.service.getOcr(auth, id); return this.service.getOcr(auth, id);
} }
@Put(':id/metadata') @Put(':id/metadata')
@Authenticated({ permission: Permission.AssetUpdate }) @Authenticated({ permission: Permission.AssetUpdate })
@ApiOperation({
summary: 'Update asset metadata',
description: 'Update or add metadata key-value pairs for the specified asset.',
})
updateAssetMetadata( updateAssetMetadata(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -122,6 +164,10 @@ export class AssetController {
@Get(':id/metadata/:key') @Get(':id/metadata/:key')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ApiOperation({
summary: 'Retrieve asset metadata by key',
description: 'Retrieve the value of a specific metadata key associated with the specified asset.',
})
getAssetMetadataByKey( getAssetMetadataByKey(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id, key }: AssetMetadataRouteParams, @Param() { id, key }: AssetMetadataRouteParams,
@ -132,6 +178,10 @@ export class AssetController {
@Delete(':id/metadata/:key') @Delete(':id/metadata/:key')
@Authenticated({ permission: Permission.AssetUpdate }) @Authenticated({ permission: Permission.AssetUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete asset metadata by key',
description: 'Delete a specific metadata key-value pair associated with the specified asset.',
})
deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise<void> { deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise<void> {
return this.service.deleteMetadataByKey(auth, id, key); return this.service.deleteMetadataByKey(auth, id, key);
} }

View file

@ -1,17 +1,21 @@
import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthAdminService } from 'src/services/auth-admin.service';
@ApiTags('Auth (admin)') @ApiTags(ApiTag.AuthenticationAdmin)
@Controller('admin/auth') @Controller('admin/auth')
export class AuthAdminController { export class AuthAdminController {
constructor(private service: AuthAdminService) {} constructor(private service: AuthAdminService) {}
@Post('unlink-all') @Post('unlink-all')
@Authenticated({ permission: Permission.AdminAuthUnlinkAll, admin: true }) @Authenticated({ permission: Permission.AdminAuthUnlinkAll, admin: true })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Unlink all OAuth accounts',
description: 'Unlinks all OAuth accounts associated with user accounts in the system.',
})
unlinkAllOAuthAccountsAdmin(@Auth() auth: AuthDto): Promise<void> { unlinkAllOAuthAccountsAdmin(@Auth() auth: AuthDto): Promise<void> {
return this.service.unlinkAll(auth); return this.service.unlinkAll(auth);
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { import {
AuthDto, AuthDto,
@ -16,17 +16,21 @@ import {
ValidateAccessTokenResponseDto, ValidateAccessTokenResponseDto,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto'; import { UserAdminResponseDto } from 'src/dtos/user.dto';
import { AuthType, ImmichCookie, Permission } from 'src/enum'; import { ApiTag, AuthType, ImmichCookie, Permission } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service'; import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
@ApiTags('Authentication') @ApiTags(ApiTag.Authentication)
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor(private service: AuthService) {} constructor(private service: AuthService) {}
@Post('login') @Post('login')
@ApiOperation({
summary: 'Login',
description: 'Login with username and password and receive a session token.',
})
async login( async login(
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Body() loginCredential: LoginCredentialDto, @Body() loginCredential: LoginCredentialDto,
@ -44,11 +48,19 @@ export class AuthController {
} }
@Post('admin-sign-up') @Post('admin-sign-up')
@ApiOperation({
summary: 'Register admin',
description: 'Create the first admin user in the system.',
})
signUpAdmin(@Body() dto: SignUpDto): Promise<UserAdminResponseDto> { signUpAdmin(@Body() dto: SignUpDto): Promise<UserAdminResponseDto> {
return this.service.adminSignUp(dto); return this.service.adminSignUp(dto);
} }
@Post('validateToken') @Post('validateToken')
@ApiOperation({
summary: 'Validate access token',
description: 'Validate the current authorization method is still valid.',
})
@Authenticated({ permission: false }) @Authenticated({ permission: false })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
validateAccessToken(): ValidateAccessTokenResponseDto { validateAccessToken(): ValidateAccessTokenResponseDto {
@ -58,6 +70,10 @@ export class AuthController {
@Post('change-password') @Post('change-password')
@Authenticated({ permission: Permission.AuthChangePassword }) @Authenticated({ permission: Permission.AuthChangePassword })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Change password',
description: 'Change the password of the current user.',
})
changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserAdminResponseDto> { changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
return this.service.changePassword(auth, dto); return this.service.changePassword(auth, dto);
} }
@ -65,6 +81,10 @@ export class AuthController {
@Post('logout') @Post('logout')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Logout',
description: 'Logout the current user and invalidate the session token.',
})
async logout( async logout(
@Req() request: Request, @Req() request: Request,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@ -82,6 +102,11 @@ export class AuthController {
@Get('status') @Get('status')
@Authenticated() @Authenticated()
@ApiOperation({
summary: 'Retrieve auth status',
description:
'Get information about the current session, including whether the user has a password, and if the session can access locked assets.',
})
getAuthStatus(@Auth() auth: AuthDto): Promise<AuthStatusResponseDto> { getAuthStatus(@Auth() auth: AuthDto): Promise<AuthStatusResponseDto> {
return this.service.getAuthStatus(auth); return this.service.getAuthStatus(auth);
} }
@ -89,6 +114,10 @@ export class AuthController {
@Post('pin-code') @Post('pin-code')
@Authenticated({ permission: Permission.PinCodeCreate }) @Authenticated({ permission: Permission.PinCodeCreate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Setup pin code',
description: 'Setup a new pin code for the current user.',
})
setupPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise<void> { setupPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise<void> {
return this.service.setupPinCode(auth, dto); return this.service.setupPinCode(auth, dto);
} }
@ -96,6 +125,10 @@ export class AuthController {
@Put('pin-code') @Put('pin-code')
@Authenticated({ permission: Permission.PinCodeUpdate }) @Authenticated({ permission: Permission.PinCodeUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Change pin code',
description: 'Change the pin code for the current user.',
})
async changePinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> { async changePinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> {
return this.service.changePinCode(auth, dto); return this.service.changePinCode(auth, dto);
} }
@ -103,6 +136,10 @@ export class AuthController {
@Delete('pin-code') @Delete('pin-code')
@Authenticated({ permission: Permission.PinCodeDelete }) @Authenticated({ permission: Permission.PinCodeDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Reset pin code',
description: 'Reset the pin code for the current user by providing the account password',
})
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise<void> { async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise<void> {
return this.service.resetPinCode(auth, dto); return this.service.resetPinCode(auth, dto);
} }
@ -110,12 +147,20 @@ export class AuthController {
@Post('session/unlock') @Post('session/unlock')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Unlock auth session',
description: 'Temporarily grant the session elevated access to locked assets by providing the correct PIN code.',
})
async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise<void> { async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise<void> {
return this.service.unlockSession(auth, dto); return this.service.unlockSession(auth, dto);
} }
@Post('session/lock') @Post('session/lock')
@Authenticated() @Authenticated()
@ApiOperation({
summary: 'Lock auth session',
description: 'Remove elevated access to locked assets from the current session.',
})
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
async lockAuthSession(@Auth() auth: AuthDto): Promise<void> { async lockAuthSession(@Auth() auth: AuthDto): Promise<void> {
return this.service.lockSession(auth); return this.service.lockSession(auth);

View file

@ -1,20 +1,25 @@
import { Body, Controller, HttpCode, HttpStatus, Post, StreamableFile } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Post, StreamableFile } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { DownloadService } from 'src/services/download.service'; import { DownloadService } from 'src/services/download.service';
import { asStreamableFile } from 'src/utils/file'; import { asStreamableFile } from 'src/utils/file';
@ApiTags('Download') @ApiTags(ApiTag.Download)
@Controller('download') @Controller('download')
export class DownloadController { export class DownloadController {
constructor(private service: DownloadService) {} constructor(private service: DownloadService) {}
@Post('info') @Post('info')
@Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) @Authenticated({ permission: Permission.AssetDownload, sharedLink: true })
@ApiOperation({
summary: 'Retrieve download information',
description:
'Retrieve information about how to request a download for the specified assets or album. The response includes groups of assets that can be downloaded together.',
})
getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> { getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
return this.service.getDownloadInfo(auth, dto); return this.service.getDownloadInfo(auth, dto);
} }
@ -23,6 +28,11 @@ export class DownloadController {
@Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) @Authenticated({ permission: Permission.AssetDownload, sharedLink: true })
@FileResponse() @FileResponse()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Download asset archive',
description:
'Download a ZIP archive containing the specified assets. The assets must have been previously requested via the "getDownloadInfo" endpoint.',
})
downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> { downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
return this.service.downloadArchive(auth, dto).then(asStreamableFile); return this.service.downloadArchive(auth, dto).then(asStreamableFile);
} }

View file

@ -1,20 +1,24 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto'; import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { DuplicateService } from 'src/services/duplicate.service'; import { DuplicateService } from 'src/services/duplicate.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Duplicates') @ApiTags(ApiTag.Duplicates)
@Controller('duplicates') @Controller('duplicates')
export class DuplicateController { export class DuplicateController {
constructor(private service: DuplicateService) {} constructor(private service: DuplicateService) {}
@Get() @Get()
@Authenticated({ permission: Permission.DuplicateRead }) @Authenticated({ permission: Permission.DuplicateRead })
@ApiOperation({
summary: 'Retrieve duplicates',
description: 'Retrieve a list of duplicate assets available to the authenticated user.',
})
getAssetDuplicates(@Auth() auth: AuthDto): Promise<DuplicateResponseDto[]> { getAssetDuplicates(@Auth() auth: AuthDto): Promise<DuplicateResponseDto[]> {
return this.service.getDuplicates(auth); return this.service.getDuplicates(auth);
} }
@ -22,6 +26,10 @@ export class DuplicateController {
@Delete() @Delete()
@Authenticated({ permission: Permission.DuplicateDelete }) @Authenticated({ permission: Permission.DuplicateDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete duplicates',
description: 'Delete multiple duplicate assets specified by their IDs.',
})
deleteDuplicates(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { deleteDuplicates(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@ -29,6 +37,10 @@ export class DuplicateController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.DuplicateDelete }) @Authenticated({ permission: Permission.DuplicateDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a duplicate',
description: 'Delete a single duplicate asset specified by its ID.',
})
deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
AssetFaceCreateDto, AssetFaceCreateDto,
@ -8,30 +8,43 @@ import {
FaceDto, FaceDto,
PersonResponseDto, PersonResponseDto,
} from 'src/dtos/person.dto'; } from 'src/dtos/person.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PersonService } from 'src/services/person.service'; import { PersonService } from 'src/services/person.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Faces') @ApiTags(ApiTag.Faces)
@Controller('faces') @Controller('faces')
export class FaceController { export class FaceController {
constructor(private service: PersonService) {} constructor(private service: PersonService) {}
@Post() @Post()
@Authenticated({ permission: Permission.FaceCreate }) @Authenticated({ permission: Permission.FaceCreate })
@ApiOperation({
summary: 'Create a face',
description:
'Create a new face that has not been discovered by facial recognition. The content of the bounding box is considered a face.',
})
createFace(@Auth() auth: AuthDto, @Body() dto: AssetFaceCreateDto) { createFace(@Auth() auth: AuthDto, @Body() dto: AssetFaceCreateDto) {
return this.service.createFace(auth, dto); return this.service.createFace(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.FaceRead }) @Authenticated({ permission: Permission.FaceRead })
@ApiOperation({
summary: 'Retrieve faces for asset',
description: 'Retrieve all faces belonging to an asset.',
})
getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> { getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
return this.service.getFacesById(auth, dto); return this.service.getFacesById(auth, dto);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.FaceUpdate }) @Authenticated({ permission: Permission.FaceUpdate })
@ApiOperation({
summary: 'Re-assign a face to another person',
description: 'Re-assign the face provided in the body to the person identified by the id in the path parameter.',
})
reassignFacesById( reassignFacesById(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -43,6 +56,10 @@ export class FaceController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.FaceDelete }) @Authenticated({ permission: Permission.FaceDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a face',
description: 'Delete a face identified by the id. Optionally can be force deleted.',
})
deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto): Promise<void> { deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto): Promise<void> {
return this.service.deleteFace(auth, id, dto); return this.service.deleteFace(auth, id, dto);
} }

View file

@ -1,17 +1,21 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { JobService } from 'src/services/job.service'; import { JobService } from 'src/services/job.service';
@ApiTags('Jobs') @ApiTags(ApiTag.Jobs)
@Controller('jobs') @Controller('jobs')
export class JobController { export class JobController {
constructor(private service: JobService) {} constructor(private service: JobService) {}
@Get() @Get()
@Authenticated({ permission: Permission.JobRead, admin: true }) @Authenticated({ permission: Permission.JobRead, admin: true })
@ApiOperation({
summary: 'Retrieve queue counts and status',
description: 'Retrieve the counts of the current queue, as well as the current status.',
})
getAllJobsStatus(): Promise<AllJobStatusResponseDto> { getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
return this.service.getAllJobsStatus(); return this.service.getAllJobsStatus();
} }
@ -19,12 +23,22 @@ export class JobController {
@Post() @Post()
@Authenticated({ permission: Permission.JobCreate, admin: true }) @Authenticated({ permission: Permission.JobCreate, admin: true })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Create a manual job',
description:
'Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.',
})
createJob(@Body() dto: JobCreateDto): Promise<void> { createJob(@Body() dto: JobCreateDto): Promise<void> {
return this.service.create(dto); return this.service.create(dto);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.JobCreate, admin: true }) @Authenticated({ permission: Permission.JobCreate, admin: true })
@ApiOperation({
summary: 'Run jobs',
description:
'Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.',
})
sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> { sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
return this.service.handleCommand(id, dto); return this.service.handleCommand(id, dto);
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { import {
CreateLibraryDto, CreateLibraryDto,
LibraryResponseDto, LibraryResponseDto,
@ -8,36 +8,52 @@ import {
ValidateLibraryDto, ValidateLibraryDto,
ValidateLibraryResponseDto, ValidateLibraryResponseDto,
} from 'src/dtos/library.dto'; } from 'src/dtos/library.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { LibraryService } from 'src/services/library.service'; import { LibraryService } from 'src/services/library.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Libraries') @ApiTags(ApiTag.Libraries)
@Controller('libraries') @Controller('libraries')
export class LibraryController { export class LibraryController {
constructor(private service: LibraryService) {} constructor(private service: LibraryService) {}
@Get() @Get()
@Authenticated({ permission: Permission.LibraryRead, admin: true }) @Authenticated({ permission: Permission.LibraryRead, admin: true })
@ApiOperation({
summary: 'Retrieve libraries',
description: 'Retrieve a list of external libraries.',
})
getAllLibraries(): Promise<LibraryResponseDto[]> { getAllLibraries(): Promise<LibraryResponseDto[]> {
return this.service.getAll(); return this.service.getAll();
} }
@Post() @Post()
@Authenticated({ permission: Permission.LibraryCreate, admin: true }) @Authenticated({ permission: Permission.LibraryCreate, admin: true })
@ApiOperation({
summary: 'Create a library',
description: 'Create a new external library.',
})
createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> { createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> {
return this.service.create(dto); return this.service.create(dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.LibraryRead, admin: true }) @Authenticated({ permission: Permission.LibraryRead, admin: true })
@ApiOperation({
summary: 'Retrieve a library',
description: 'Retrieve an external library by its ID.',
})
getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> { getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.LibraryUpdate, admin: true }) @Authenticated({ permission: Permission.LibraryUpdate, admin: true })
@ApiOperation({
summary: 'Update a library',
description: 'Update an existing external library.',
})
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> { updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto); return this.service.update(id, dto);
} }
@ -45,6 +61,10 @@ export class LibraryController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.LibraryDelete, admin: true }) @Authenticated({ permission: Permission.LibraryDelete, admin: true })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a library',
description: 'Delete an external library by its ID.',
})
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> { deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(id); return this.service.delete(id);
} }
@ -52,6 +72,10 @@ export class LibraryController {
@Post(':id/validate') @Post(':id/validate')
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Validate library settings',
description: 'Validate the settings of an external library.',
})
// TODO: change endpoint to validate current settings instead // TODO: change endpoint to validate current settings instead
validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> { validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
return this.service.validate(id, dto); return this.service.validate(id, dto);
@ -59,6 +83,11 @@ export class LibraryController {
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.LibraryStatistics, admin: true }) @Authenticated({ permission: Permission.LibraryStatistics, admin: true })
@ApiOperation({
summary: 'Retrieve library statistics',
description:
'Retrieve statistics for a specific external library, including number of videos, images, and storage usage.',
})
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> { getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(id); return this.service.getStatistics(id);
} }
@ -66,6 +95,10 @@ export class LibraryController {
@Post(':id/scan') @Post(':id/scan')
@Authenticated({ permission: Permission.LibraryUpdate, admin: true }) @Authenticated({ permission: Permission.LibraryUpdate, admin: true })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Scan a library',
description: 'Queue a scan for the external library to find and import new assets.',
})
scanLibrary(@Param() { id }: UUIDParamDto): Promise<void> { scanLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
return this.service.queueScan(id); return this.service.queueScan(id);
} }

View file

@ -1,5 +1,5 @@
import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common'; import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
MapMarkerDto, MapMarkerDto,
@ -7,16 +7,21 @@ import {
MapReverseGeocodeDto, MapReverseGeocodeDto,
MapReverseGeocodeResponseDto, MapReverseGeocodeResponseDto,
} from 'src/dtos/map.dto'; } from 'src/dtos/map.dto';
import { ApiTag } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { MapService } from 'src/services/map.service'; import { MapService } from 'src/services/map.service';
@ApiTags('Map') @ApiTags(ApiTag.Map)
@Controller('map') @Controller('map')
export class MapController { export class MapController {
constructor(private service: MapService) {} constructor(private service: MapService) {}
@Get('markers') @Get('markers')
@Authenticated() @Authenticated()
@ApiOperation({
summary: 'Retrieve map markers',
description: 'Retrieve a list of latitude and longitude coordinates for every asset with location data.',
})
getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> { getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.service.getMapMarkers(auth, options); return this.service.getMapMarkers(auth, options);
} }
@ -24,6 +29,10 @@ export class MapController {
@Authenticated() @Authenticated()
@Get('reverse-geocode') @Get('reverse-geocode')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Reverse geocode coordinates',
description: 'Retrieve location information (e.g., city, country) for given latitude and longitude coordinates.',
})
reverseGeocode(@Query() dto: MapReverseGeocodeDto): Promise<MapReverseGeocodeResponseDto[]> { reverseGeocode(@Query() dto: MapReverseGeocodeDto): Promise<MapReverseGeocodeResponseDto[]> {
return this.service.reverseGeocode(dto); return this.service.reverseGeocode(dto);
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
@ -9,42 +9,64 @@ import {
MemoryStatisticsResponseDto, MemoryStatisticsResponseDto,
MemoryUpdateDto, MemoryUpdateDto,
} from 'src/dtos/memory.dto'; } from 'src/dtos/memory.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { MemoryService } from 'src/services/memory.service'; import { MemoryService } from 'src/services/memory.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Memories') @ApiTags(ApiTag.Memories)
@Controller('memories') @Controller('memories')
export class MemoryController { export class MemoryController {
constructor(private service: MemoryService) {} constructor(private service: MemoryService) {}
@Get() @Get()
@Authenticated({ permission: Permission.MemoryRead }) @Authenticated({ permission: Permission.MemoryRead })
@ApiOperation({
summary: 'Retrieve memories',
description:
'Retrieve a list of memories. Memories are sorted descending by creation date by default, although they can also be sorted in ascending order, or randomly.',
})
searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryResponseDto[]> { searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post() @Post()
@Authenticated({ permission: Permission.MemoryCreate }) @Authenticated({ permission: Permission.MemoryCreate })
@ApiOperation({
summary: 'Create a memory',
description:
'Create a new memory by providing a name, description, and a list of asset IDs to include in the memory.',
})
createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> { createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.MemoryStatistics }) @Authenticated({ permission: Permission.MemoryStatistics })
@ApiOperation({
summary: 'Retrieve memories statistics',
description: 'Retrieve statistics about memories, such as total count and other relevant metrics.',
})
memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryStatisticsResponseDto> { memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryStatisticsResponseDto> {
return this.service.statistics(auth, dto); return this.service.statistics(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.MemoryRead }) @Authenticated({ permission: Permission.MemoryRead })
@ApiOperation({
summary: 'Retrieve a memory',
description: 'Retrieve a specific memory by its ID.',
})
getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> { getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.MemoryUpdate }) @Authenticated({ permission: Permission.MemoryUpdate })
@ApiOperation({
summary: 'Update a memory',
description: 'Update an existing memory by its ID.',
})
updateMemory( updateMemory(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -56,12 +78,20 @@ export class MemoryController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.MemoryDelete }) @Authenticated({ permission: Permission.MemoryDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a memory',
description: 'Delete a specific memory by its ID.',
})
deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
@Authenticated({ permission: Permission.MemoryAssetCreate }) @Authenticated({ permission: Permission.MemoryAssetCreate })
@ApiOperation({
summary: 'Add assets to a memory',
description: 'Add a list of asset IDs to a specific memory.',
})
addMemoryAssets( addMemoryAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -73,6 +103,10 @@ export class MemoryController {
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated({ permission: Permission.MemoryAssetDelete }) @Authenticated({ permission: Permission.MemoryAssetDelete })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Remove assets from a memory',
description: 'Remove a list of asset IDs from a specific memory.',
})
removeMemoryAssets( removeMemoryAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: BulkIdsDto, @Body() dto: BulkIdsDto,

View file

@ -1,5 +1,5 @@
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
NotificationCreateDto, NotificationCreateDto,
@ -9,17 +9,22 @@ import {
TestEmailResponseDto, TestEmailResponseDto,
} from 'src/dtos/notification.dto'; } from 'src/dtos/notification.dto';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { ApiTag } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { EmailTemplate } from 'src/repositories/email.repository'; import { EmailTemplate } from 'src/repositories/email.repository';
import { NotificationAdminService } from 'src/services/notification-admin.service'; import { NotificationAdminService } from 'src/services/notification-admin.service';
@ApiTags('Notifications (Admin)') @ApiTags(ApiTag.NotificationsAdmin)
@Controller('admin/notifications') @Controller('admin/notifications')
export class NotificationAdminController { export class NotificationAdminController {
constructor(private service: NotificationAdminService) {} constructor(private service: NotificationAdminService) {}
@Post() @Post()
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@ApiOperation({
summary: 'Create a notification',
description: 'Create a new notification for a specific user.',
})
createNotification(@Auth() auth: AuthDto, @Body() dto: NotificationCreateDto): Promise<NotificationDto> { createNotification(@Auth() auth: AuthDto, @Body() dto: NotificationCreateDto): Promise<NotificationDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@ -27,6 +32,10 @@ export class NotificationAdminController {
@Post('test-email') @Post('test-email')
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Send test email',
description: 'Send a test email using the provided SMTP configuration.',
})
sendTestEmailAdmin(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise<TestEmailResponseDto> { sendTestEmailAdmin(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise<TestEmailResponseDto> {
return this.service.sendTestEmail(auth.user.id, dto); return this.service.sendTestEmail(auth.user.id, dto);
} }
@ -34,6 +43,10 @@ export class NotificationAdminController {
@Post('templates/:name') @Post('templates/:name')
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Render email template',
description: 'Retrieve a preview of the provided email template.',
})
getNotificationTemplateAdmin( getNotificationTemplateAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param('name') name: EmailTemplate, @Param('name') name: EmailTemplate,

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
NotificationDeleteAllDto, NotificationDeleteAllDto,
@ -8,18 +8,22 @@ import {
NotificationUpdateAllDto, NotificationUpdateAllDto,
NotificationUpdateDto, NotificationUpdateDto,
} from 'src/dtos/notification.dto'; } from 'src/dtos/notification.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { NotificationService } from 'src/services/notification.service'; import { NotificationService } from 'src/services/notification.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Notifications') @ApiTags(ApiTag.Notifications)
@Controller('notifications') @Controller('notifications')
export class NotificationController { export class NotificationController {
constructor(private service: NotificationService) {} constructor(private service: NotificationService) {}
@Get() @Get()
@Authenticated({ permission: Permission.NotificationRead }) @Authenticated({ permission: Permission.NotificationRead })
@ApiOperation({
summary: 'Retrieve notifications',
description: 'Retrieve a list of notifications.',
})
getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> { getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@ -27,6 +31,10 @@ export class NotificationController {
@Put() @Put()
@Authenticated({ permission: Permission.NotificationUpdate }) @Authenticated({ permission: Permission.NotificationUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Update notifications',
description: 'Update a list of notifications. Allows to bulk-set the read status of notifications.',
})
updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> { updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> {
return this.service.updateAll(auth, dto); return this.service.updateAll(auth, dto);
} }
@ -34,18 +42,30 @@ export class NotificationController {
@Delete() @Delete()
@Authenticated({ permission: Permission.NotificationDelete }) @Authenticated({ permission: Permission.NotificationDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete notifications',
description: 'Delete a list of notifications at once.',
})
deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> { deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.NotificationRead }) @Authenticated({ permission: Permission.NotificationRead })
@ApiOperation({
summary: 'Get a notification',
description: 'Retrieve a specific notification identified by id.',
})
getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> { getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.NotificationUpdate }) @Authenticated({ permission: Permission.NotificationUpdate })
@ApiOperation({
summary: 'Update a notification',
description: 'Update a specific notification to set its read status.',
})
updateNotification( updateNotification(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -57,6 +77,10 @@ export class NotificationController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.NotificationDelete }) @Authenticated({ permission: Permission.NotificationDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a notification',
description: 'Delete a specific notification.',
})
deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { import {
AuthDto, AuthDto,
@ -9,18 +9,23 @@ import {
OAuthConfigDto, OAuthConfigDto,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto'; import { UserAdminResponseDto } from 'src/dtos/user.dto';
import { AuthType, ImmichCookie } from 'src/enum'; import { ApiTag, AuthType, ImmichCookie } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service'; import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie } from 'src/utils/response'; import { respondWithCookie } from 'src/utils/response';
@ApiTags('OAuth') @ApiTags(ApiTag.Authentication)
@Controller('oauth') @Controller('oauth')
export class OAuthController { export class OAuthController {
constructor(private service: AuthService) {} constructor(private service: AuthService) {}
@Get('mobile-redirect') @Get('mobile-redirect')
@Redirect() @Redirect()
@ApiOperation({
summary: 'Redirect OAuth to mobile',
description:
'Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting.',
})
redirectOAuthToMobile(@Req() request: Request) { redirectOAuthToMobile(@Req() request: Request) {
return { return {
url: this.service.getMobileRedirect(request.url), url: this.service.getMobileRedirect(request.url),
@ -29,6 +34,10 @@ export class OAuthController {
} }
@Post('authorize') @Post('authorize')
@ApiOperation({
summary: 'Start OAuth',
description: 'Initiate the OAuth authorization process.',
})
async startOAuth( async startOAuth(
@Body() dto: OAuthConfigDto, @Body() dto: OAuthConfigDto,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@ -49,6 +58,10 @@ export class OAuthController {
} }
@Post('callback') @Post('callback')
@ApiOperation({
summary: 'Finish OAuth',
description: 'Complete the OAuth authorization process by exchanging the authorization code for a session token.',
})
async finishOAuth( async finishOAuth(
@Req() request: Request, @Req() request: Request,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@ -71,6 +84,10 @@ export class OAuthController {
@Post('link') @Post('link')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Link OAuth account',
description: 'Link an OAuth account to the authenticated user.',
})
linkOAuthAccount( linkOAuthAccount(
@Req() request: Request, @Req() request: Request,
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@ -82,6 +99,10 @@ export class OAuthController {
@Post('unlink') @Post('unlink')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Unlink OAuth account',
description: 'Unlink the OAuth account from the authenticated user.',
})
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> { unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
return this.service.unlink(auth); return this.service.unlink(auth);
} }

View file

@ -1,32 +1,44 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { EndpointLifecycle } from 'src/decorators'; import { EndpointLifecycle } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto'; import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Partners') @ApiTags(ApiTag.Partners)
@Controller('partners') @Controller('partners')
export class PartnerController { export class PartnerController {
constructor(private service: PartnerService) {} constructor(private service: PartnerService) {}
@Get() @Get()
@Authenticated({ permission: Permission.PartnerRead }) @Authenticated({ permission: Permission.PartnerRead })
@ApiOperation({
summary: 'Retrieve partners',
description: 'Retrieve a list of partners with whom assets are shared.',
})
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> { getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post() @Post()
@Authenticated({ permission: Permission.PartnerCreate }) @Authenticated({ permission: Permission.PartnerCreate })
@ApiOperation({
summary: 'Create a partner',
description: 'Create a new partner to share assets with.',
})
createPartner(@Auth() auth: AuthDto, @Body() dto: PartnerCreateDto): Promise<PartnerResponseDto> { createPartner(@Auth() auth: AuthDto, @Body() dto: PartnerCreateDto): Promise<PartnerResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Post(':id') @Post(':id')
@EndpointLifecycle({ deprecatedAt: 'v1.141.0' }) @EndpointLifecycle({
deprecatedAt: 'v1.141.0',
summary: 'Create a partner',
description: 'Create a new partner to share assets with.',
})
@Authenticated({ permission: Permission.PartnerCreate }) @Authenticated({ permission: Permission.PartnerCreate })
createPartnerDeprecated(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> { createPartnerDeprecated(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(auth, { sharedWithId: id }); return this.service.create(auth, { sharedWithId: id });
@ -34,6 +46,10 @@ export class PartnerController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.PartnerUpdate }) @Authenticated({ permission: Permission.PartnerUpdate })
@ApiOperation({
summary: 'Update a partner',
description: "Specify whether a partner's assets should appear in the user's timeline.",
})
updatePartner( updatePartner(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -45,6 +61,10 @@ export class PartnerController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.PartnerDelete }) @Authenticated({ permission: Permission.PartnerDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Remove a partner',
description: 'Stop sharing assets with a partner.',
})
removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }

View file

@ -12,7 +12,7 @@ import {
Query, Query,
Res, Res,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -27,14 +27,14 @@ import {
PersonStatisticsResponseDto, PersonStatisticsResponseDto,
PersonUpdateDto, PersonUpdateDto,
} from 'src/dtos/person.dto'; } from 'src/dtos/person.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { PersonService } from 'src/services/person.service'; import { PersonService } from 'src/services/person.service';
import { sendFile } from 'src/utils/file'; import { sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('People') @ApiTags(ApiTag.People)
@Controller('people') @Controller('people')
export class PersonController { export class PersonController {
constructor( constructor(
@ -46,18 +46,24 @@ export class PersonController {
@Get() @Get()
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ApiOperation({ summary: 'Get all people', description: 'Retrieve a list of all people.' })
getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise<PeopleResponseDto> { getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(auth, options); return this.service.getAll(auth, options);
} }
@Post() @Post()
@Authenticated({ permission: Permission.PersonCreate }) @Authenticated({ permission: Permission.PersonCreate })
@ApiOperation({
summary: 'Create a person',
description: 'Create a new person that can have multiple faces assigned to them.',
})
createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> { createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Put() @Put()
@Authenticated({ permission: Permission.PersonUpdate }) @Authenticated({ permission: Permission.PersonUpdate })
@ApiOperation({ summary: 'Update people', description: 'Bulk update multiple people at once.' })
updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> { updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updateAll(auth, dto); return this.service.updateAll(auth, dto);
} }
@ -65,18 +71,21 @@ export class PersonController {
@Delete() @Delete()
@Authenticated({ permission: Permission.PersonDelete }) @Authenticated({ permission: Permission.PersonDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete people', description: 'Bulk delete a list of people at once.' })
deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ApiOperation({ summary: 'Get a person', description: 'Retrieve a person by id.' })
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> { getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(auth, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.PersonUpdate }) @Authenticated({ permission: Permission.PersonUpdate })
@ApiOperation({ summary: 'Update person', description: 'Update an individual person.' })
updatePerson( updatePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -88,12 +97,14 @@ export class PersonController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.PersonDelete }) @Authenticated({ permission: Permission.PersonDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete person', description: 'Delete an individual person.' })
deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.PersonStatistics }) @Authenticated({ permission: Permission.PersonStatistics })
@ApiOperation({ summary: 'Get person statistics', description: 'Retrieve statistics about a specific person.' })
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> { getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
return this.service.getStatistics(auth, id); return this.service.getStatistics(auth, id);
} }
@ -101,6 +112,7 @@ export class PersonController {
@Get(':id/thumbnail') @Get(':id/thumbnail')
@FileResponse() @FileResponse()
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ApiOperation({ summary: 'Get person thumbnail', description: 'Retrieve the thumbnail file for a person.' })
async getPersonThumbnail( async getPersonThumbnail(
@Res() res: Response, @Res() res: Response,
@Next() next: NextFunction, @Next() next: NextFunction,
@ -112,6 +124,7 @@ export class PersonController {
@Put(':id/reassign') @Put(':id/reassign')
@Authenticated({ permission: Permission.PersonReassign }) @Authenticated({ permission: Permission.PersonReassign })
@ApiOperation({ summary: 'Reassign faces', description: 'Bulk reassign a list of faces to a different person.' })
reassignFaces( reassignFaces(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -123,6 +136,10 @@ export class PersonController {
@Post(':id/merge') @Post(':id/merge')
@Authenticated({ permission: Permission.PersonMerge }) @Authenticated({ permission: Permission.PersonMerge })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Merge people',
description: 'Merge a list of people into the person specified in the path parameter.',
})
mergePerson( mergePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,

View file

@ -1,5 +1,5 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto'; import { PersonResponseDto } from 'src/dtos/person.dto';
@ -17,11 +17,11 @@ import {
SmartSearchDto, SmartSearchDto,
StatisticsSearchDto, StatisticsSearchDto,
} from 'src/dtos/search.dto'; } from 'src/dtos/search.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
@ApiTags('Search') @ApiTags(ApiTag.Search)
@Controller('search') @Controller('search')
export class SearchController { export class SearchController {
constructor(private service: SearchService) {} constructor(private service: SearchService) {}
@ -29,6 +29,10 @@ export class SearchController {
@Post('metadata') @Post('metadata')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Search assets by metadata',
description: 'Search for assets based on various metadata criteria.',
})
searchAssets(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> { searchAssets(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
return this.service.searchMetadata(auth, dto); return this.service.searchMetadata(auth, dto);
} }
@ -36,6 +40,10 @@ export class SearchController {
@Post('statistics') @Post('statistics')
@Authenticated({ permission: Permission.AssetStatistics }) @Authenticated({ permission: Permission.AssetStatistics })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Search asset statistics',
description: 'Retrieve statistical data about assets based on search criteria, such as the total matching count.',
})
searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> { searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
return this.service.searchStatistics(auth, dto); return this.service.searchStatistics(auth, dto);
} }
@ -43,6 +51,10 @@ export class SearchController {
@Post('random') @Post('random')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Search random assets',
description: 'Retrieve a random selection of assets based on the provided criteria.',
})
searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<AssetResponseDto[]> { searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<AssetResponseDto[]> {
return this.service.searchRandom(auth, dto); return this.service.searchRandom(auth, dto);
} }
@ -50,6 +62,10 @@ export class SearchController {
@Post('large-assets') @Post('large-assets')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Search large assets',
description: 'Search for assets that are considered large based on specified criteria.',
})
searchLargeAssets(@Auth() auth: AuthDto, @Query() dto: LargeAssetSearchDto): Promise<AssetResponseDto[]> { searchLargeAssets(@Auth() auth: AuthDto, @Query() dto: LargeAssetSearchDto): Promise<AssetResponseDto[]> {
return this.service.searchLargeAssets(auth, dto); return this.service.searchLargeAssets(auth, dto);
} }
@ -57,36 +73,62 @@ export class SearchController {
@Post('smart') @Post('smart')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Smart asset search',
description: 'Perform a smart search for assets by using machine learning vectors to determine relevance.',
})
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> { searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
return this.service.searchSmart(auth, dto); return this.service.searchSmart(auth, dto);
} }
@Get('explore') @Get('explore')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ApiOperation({
summary: 'Retrieve explore data',
description: 'Retrieve data for the explore section, such as popular people and places.',
})
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> { getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
return this.service.getExploreData(auth); return this.service.getExploreData(auth);
} }
@Get('person') @Get('person')
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ApiOperation({
summary: 'Search people',
description: 'Search for people by name.',
})
searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> { searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.service.searchPerson(auth, dto); return this.service.searchPerson(auth, dto);
} }
@Get('places') @Get('places')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ApiOperation({
summary: 'Search places',
description: 'Search for places by name.',
})
searchPlaces(@Query() dto: SearchPlacesDto): Promise<PlacesResponseDto[]> { searchPlaces(@Query() dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
return this.service.searchPlaces(dto); return this.service.searchPlaces(dto);
} }
@Get('cities') @Get('cities')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ApiOperation({
summary: 'Retrieve assets by city',
description:
'Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.',
})
getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> { getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> {
return this.service.getAssetsByCity(auth); return this.service.getAssetsByCity(auth);
} }
@Get('suggestions') @Get('suggestions')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ApiOperation({
summary: 'Retrieve search suggestions',
description:
'Retrieve search suggestions based on partial input. This endpoint is used for typeahead search features.',
})
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> { getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
// TODO fix open api generation to indicate that results can be nullable // TODO fix open api generation to indicate that results can be nullable
return this.service.getSearchSuggestions(auth, dto) as Promise<string[]>; return this.service.getSearchSuggestions(auth, dto) as Promise<string[]>;

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Put } from '@nestjs/common';
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; import { ApiNotFoundResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import { import {
ServerAboutResponseDto, ServerAboutResponseDto,
@ -15,13 +15,13 @@ import {
ServerVersionResponseDto, ServerVersionResponseDto,
} from 'src/dtos/server.dto'; } from 'src/dtos/server.dto';
import { VersionCheckStateResponseDto } from 'src/dtos/system-metadata.dto'; import { VersionCheckStateResponseDto } from 'src/dtos/system-metadata.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { ServerService } from 'src/services/server.service'; import { ServerService } from 'src/services/server.service';
import { SystemMetadataService } from 'src/services/system-metadata.service'; import { SystemMetadataService } from 'src/services/system-metadata.service';
import { VersionService } from 'src/services/version.service'; import { VersionService } from 'src/services/version.service';
@ApiTags('Server') @ApiTags(ApiTag.Server)
@Controller('server') @Controller('server')
export class ServerController { export class ServerController {
constructor( constructor(
@ -32,59 +32,85 @@ export class ServerController {
@Get('about') @Get('about')
@Authenticated({ permission: Permission.ServerAbout }) @Authenticated({ permission: Permission.ServerAbout })
@ApiOperation({ summary: 'Get server information', description: 'Retrieve a list of information about the server.' })
getAboutInfo(): Promise<ServerAboutResponseDto> { getAboutInfo(): Promise<ServerAboutResponseDto> {
return this.service.getAboutInfo(); return this.service.getAboutInfo();
} }
@Get('apk-links') @Get('apk-links')
@Authenticated({ permission: Permission.ServerApkLinks }) @Authenticated({ permission: Permission.ServerApkLinks })
@ApiOperation({ summary: 'Get APK links', description: 'Retrieve links to the APKs for the current server version.' })
getApkLinks(): ServerApkLinksDto { getApkLinks(): ServerApkLinksDto {
return this.service.getApkLinks(); return this.service.getApkLinks();
} }
@Get('storage') @Get('storage')
@Authenticated({ permission: Permission.ServerStorage }) @Authenticated({ permission: Permission.ServerStorage })
@ApiOperation({
summary: 'Get storage',
description: 'Retrieve the current storage utilization information of the server.',
})
getStorage(): Promise<ServerStorageResponseDto> { getStorage(): Promise<ServerStorageResponseDto> {
return this.service.getStorage(); return this.service.getStorage();
} }
@Get('ping') @Get('ping')
@ApiOperation({ summary: 'Ping', description: 'Pong' })
pingServer(): ServerPingResponse { pingServer(): ServerPingResponse {
return this.service.ping(); return this.service.ping();
} }
@Get('version') @Get('version')
@ApiOperation({
summary: 'Get server version',
description: 'Retrieve the current server version in semantic versioning (semver) format.',
})
getServerVersion(): ServerVersionResponseDto { getServerVersion(): ServerVersionResponseDto {
return this.versionService.getVersion(); return this.versionService.getVersion();
} }
@Get('version-history') @Get('version-history')
@ApiOperation({
summary: 'Get version history',
description: 'Retrieve a list of past versions the server has been on.',
})
getVersionHistory(): Promise<ServerVersionHistoryResponseDto[]> { getVersionHistory(): Promise<ServerVersionHistoryResponseDto[]> {
return this.versionService.getVersionHistory(); return this.versionService.getVersionHistory();
} }
@Get('features') @Get('features')
@ApiOperation({ summary: 'Get features', description: 'Retrieve available features supported by this server.' })
getServerFeatures(): Promise<ServerFeaturesDto> { getServerFeatures(): Promise<ServerFeaturesDto> {
return this.service.getFeatures(); return this.service.getFeatures();
} }
@Get('theme') @Get('theme')
@ApiOperation({ summary: 'Get theme', description: 'Retrieve the custom CSS, if existent.' })
getTheme(): Promise<ServerThemeDto> { getTheme(): Promise<ServerThemeDto> {
return this.service.getTheme(); return this.service.getTheme();
} }
@Get('config') @Get('config')
@ApiOperation({ summary: 'Get config', description: 'Retrieve the current server configuration.' })
getServerConfig(): Promise<ServerConfigDto> { getServerConfig(): Promise<ServerConfigDto> {
return this.service.getSystemConfig(); return this.service.getSystemConfig();
} }
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.ServerStatistics, admin: true }) @Authenticated({ permission: Permission.ServerStatistics, admin: true })
@ApiOperation({
summary: 'Get statistics',
description: 'Retrieve statistics about the entire Immich instance such as asset counts.',
})
getServerStatistics(): Promise<ServerStatsResponseDto> { getServerStatistics(): Promise<ServerStatsResponseDto> {
return this.service.getStatistics(); return this.service.getStatistics();
} }
@Get('media-types') @Get('media-types')
@ApiOperation({
summary: 'Get supported media types',
description: 'Retrieve all media types supported by the server.',
})
getSupportedMediaTypes(): ServerMediaTypesResponseDto { getSupportedMediaTypes(): ServerMediaTypesResponseDto {
return this.service.getSupportedMediaTypes(); return this.service.getSupportedMediaTypes();
} }
@ -92,12 +118,20 @@ export class ServerController {
@Get('license') @Get('license')
@Authenticated({ permission: Permission.ServerLicenseRead, admin: true }) @Authenticated({ permission: Permission.ServerLicenseRead, admin: true })
@ApiNotFoundResponse() @ApiNotFoundResponse()
@ApiOperation({
summary: 'Get product key',
description: 'Retrieve information about whether the server currently has a product key registered.',
})
getServerLicense(): Promise<LicenseResponseDto> { getServerLicense(): Promise<LicenseResponseDto> {
return this.service.getLicense(); return this.service.getLicense();
} }
@Put('license') @Put('license')
@Authenticated({ permission: Permission.ServerLicenseUpdate, admin: true }) @Authenticated({ permission: Permission.ServerLicenseUpdate, admin: true })
@ApiOperation({
summary: 'Set server product key',
description: 'Validate and set the server product key if successful.',
})
setServerLicense(@Body() license: LicenseKeyDto): Promise<LicenseResponseDto> { setServerLicense(@Body() license: LicenseKeyDto): Promise<LicenseResponseDto> {
return this.service.setLicense(license); return this.service.setLicense(license);
} }
@ -105,12 +139,17 @@ export class ServerController {
@Delete('license') @Delete('license')
@Authenticated({ permission: Permission.ServerLicenseDelete, admin: true }) @Authenticated({ permission: Permission.ServerLicenseDelete, admin: true })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete server product key', description: 'Delete the currently set server product key.' })
deleteServerLicense(): Promise<void> { deleteServerLicense(): Promise<void> {
return this.service.deleteLicense(); return this.service.deleteLicense();
} }
@Get('version-check') @Get('version-check')
@Authenticated({ permission: Permission.ServerVersionCheck }) @Authenticated({ permission: Permission.ServerVersionCheck })
@ApiOperation({
summary: 'Get version check status',
description: 'Retrieve information about the last time the version check ran.',
})
getVersionCheck(): Promise<VersionCheckStateResponseDto> { getVersionCheck(): Promise<VersionCheckStateResponseDto> {
return this.systemMetadataService.getVersionCheckState(); return this.systemMetadataService.getVersionCheckState();
} }

View file

@ -1,25 +1,33 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, SessionUpdateDto } from 'src/dtos/session.dto'; import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, SessionUpdateDto } from 'src/dtos/session.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SessionService } from 'src/services/session.service'; import { SessionService } from 'src/services/session.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Sessions') @ApiTags(ApiTag.Sessions)
@Controller('sessions') @Controller('sessions')
export class SessionController { export class SessionController {
constructor(private service: SessionService) {} constructor(private service: SessionService) {}
@Post() @Post()
@Authenticated({ permission: Permission.SessionCreate }) @Authenticated({ permission: Permission.SessionCreate })
@ApiOperation({
summary: 'Create a session',
description: 'Create a session as a child to the current session. This endpoint is used for casting.',
})
createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise<SessionCreateResponseDto> { createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise<SessionCreateResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.SessionRead }) @Authenticated({ permission: Permission.SessionRead })
@ApiOperation({
summary: 'Retrieve sessions',
description: 'Retrieve a list of sessions for the user.',
})
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> { getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@ -27,12 +35,20 @@ export class SessionController {
@Delete() @Delete()
@Authenticated({ permission: Permission.SessionDelete }) @Authenticated({ permission: Permission.SessionDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete all sessions',
description: 'Delete all sessions for the user. This will not delete the current session.',
})
deleteAllSessions(@Auth() auth: AuthDto): Promise<void> { deleteAllSessions(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteAll(auth); return this.service.deleteAll(auth);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.SessionUpdate }) @Authenticated({ permission: Permission.SessionUpdate })
@ApiOperation({
summary: 'Update a session',
description: 'Update a specific session identified by id.',
})
updateSession( updateSession(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -44,6 +60,10 @@ export class SessionController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.SessionDelete }) @Authenticated({ permission: Permission.SessionDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a session',
description: 'Delete a specific session by id.',
})
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@ -51,6 +71,10 @@ export class SessionController {
@Post(':id/lock') @Post(':id/lock')
@Authenticated({ permission: Permission.SessionLock }) @Authenticated({ permission: Permission.SessionLock })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Lock a session',
description: 'Lock a specific session by id.',
})
lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.lock(auth, id); return this.service.lock(auth, id);
} }

View file

@ -13,7 +13,7 @@ import {
Req, Req,
Res, Res,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto';
@ -25,26 +25,34 @@ import {
SharedLinkResponseDto, SharedLinkResponseDto,
SharedLinkSearchDto, SharedLinkSearchDto,
} from 'src/dtos/shared-link.dto'; } from 'src/dtos/shared-link.dto';
import { ImmichCookie, Permission } from 'src/enum'; import { ApiTag, ImmichCookie, Permission } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { LoginDetails } from 'src/services/auth.service'; import { LoginDetails } from 'src/services/auth.service';
import { SharedLinkService } from 'src/services/shared-link.service'; import { SharedLinkService } from 'src/services/shared-link.service';
import { respondWithCookie } from 'src/utils/response'; import { respondWithCookie } from 'src/utils/response';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Shared Links') @ApiTags(ApiTag.SharedLinks)
@Controller('shared-links') @Controller('shared-links')
export class SharedLinkController { export class SharedLinkController {
constructor(private service: SharedLinkService) {} constructor(private service: SharedLinkService) {}
@Get() @Get()
@Authenticated({ permission: Permission.SharedLinkRead }) @Authenticated({ permission: Permission.SharedLinkRead })
@ApiOperation({
summary: 'Retrieve all shared links',
description: 'Retrieve a list of all shared links.',
})
getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> { getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(auth, dto); return this.service.getAll(auth, dto);
} }
@Get('me') @Get('me')
@Authenticated({ sharedLink: true }) @Authenticated({ sharedLink: true })
@ApiOperation({
summary: 'Retrieve current shared link',
description: 'Retrieve the current shared link associated with authentication method.',
})
async getMySharedLink( async getMySharedLink(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Query() dto: SharedLinkPasswordDto, @Query() dto: SharedLinkPasswordDto,
@ -65,18 +73,30 @@ export class SharedLinkController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.SharedLinkRead }) @Authenticated({ permission: Permission.SharedLinkRead })
@ApiOperation({
summary: 'Retrieve a shared link',
description: 'Retrieve a specific shared link by its ID.',
})
getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> { getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Post() @Post()
@Authenticated({ permission: Permission.SharedLinkCreate }) @Authenticated({ permission: Permission.SharedLinkCreate })
@ApiOperation({
summary: 'Create a shared link',
description: 'Create a new shared link.',
})
createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Patch(':id') @Patch(':id')
@Authenticated({ permission: Permission.SharedLinkUpdate }) @Authenticated({ permission: Permission.SharedLinkUpdate })
@ApiOperation({
summary: 'Update a shared link',
description: 'Update an existing shared link by its ID.',
})
updateSharedLink( updateSharedLink(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -88,12 +108,21 @@ export class SharedLinkController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.SharedLinkDelete }) @Authenticated({ permission: Permission.SharedLinkDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a shared link',
description: 'Delete a specific shared link by its ID.',
})
removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
@Authenticated({ sharedLink: true }) @Authenticated({ sharedLink: true })
@ApiOperation({
summary: 'Add assets to a shared link',
description:
'Add assets to a specific shared link by its ID. This endpoint is only relevant for shared link of type individual.',
})
addSharedLinkAssets( addSharedLinkAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -104,6 +133,11 @@ export class SharedLinkController {
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated({ sharedLink: true }) @Authenticated({ sharedLink: true })
@ApiOperation({
summary: 'Remove assets from a shared link',
description:
'Remove assets from a specific shared link by its ID. This endpoint is only relevant for shared link of type individual.',
})
removeSharedLinkAssets( removeSharedLinkAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,

View file

@ -1,26 +1,35 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto } from 'src/dtos/stack.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto } from 'src/dtos/stack.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { StackService } from 'src/services/stack.service'; import { StackService } from 'src/services/stack.service';
import { UUIDAssetIDParamDto, UUIDParamDto } from 'src/validation'; import { UUIDAssetIDParamDto, UUIDParamDto } from 'src/validation';
@ApiTags('Stacks') @ApiTags(ApiTag.Stacks)
@Controller('stacks') @Controller('stacks')
export class StackController { export class StackController {
constructor(private service: StackService) {} constructor(private service: StackService) {}
@Get() @Get()
@Authenticated({ permission: Permission.StackRead }) @Authenticated({ permission: Permission.StackRead })
@ApiOperation({
summary: 'Retrieve stacks',
description: 'Retrieve a list of stacks.',
})
searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise<StackResponseDto[]> { searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise<StackResponseDto[]> {
return this.service.search(auth, query); return this.service.search(auth, query);
} }
@Post() @Post()
@Authenticated({ permission: Permission.StackCreate }) @Authenticated({ permission: Permission.StackCreate })
@ApiOperation({
summary: 'Create a stack',
description:
'Create a new stack by providing a name and a list of asset IDs to include in the stack. If any of the provided asset IDs are primary assets of an existing stack, the existing stack will be merged into the newly created stack.',
})
createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise<StackResponseDto> { createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise<StackResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@ -28,18 +37,30 @@ export class StackController {
@Delete() @Delete()
@Authenticated({ permission: Permission.StackDelete }) @Authenticated({ permission: Permission.StackDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete stacks',
description: 'Delete multiple stacks by providing a list of stack IDs.',
})
deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.StackRead }) @Authenticated({ permission: Permission.StackRead })
@ApiOperation({
summary: 'Retrieve a stack',
description: 'Retrieve a specific stack by its ID.',
})
getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<StackResponseDto> { getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<StackResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.StackUpdate }) @Authenticated({ permission: Permission.StackUpdate })
@ApiOperation({
summary: 'Update a stack',
description: 'Update an existing stack by its ID.',
})
updateStack( updateStack(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -51,6 +72,10 @@ export class StackController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.StackDelete }) @Authenticated({ permission: Permission.StackDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a stack',
description: 'Delete a specific stack by its ID.',
})
deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@ -58,6 +83,10 @@ export class StackController {
@Delete(':id/assets/:assetId') @Delete(':id/assets/:assetId')
@Authenticated({ permission: Permission.StackUpdate }) @Authenticated({ permission: Permission.StackUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Remove an asset from a stack',
description: 'Remove a specific asset from a stack by providing the stack ID and asset ID.',
})
removeAssetFromStack(@Auth() auth: AuthDto, @Param() dto: UUIDAssetIDParamDto): Promise<void> { removeAssetFromStack(@Auth() auth: AuthDto, @Param() dto: UUIDAssetIDParamDto): Promise<void> {
return this.service.removeAsset(auth, dto); return this.service.removeAsset(auth, dto);
} }

View file

@ -1,6 +1,7 @@
import { Body, Controller, Delete, Get, Header, HttpCode, HttpStatus, Post, Res } from '@nestjs/common'; import { Body, Controller, Delete, Get, Header, HttpCode, HttpStatus, Post, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Response } from 'express'; import { Response } from 'express';
import { EndpointLifecycle } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
@ -12,12 +13,12 @@ import {
SyncAckSetDto, SyncAckSetDto,
SyncStreamDto, SyncStreamDto,
} from 'src/dtos/sync.dto'; } from 'src/dtos/sync.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
import { SyncService } from 'src/services/sync.service'; import { SyncService } from 'src/services/sync.service';
@ApiTags('Sync') @ApiTags(ApiTag.Sync)
@Controller('sync') @Controller('sync')
export class SyncController { export class SyncController {
constructor( constructor(
@ -28,6 +29,11 @@ export class SyncController {
@Post('full-sync') @Post('full-sync')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@EndpointLifecycle({
deprecatedAt: 'v2.0.0',
summary: 'Get full sync for user',
description: 'Retrieve all assets for a full synchronization for the authenticated user.',
})
getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> { getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
return this.service.getFullSync(auth, dto); return this.service.getFullSync(auth, dto);
} }
@ -35,6 +41,11 @@ export class SyncController {
@Post('delta-sync') @Post('delta-sync')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@EndpointLifecycle({
deprecatedAt: 'v2.0.0',
summary: 'Get delta sync for user',
description: 'Retrieve changed assets since the last sync for the authenticated user.',
})
getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> { getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
return this.service.getDeltaSync(auth, dto); return this.service.getDeltaSync(auth, dto);
} }
@ -43,6 +54,11 @@ export class SyncController {
@Authenticated({ permission: Permission.SyncStream }) @Authenticated({ permission: Permission.SyncStream })
@Header('Content-Type', 'application/jsonlines+json') @Header('Content-Type', 'application/jsonlines+json')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Stream sync changes',
description:
'Retrieve a JSON lines streamed response of changes for synchronization. This endpoint is used by the mobile app to efficiently stay up to date with changes.',
})
async getSyncStream(@Auth() auth: AuthDto, @Res() res: Response, @Body() dto: SyncStreamDto) { async getSyncStream(@Auth() auth: AuthDto, @Res() res: Response, @Body() dto: SyncStreamDto) {
try { try {
await this.service.stream(auth, res, dto); await this.service.stream(auth, res, dto);
@ -54,6 +70,10 @@ export class SyncController {
@Get('ack') @Get('ack')
@Authenticated({ permission: Permission.SyncCheckpointRead }) @Authenticated({ permission: Permission.SyncCheckpointRead })
@ApiOperation({
summary: 'Retrieve acknowledgements',
description: 'Retrieve the synchronization acknowledgments for the current session.',
})
getSyncAck(@Auth() auth: AuthDto): Promise<SyncAckDto[]> { getSyncAck(@Auth() auth: AuthDto): Promise<SyncAckDto[]> {
return this.service.getAcks(auth); return this.service.getAcks(auth);
} }
@ -61,6 +81,11 @@ export class SyncController {
@Post('ack') @Post('ack')
@Authenticated({ permission: Permission.SyncCheckpointUpdate }) @Authenticated({ permission: Permission.SyncCheckpointUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Acknowledge changes',
description:
'Send a list of synchronization acknowledgements to confirm that the latest changes have been received.',
})
sendSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckSetDto) { sendSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckSetDto) {
return this.service.setAcks(auth, dto); return this.service.setAcks(auth, dto);
} }
@ -68,6 +93,10 @@ export class SyncController {
@Delete('ack') @Delete('ack')
@Authenticated({ permission: Permission.SyncCheckpointDelete }) @Authenticated({ permission: Permission.SyncCheckpointDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete acknowledgements',
description: 'Delete specific synchronization acknowledgments.',
})
deleteSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckDeleteDto): Promise<void> { deleteSyncAck(@Auth() auth: AuthDto, @Body() dto: SyncAckDeleteDto): Promise<void> {
return this.service.deleteAcks(auth, dto); return this.service.deleteAcks(auth, dto);
} }

View file

@ -1,12 +1,12 @@
import { Body, Controller, Get, Put } from '@nestjs/common'; import { Body, Controller, Get, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageTemplateService } from 'src/services/storage-template.service';
import { SystemConfigService } from 'src/services/system-config.service'; import { SystemConfigService } from 'src/services/system-config.service';
@ApiTags('System Config') @ApiTags(ApiTag.SystemConfig)
@Controller('system-config') @Controller('system-config')
export class SystemConfigController { export class SystemConfigController {
constructor( constructor(
@ -16,24 +16,37 @@ export class SystemConfigController {
@Get() @Get()
@Authenticated({ permission: Permission.SystemConfigRead, admin: true }) @Authenticated({ permission: Permission.SystemConfigRead, admin: true })
@ApiOperation({ summary: 'Get system configuration', description: 'Retrieve the current system configuration.' })
getConfig(): Promise<SystemConfigDto> { getConfig(): Promise<SystemConfigDto> {
return this.service.getSystemConfig(); return this.service.getSystemConfig();
} }
@Get('defaults') @Get('defaults')
@Authenticated({ permission: Permission.SystemConfigRead, admin: true }) @Authenticated({ permission: Permission.SystemConfigRead, admin: true })
@ApiOperation({
summary: 'Get system configuration defaults',
description: 'Retrieve the default values for the system configuration.',
})
getConfigDefaults(): SystemConfigDto { getConfigDefaults(): SystemConfigDto {
return this.service.getDefaults(); return this.service.getDefaults();
} }
@Put() @Put()
@Authenticated({ permission: Permission.SystemConfigUpdate, admin: true }) @Authenticated({ permission: Permission.SystemConfigUpdate, admin: true })
@ApiOperation({
summary: 'Update system configuration',
description: 'Update the system configuration with a new system configuration.',
})
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> { updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
return this.service.updateSystemConfig(dto); return this.service.updateSystemConfig(dto);
} }
@Get('storage-template-options') @Get('storage-template-options')
@Authenticated({ permission: Permission.SystemConfigRead, admin: true }) @Authenticated({ permission: Permission.SystemConfigRead, admin: true })
@ApiOperation({
summary: 'Get storage template options',
description: 'Retrieve exemplary storage template options.',
})
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
return this.storageTemplateService.getStorageTemplateOptions(); return this.storageTemplateService.getStorageTemplateOptions();
} }

View file

@ -1,21 +1,25 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { import {
AdminOnboardingUpdateDto, AdminOnboardingUpdateDto,
ReverseGeocodingStateResponseDto, ReverseGeocodingStateResponseDto,
VersionCheckStateResponseDto, VersionCheckStateResponseDto,
} from 'src/dtos/system-metadata.dto'; } from 'src/dtos/system-metadata.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { SystemMetadataService } from 'src/services/system-metadata.service'; import { SystemMetadataService } from 'src/services/system-metadata.service';
@ApiTags('System Metadata') @ApiTags(ApiTag.SystemMetadata)
@Controller('system-metadata') @Controller('system-metadata')
export class SystemMetadataController { export class SystemMetadataController {
constructor(private service: SystemMetadataService) {} constructor(private service: SystemMetadataService) {}
@Get('admin-onboarding') @Get('admin-onboarding')
@Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) @Authenticated({ permission: Permission.SystemMetadataRead, admin: true })
@ApiOperation({
summary: 'Retrieve admin onboarding',
description: 'Retrieve the current admin onboarding status.',
})
getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> { getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
return this.service.getAdminOnboarding(); return this.service.getAdminOnboarding();
} }
@ -23,18 +27,30 @@ export class SystemMetadataController {
@Post('admin-onboarding') @Post('admin-onboarding')
@Authenticated({ permission: Permission.SystemMetadataUpdate, admin: true }) @Authenticated({ permission: Permission.SystemMetadataUpdate, admin: true })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Update admin onboarding',
description: 'Update the admin onboarding status.',
})
updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> { updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
return this.service.updateAdminOnboarding(dto); return this.service.updateAdminOnboarding(dto);
} }
@Get('reverse-geocoding-state') @Get('reverse-geocoding-state')
@Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) @Authenticated({ permission: Permission.SystemMetadataRead, admin: true })
@ApiOperation({
summary: 'Retrieve reverse geocoding state',
description: 'Retrieve the current state of the reverse geocoding import.',
})
getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> { getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
return this.service.getReverseGeocodingState(); return this.service.getReverseGeocodingState();
} }
@Get('version-check-state') @Get('version-check-state')
@Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) @Authenticated({ permission: Permission.SystemMetadataRead, admin: true })
@ApiOperation({
summary: 'Retrieve version check state',
description: 'Retrieve the current state of the version check process.',
})
getVersionCheckState(): Promise<VersionCheckStateResponseDto> { getVersionCheckState(): Promise<VersionCheckStateResponseDto> {
return this.service.getVersionCheckState(); return this.service.getVersionCheckState();
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
@ -10,48 +10,72 @@ import {
TagUpdateDto, TagUpdateDto,
TagUpsertDto, TagUpsertDto,
} from 'src/dtos/tag.dto'; } from 'src/dtos/tag.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TagService } from 'src/services/tag.service'; import { TagService } from 'src/services/tag.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Tags') @ApiTags(ApiTag.Tags)
@Controller('tags') @Controller('tags')
export class TagController { export class TagController {
constructor(private service: TagService) {} constructor(private service: TagService) {}
@Post() @Post()
@Authenticated({ permission: Permission.TagCreate }) @Authenticated({ permission: Permission.TagCreate })
@ApiOperation({
summary: 'Create a tag',
description: 'Create a new tag by providing a name and optional color.',
})
createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise<TagResponseDto> { createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise<TagResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.TagRead }) @Authenticated({ permission: Permission.TagRead })
@ApiOperation({
summary: 'Retrieve tags',
description: 'Retrieve a list of all tags.',
})
getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> { getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Put() @Put()
@Authenticated({ permission: Permission.TagCreate }) @Authenticated({ permission: Permission.TagCreate })
@ApiOperation({
summary: 'Upsert tags',
description: 'Create or update multiple tags in a single request.',
})
upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise<TagResponseDto[]> { upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise<TagResponseDto[]> {
return this.service.upsert(auth, dto); return this.service.upsert(auth, dto);
} }
@Put('assets') @Put('assets')
@Authenticated({ permission: Permission.TagAsset }) @Authenticated({ permission: Permission.TagAsset })
@ApiOperation({
summary: 'Tag assets',
description: 'Add multiple tags to multiple assets in a single request.',
})
bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> { bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
return this.service.bulkTagAssets(auth, dto); return this.service.bulkTagAssets(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.TagRead }) @Authenticated({ permission: Permission.TagRead })
@ApiOperation({
summary: 'Retrieve a tag',
description: 'Retrieve a specific tag by its ID.',
})
getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> { getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.TagUpdate }) @Authenticated({ permission: Permission.TagUpdate })
@ApiOperation({
summary: 'Update a tag',
description: 'Update an existing tag identified by its ID.',
})
updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise<TagResponseDto> { updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise<TagResponseDto> {
return this.service.update(auth, id, dto); return this.service.update(auth, id, dto);
} }
@ -59,12 +83,20 @@ export class TagController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.TagDelete }) @Authenticated({ permission: Permission.TagDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete a tag',
description: 'Delete a specific tag by its ID.',
})
deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
@Authenticated({ permission: Permission.TagAsset }) @Authenticated({ permission: Permission.TagAsset })
@ApiOperation({
summary: 'Tag assets',
description: 'Add a tag to all the specified assets.',
})
tagAssets( tagAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -75,6 +107,10 @@ export class TagController {
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated({ permission: Permission.TagAsset }) @Authenticated({ permission: Permission.TagAsset })
@ApiOperation({
summary: 'Untag assets',
description: 'Remove a tag from all the specified assets.',
})
untagAssets( untagAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: BulkIdsDto, @Body() dto: BulkIdsDto,

View file

@ -1,18 +1,19 @@
import { Controller, Get, Header, Query } from '@nestjs/common'; import { Controller, Get, Header, Query } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketAssetResponseDto, TimeBucketDto } from 'src/dtos/time-bucket.dto'; import { TimeBucketAssetDto, TimeBucketAssetResponseDto, TimeBucketDto } from 'src/dtos/time-bucket.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TimelineService } from 'src/services/timeline.service'; import { TimelineService } from 'src/services/timeline.service';
@ApiTags('Timeline') @ApiTags(ApiTag.Timeline)
@Controller('timeline') @Controller('timeline')
export class TimelineController { export class TimelineController {
constructor(private service: TimelineService) {} constructor(private service: TimelineService) {}
@Get('buckets') @Get('buckets')
@Authenticated({ permission: Permission.AssetRead, sharedLink: true }) @Authenticated({ permission: Permission.AssetRead, sharedLink: true })
@ApiOperation({ summary: 'Get time buckets', description: 'Retrieve a list of all minimal time buckets.' })
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) { getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) {
return this.service.getTimeBuckets(auth, dto); return this.service.getTimeBuckets(auth, dto);
} }
@ -21,6 +22,10 @@ export class TimelineController {
@Authenticated({ permission: Permission.AssetRead, sharedLink: true }) @Authenticated({ permission: Permission.AssetRead, sharedLink: true })
@ApiOkResponse({ type: TimeBucketAssetResponseDto }) @ApiOkResponse({ type: TimeBucketAssetResponseDto })
@Header('Content-Type', 'application/json') @Header('Content-Type', 'application/json')
@ApiOperation({
summary: 'Get time bucket',
description: 'Retrieve a string of all asset ids in a given time bucket.',
})
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) { getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) {
return this.service.getTimeBucket(auth, dto); return this.service.getTimeBucket(auth, dto);
} }

View file

@ -1,13 +1,13 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TrashResponseDto } from 'src/dtos/trash.dto'; import { TrashResponseDto } from 'src/dtos/trash.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TrashService } from 'src/services/trash.service'; import { TrashService } from 'src/services/trash.service';
@ApiTags('Trash') @ApiTags(ApiTag.Trash)
@Controller('trash') @Controller('trash')
export class TrashController { export class TrashController {
constructor(private service: TrashService) {} constructor(private service: TrashService) {}
@ -15,6 +15,10 @@ export class TrashController {
@Post('empty') @Post('empty')
@Authenticated({ permission: Permission.AssetDelete }) @Authenticated({ permission: Permission.AssetDelete })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Empty trash',
description: 'Permanently delete all items in the trash.',
})
emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> { emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
return this.service.empty(auth); return this.service.empty(auth);
} }
@ -22,6 +26,10 @@ export class TrashController {
@Post('restore') @Post('restore')
@Authenticated({ permission: Permission.AssetDelete }) @Authenticated({ permission: Permission.AssetDelete })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Restore trash',
description: 'Restore all items in the trash.',
})
restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> { restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
return this.service.restore(auth); return this.service.restore(auth);
} }
@ -29,6 +37,10 @@ export class TrashController {
@Post('restore/assets') @Post('restore/assets')
@Authenticated({ permission: Permission.AssetDelete }) @Authenticated({ permission: Permission.AssetDelete })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Restore assets',
description: 'Restore specific assets from the trash.',
})
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> { restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> {
return this.service.restoreAssets(auth, dto); return this.service.restoreAssets(auth, dto);
} }

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto } from 'src/dtos/session.dto'; import { SessionResponseDto } from 'src/dtos/session.dto';
@ -11,36 +11,52 @@ import {
UserAdminSearchDto, UserAdminSearchDto,
UserAdminUpdateDto, UserAdminUpdateDto,
} from 'src/dtos/user.dto'; } from 'src/dtos/user.dto';
import { Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { UserAdminService } from 'src/services/user-admin.service'; import { UserAdminService } from 'src/services/user-admin.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Users (admin)') @ApiTags(ApiTag.UsersAdmin)
@Controller('admin/users') @Controller('admin/users')
export class UserAdminController { export class UserAdminController {
constructor(private service: UserAdminService) {} constructor(private service: UserAdminService) {}
@Get() @Get()
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ApiOperation({
summary: 'Search users',
description: 'Search for users.',
})
searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> { searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post() @Post()
@Authenticated({ permission: Permission.AdminUserCreate, admin: true }) @Authenticated({ permission: Permission.AdminUserCreate, admin: true })
@ApiOperation({
summary: 'Create a user',
description: 'Create a new user.',
})
createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> { createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
return this.service.create(createUserDto); return this.service.create(createUserDto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ApiOperation({
summary: 'Retrieve a user',
description: 'Retrieve a specific user by their ID.',
})
getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> { getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) @Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
@ApiOperation({
summary: 'Update a user',
description: 'Update an existing user.',
})
updateUserAdmin( updateUserAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -51,6 +67,10 @@ export class UserAdminController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@ApiOperation({
summary: 'Delete a user',
description: 'Delete a user.',
})
deleteUserAdmin( deleteUserAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -61,12 +81,20 @@ export class UserAdminController {
@Get(':id/sessions') @Get(':id/sessions')
@Authenticated({ permission: Permission.AdminSessionRead, admin: true }) @Authenticated({ permission: Permission.AdminSessionRead, admin: true })
@ApiOperation({
summary: 'Retrieve user sessions',
description: 'Retrieve all sessions for a specific user.',
})
getUserSessionsAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SessionResponseDto[]> { getUserSessionsAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SessionResponseDto[]> {
return this.service.getSessions(auth, id); return this.service.getSessions(auth, id);
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ApiOperation({
summary: 'Retrieve user statistics',
description: 'Retrieve asset statistics for a specific user.',
})
getUserStatisticsAdmin( getUserStatisticsAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -77,12 +105,20 @@ export class UserAdminController {
@Get(':id/preferences') @Get(':id/preferences')
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ApiOperation({
summary: 'Retrieve user preferences',
description: 'Retrieve the preferences of a specific user.',
})
getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> { getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> {
return this.service.getPreferences(auth, id); return this.service.getPreferences(auth, id);
} }
@Put(':id/preferences') @Put(':id/preferences')
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) @Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
@ApiOperation({
summary: 'Update user preferences',
description: 'Update the preferences of a specific user.',
})
updateUserPreferencesAdmin( updateUserPreferencesAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -94,6 +130,10 @@ export class UserAdminController {
@Post(':id/restore') @Post(':id/restore')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Restore a deleted user',
description: 'Restore a previously deleted user.',
})
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> { restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.restore(auth, id); return this.service.restore(auth, id);
} }

View file

@ -13,7 +13,7 @@ import {
UploadedFile, UploadedFile,
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
@ -21,7 +21,7 @@ import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
import { Permission, RouteKey } from 'src/enum'; import { ApiTag, Permission, RouteKey } from 'src/enum';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
@ -29,7 +29,7 @@ import { UserService } from 'src/services/user.service';
import { sendFile } from 'src/utils/file'; import { sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Users') @ApiTags(ApiTag.Users)
@Controller(RouteKey.User) @Controller(RouteKey.User)
export class UserController { export class UserController {
constructor( constructor(
@ -39,30 +39,38 @@ export class UserController {
@Get() @Get()
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ApiOperation({ summary: 'Get all users', description: 'Retrieve a list of all users on the server.' })
searchUsers(@Auth() auth: AuthDto): Promise<UserResponseDto[]> { searchUsers(@Auth() auth: AuthDto): Promise<UserResponseDto[]> {
return this.service.search(auth); return this.service.search(auth);
} }
@Get('me') @Get('me')
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ApiOperation({
summary: 'Get current user',
description: 'Retrieve information about the user making the API request.',
})
getMyUser(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> { getMyUser(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
return this.service.getMe(auth); return this.service.getMe(auth);
} }
@Put('me') @Put('me')
@Authenticated({ permission: Permission.UserUpdate }) @Authenticated({ permission: Permission.UserUpdate })
@ApiOperation({ summary: 'Update current user', description: 'Update the current user making teh API request.' })
updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise<UserAdminResponseDto> { updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
return this.service.updateMe(auth, dto); return this.service.updateMe(auth, dto);
} }
@Get('me/preferences') @Get('me/preferences')
@Authenticated({ permission: Permission.UserPreferenceRead }) @Authenticated({ permission: Permission.UserPreferenceRead })
@ApiOperation({ summary: 'Get my preferences', description: 'Retrieve the preferences for the current user.' })
getMyPreferences(@Auth() auth: AuthDto): Promise<UserPreferencesResponseDto> { getMyPreferences(@Auth() auth: AuthDto): Promise<UserPreferencesResponseDto> {
return this.service.getMyPreferences(auth); return this.service.getMyPreferences(auth);
} }
@Put('me/preferences') @Put('me/preferences')
@Authenticated({ permission: Permission.UserPreferenceUpdate }) @Authenticated({ permission: Permission.UserPreferenceUpdate })
@ApiOperation({ summary: 'Update my preferences', description: 'Update the preferences of the current user.' })
updateMyPreferences( updateMyPreferences(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: UserPreferencesUpdateDto, @Body() dto: UserPreferencesUpdateDto,
@ -72,12 +80,20 @@ export class UserController {
@Get('me/license') @Get('me/license')
@Authenticated({ permission: Permission.UserLicenseRead }) @Authenticated({ permission: Permission.UserLicenseRead })
@ApiOperation({
summary: 'Retrieve user product key',
description: 'Retrieve information about whether the current user has a registered product key.',
})
getUserLicense(@Auth() auth: AuthDto): Promise<LicenseResponseDto> { getUserLicense(@Auth() auth: AuthDto): Promise<LicenseResponseDto> {
return this.service.getLicense(auth); return this.service.getLicense(auth);
} }
@Put('me/license') @Put('me/license')
@Authenticated({ permission: Permission.UserLicenseUpdate }) @Authenticated({ permission: Permission.UserLicenseUpdate })
@ApiOperation({
summary: 'Set user product key',
description: 'Register a product key for the current user.',
})
async setUserLicense(@Auth() auth: AuthDto, @Body() license: LicenseKeyDto): Promise<LicenseResponseDto> { async setUserLicense(@Auth() auth: AuthDto, @Body() license: LicenseKeyDto): Promise<LicenseResponseDto> {
return this.service.setLicense(auth, license); return this.service.setLicense(auth, license);
} }
@ -85,18 +101,30 @@ export class UserController {
@Delete('me/license') @Delete('me/license')
@Authenticated({ permission: Permission.UserLicenseDelete }) @Authenticated({ permission: Permission.UserLicenseDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete user product key',
description: 'Delete the registered product key for the current user.',
})
async deleteUserLicense(@Auth() auth: AuthDto): Promise<void> { async deleteUserLicense(@Auth() auth: AuthDto): Promise<void> {
await this.service.deleteLicense(auth); await this.service.deleteLicense(auth);
} }
@Get('me/onboarding') @Get('me/onboarding')
@Authenticated({ permission: Permission.UserOnboardingRead }) @Authenticated({ permission: Permission.UserOnboardingRead })
@ApiOperation({
summary: 'Retrieve user onboarding',
description: 'Retrieve the onboarding status of the current user.',
})
getUserOnboarding(@Auth() auth: AuthDto): Promise<OnboardingResponseDto> { getUserOnboarding(@Auth() auth: AuthDto): Promise<OnboardingResponseDto> {
return this.service.getOnboarding(auth); return this.service.getOnboarding(auth);
} }
@Put('me/onboarding') @Put('me/onboarding')
@Authenticated({ permission: Permission.UserOnboardingUpdate }) @Authenticated({ permission: Permission.UserOnboardingUpdate })
@ApiOperation({
summary: 'Update user onboarding',
description: 'Update the onboarding status of the current user.',
})
async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise<OnboardingResponseDto> { async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise<OnboardingResponseDto> {
return this.service.setOnboarding(auth, Onboarding); return this.service.setOnboarding(auth, Onboarding);
} }
@ -104,12 +132,20 @@ export class UserController {
@Delete('me/onboarding') @Delete('me/onboarding')
@Authenticated({ permission: Permission.UserOnboardingDelete }) @Authenticated({ permission: Permission.UserOnboardingDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete user onboarding',
description: 'Delete the onboarding status of the current user.',
})
async deleteUserOnboarding(@Auth() auth: AuthDto): Promise<void> { async deleteUserOnboarding(@Auth() auth: AuthDto): Promise<void> {
await this.service.deleteOnboarding(auth); await this.service.deleteOnboarding(auth);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ApiOperation({
summary: 'Retrieve a user',
description: 'Retrieve a specific user by their ID.',
})
getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> { getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@ -119,6 +155,10 @@ export class UserController {
@UseInterceptors(FileUploadInterceptor) @UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto })
@ApiOperation({
summary: 'Create user profile image',
description: 'Upload and set a new profile image for the current user.',
})
createProfileImage( createProfileImage(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@UploadedFile() fileInfo: Express.Multer.File, @UploadedFile() fileInfo: Express.Multer.File,
@ -129,6 +169,10 @@ export class UserController {
@Delete('profile-image') @Delete('profile-image')
@Authenticated({ permission: Permission.UserProfileImageDelete }) @Authenticated({ permission: Permission.UserProfileImageDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete user profile image',
description: 'Delete the profile image of the current user.',
})
deleteProfileImage(@Auth() auth: AuthDto): Promise<void> { deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteProfileImage(auth); return this.service.deleteProfileImage(auth);
} }
@ -136,6 +180,10 @@ export class UserController {
@Get(':id/profile-image') @Get(':id/profile-image')
@FileResponse() @FileResponse()
@Authenticated({ permission: Permission.UserProfileImageRead }) @Authenticated({ permission: Permission.UserProfileImageRead })
@ApiOperation({
summary: 'Retrieve user profile image',
description: 'Retrieve the profile image file for a user.',
})
async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) { async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) {
await sendFile(res, next, () => this.service.getProfileImage(id), this.logger); await sendFile(res, next, () => this.service.getProfileImage(id), this.logger);
} }

View file

@ -1,23 +1,32 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ApiTag } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { ViewService } from 'src/services/view.service'; import { ViewService } from 'src/services/view.service';
@ApiTags('View') @ApiTags(ApiTag.Views)
@Controller('view') @Controller('view')
export class ViewController { export class ViewController {
constructor(private service: ViewService) {} constructor(private service: ViewService) {}
@Get('folder/unique-paths') @Get('folder/unique-paths')
@Authenticated() @Authenticated()
@ApiOperation({
summary: 'Retrieve unique paths',
description: 'Retrieve a list of unique folder paths from asset original paths.',
})
getUniqueOriginalPaths(@Auth() auth: AuthDto): Promise<string[]> { getUniqueOriginalPaths(@Auth() auth: AuthDto): Promise<string[]> {
return this.service.getUniqueOriginalPaths(auth); return this.service.getUniqueOriginalPaths(auth);
} }
@Get('folder') @Get('folder')
@Authenticated() @Authenticated()
@ApiOperation({
summary: 'Retrieve assets by original path',
description: 'Retrieve assets that are children of a specific folder.',
})
getAssetsByOriginalPath(@Auth() auth: AuthDto, @Query('path') path: string): Promise<AssetResponseDto[]> { getAssetsByOriginalPath(@Auth() auth: AuthDto, @Query('path') path: string): Promise<AssetResponseDto[]> {
return this.service.getAssetsByOriginalPath(auth, path); return this.service.getAssetsByOriginalPath(auth, path);
} }

View file

@ -2,7 +2,7 @@ import { SetMetadata, applyDecorators } from '@nestjs/common';
import { ApiExtension, ApiOperation, ApiOperationOptions, ApiProperty, ApiTags } from '@nestjs/swagger'; import { ApiExtension, ApiOperation, ApiOperationOptions, ApiProperty, ApiTags } from '@nestjs/swagger';
import _ from 'lodash'; import _ from 'lodash';
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants'; import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum'; import { ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
import { EmitEvent } from 'src/repositories/event.repository'; import { EmitEvent } from 'src/repositories/event.repository';
import { immich_uuid_v7, updated_at } from 'src/schema/functions'; import { immich_uuid_v7, updated_at } from 'src/schema/functions';
import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools'; import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools';
@ -168,7 +168,7 @@ export const EndpointLifecycle = ({
const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })]; const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })];
if (deprecatedAt) { if (deprecatedAt) {
decorators.push( decorators.push(
ApiTags('Deprecated'), ApiTags(ApiTag.Deprecated),
ApiOperation({ ApiOperation({
deprecated: true, deprecated: true,
description: DEPRECATED_IN_PREFIX + deprecatedAt + (description ? `. ${description}` : ''), description: DEPRECATED_IN_PREFIX + deprecatedAt + (description ? `. ${description}` : ''),

View file

@ -771,3 +771,38 @@ export enum CronJob {
LibraryScan = 'LibraryScan', LibraryScan = 'LibraryScan',
NightlyJobs = 'NightlyJobs', NightlyJobs = 'NightlyJobs',
} }
export enum ApiTag {
Activities = 'Activities',
Albums = 'Albums',
ApiKeys = 'API keys',
Authentication = 'Authentication',
AuthenticationAdmin = 'Authentication (admin)',
Assets = 'Assets',
Deprecated = 'Deprecated',
Download = 'Download',
Duplicates = 'Duplicates',
Faces = 'Faces',
Jobs = 'Jobs',
Libraries = 'Libraries',
Map = 'Map',
Memories = 'Memories',
Notifications = 'Notifications',
NotificationsAdmin = 'Notifications (admin)',
Partners = 'Partners',
People = 'People',
Search = 'Search',
Server = 'Server',
Sessions = 'Sessions',
SharedLinks = 'Shared links',
Stacks = 'Stacks',
Sync = 'Sync',
SystemConfig = 'System config',
SystemMetadata = 'System metadata',
Tags = 'Tags',
Timeline = 'Timeline',
Trash = 'Trash',
UsersAdmin = 'Users (admin)',
Users = 'Users',
Views = 'Views',
}

View file

@ -17,7 +17,7 @@ import path from 'node:path';
import picomatch from 'picomatch'; import picomatch from 'picomatch';
import parse from 'picomatch/lib/parse'; 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, endpointTags, serverVersion } from 'src/constants';
import { extraSyncModels } from 'src/dtos/sync.dto'; import { extraSyncModels } from 'src/dtos/sync.dto';
import { ApiCustomExtension, 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';
@ -218,25 +218,16 @@ const patchOpenAPI = (document: OpenAPIObject) => {
delete operation.summary; delete operation.summary;
} }
if (operation.description === '') {
delete operation.description;
}
if (operation.operationId) { if (operation.operationId) {
// console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`); // console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`);
} }
const adminOnly = operation[ApiCustomExtension.AdminOnly] ?? false; if (operation.parameters) {
const permission = operation[ApiCustomExtension.Permission]; operation.parameters = _.orderBy(operation.parameters, 'name');
if (permission) {
let description = (operation.description || '').trim();
if (description && !description.endsWith('.')) {
description += '. ';
}
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');
}
} }
} }
} }
@ -245,7 +236,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
}; };
export const useSwagger = (app: INestApplication, { write }: { write: boolean }) => { export const useSwagger = (app: INestApplication, { write }: { write: boolean }) => {
const config = new DocumentBuilder() const builder = new DocumentBuilder()
.setTitle('Immich') .setTitle('Immich')
.setDescription('Immich API') .setDescription('Immich API')
.setVersion(serverVersion.toString()) .setVersion(serverVersion.toString())
@ -263,8 +254,12 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean })
}, },
MetadataKey.ApiKeySecurity, MetadataKey.ApiKeySecurity,
) )
.addServer('/api') .addServer('/api');
.build();
for (const [tag, description] of Object.entries(endpointTags)) {
builder.addTag(tag, description);
}
const config = builder.build();
const options: SwaggerDocumentOptions = { const options: SwaggerDocumentOptions = {
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey, operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,