Compare commits

..

2 Commits

Author SHA1 Message Date
Jason Rasmussen
f7285191bd WIP 2024-04-22 16:51:24 -04:00
Aaron Berndsen
c9a079201a docs: update "move all data" instructions in FAQ (#8976)
* Update FAQ.mdx

chore(docs): update "move all data" FAQ instructions.

* Apply suggestions from code review

fix: (apply suggestions) use sql-compliant comments

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>

* fix: Update FAQ.mdx

---------

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>
2024-04-22 12:53:55 +00:00
73 changed files with 1100 additions and 469 deletions

View File

@@ -117,7 +117,7 @@ For example, say you have existing transcodes with the policy "Videos higher tha
No. Our design principle is that the original assets should always be untouched.
### How can I move all data (photos, persons, albums) from one user to another?
### How can I move all data (photos, persons, albums, libraries) from one user to another?
This is not officially supported but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add, for example, an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file so that you can use a web interface.
@@ -128,18 +128,21 @@ This is not officially supported but can be accomplished with some database upda
2. Find the ID of both the 'source' and the 'destination' user (it's the id column in the `users` table)
3. Three tables need to be updated:
3. Four tables need to be updated:
```sql
// Reassign albums
-- reassign albums
UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
// Reassign people
-- reassign people
UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
// reassign assets
-- reassign assets
UPDATE assets SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>'
AND CHECKSUM NOT IN (SELECT CHECKSUM FROM assets WHERE "ownerId" = '<destinationId>');
-- reassign external libraries
UPDATE libraries SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
```
4. There might be left-over assets in the 'source' user's library if they are skipped by the last query because of duplicate checksums. These are probably duplicates anyway, and can probably be removed.

View File

@@ -8,7 +8,7 @@ import {
} from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
import request from 'supertest';
@@ -18,7 +18,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/library', () => {
describe.skip('/library', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
let library: LibraryResponseDto;
@@ -28,7 +28,7 @@ describe('/library', () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
await utils.resetAdminConfig(admin.accessToken);
user = await utils.userSetup(admin.accessToken, userDto.user1);
user = await utils.userSetup(admin.accessToken, createUserDto.user1);
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
websocket = await utils.connectWebsocket(admin.accessToken);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);

View File

@@ -135,7 +135,7 @@ describe('/user', () => {
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of Object.keys(createUserDto.user1)) {
for (const key of ['email', 'password', 'name', 'permissionPreset']) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/user`)
@@ -146,6 +146,17 @@ describe('/user', () => {
});
}
it(`should require permissions when using the custom preset `, async () => {
const { status, body } = await request(app)
.post(`/user`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, permissionPreset: 'custom' });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([expect.stringContaining('each value in permissions must be one of the following')]),
);
});
it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/user`)
@@ -154,6 +165,7 @@ describe('/user', () => {
email: 'user5@immich.cloud',
password: 'password123',
name: 'Immich',
permissionPreset: 'user',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
@@ -172,6 +184,7 @@ describe('/user', () => {
password: 'Password123',
name: 'No Memories',
memoriesEnabled: false,
permissionPreset: 'user',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({

View File

@@ -1,4 +1,4 @@
import { UserAvatarColor } from '@immich/sdk';
import { PermissionPreset, UserAvatarColor } from '@immich/sdk';
export const uuidDto = {
invalid: 'invalid-uuid',
@@ -26,33 +26,39 @@ export const createUserDto = {
email: `${key}@immich.cloud`,
name: `Generated User ${key}`,
password: `password-${key}`,
permissionPreset: PermissionPreset.User,
};
},
user1: {
email: 'user1@immich.cloud',
name: 'User 1',
password: 'password1',
permissionPreset: PermissionPreset.User,
},
user2: {
email: 'user2@immich.cloud',
name: 'User 2',
password: 'password12',
permissionPreset: PermissionPreset.User,
},
user3: {
email: 'user3@immich.cloud',
name: 'User 3',
permissionPreset: PermissionPreset.User,
password: 'password123',
},
user4: {
email: 'user4@immich.cloud',
name: 'User 4',
password: 'password123',
permissionPreset: PermissionPreset.User,
},
userQuota: {
email: 'user-quota@immich.cloud',
name: 'User Quota',
password: 'password-quota',
quotaSizeInBytes: 512,
permissionPreset: PermissionPreset.User,
},
};

View File

@@ -77,6 +77,91 @@ export const signupResponseDto = {
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
status: 'active',
permissions: [
'activity.create',
'activity.read',
'activity.update',
'activity.delete',
'album.create',
'album.read',
'album.update',
'album.delete',
'asset.create',
'asset.read',
'asset.update',
'asset.delete',
'apiKey.create',
'apiKey.read',
'apiKey.update',
'apiKey.delete',
'authDevice.create',
'authDevice.read',
'authDevice.update',
'authDevice.delete',
'face.create',
'face.read',
'face.update',
'face.delete',
'library.create',
'library.read',
'library.update',
'library.delete',
'memory.create',
'memory.read',
'memory.update',
'memory.delete',
'memory.addAsset',
'memory.removeAsset',
'partner.create',
'partner.read',
'partner.update',
'partner.delete',
'person.create',
'person.read',
'person.update',
'person.delete',
'report.create',
'report.read',
'report.update',
'report.delete',
'sharedLink.create',
'sharedLink.read',
'sharedLink.update',
'sharedLink.delete',
'systemConfig.read',
'systemConfig.update',
'systemConfig.delete',
'stack.create',
'stack.read',
'stack.update',
'stack.delete',
'tag.create',
'tag.read',
'tag.update',
'tag.delete',
'user.create',
'user.read',
'user.update',
'user.delete',
'auth.changePassword',
'auth.oauth',
'album.addAsset',
'album.removeAsset',
'album.addUser',
'album.removeUser',
'asset.viewThumb',
'asset.viewPreview',
'asset.viewOriginal',
'asset.upload',
'asset.download',
'job.read',
'job.run',
'map.read',
'user.readSimple',
'user.changePassword',
'server.read',
'server.setup',
],
},
};

View File

@@ -6428,15 +6428,6 @@
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
},
{
"bearer": []
},
@@ -6564,15 +6555,6 @@
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
},
{
"bearer": []
},
@@ -7975,6 +7957,103 @@
],
"type": "object"
},
"AuthorizationPermission": {
"enum": [
"activity.create",
"activity.read",
"activity.update",
"activity.delete",
"album.create",
"album.read",
"album.update",
"album.delete",
"asset.create",
"asset.read",
"asset.update",
"asset.delete",
"apiKey.create",
"apiKey.read",
"apiKey.update",
"apiKey.delete",
"authDevice.create",
"authDevice.read",
"authDevice.update",
"authDevice.delete",
"face.create",
"face.read",
"face.update",
"face.delete",
"library.create",
"library.read",
"library.update",
"library.delete",
"memory.create",
"memory.read",
"memory.update",
"memory.delete",
"memory.addAsset",
"memory.removeAsset",
"partner.create",
"partner.read",
"partner.update",
"partner.delete",
"person.create",
"person.read",
"person.update",
"person.delete",
"report.create",
"report.read",
"report.update",
"report.delete",
"session.create",
"session.read",
"session.update",
"session.delete",
"sharedLink.create",
"sharedLink.read",
"sharedLink.update",
"sharedLink.delete",
"systemConfig.create",
"systemConfig.read",
"systemConfig.update",
"systemConfig.delete",
"systemMetadata.create",
"systemMetadata.read",
"systemMetadata.update",
"systemMetadata.delete",
"stack.create",
"stack.read",
"stack.update",
"stack.delete",
"tag.create",
"tag.read",
"tag.update",
"tag.delete",
"user.create",
"user.read",
"user.update",
"user.delete",
"auth.changePassword",
"auth.oauth",
"album.addAsset",
"album.removeAsset",
"album.addUser",
"album.removeUser",
"asset.viewThumb",
"asset.viewPreview",
"asset.viewOriginal",
"asset.upload",
"asset.download",
"job.read",
"job.run",
"map.read",
"user.readSimple",
"user.changePassword",
"server.read",
"server.setup"
],
"type": "string"
},
"BulkIdResponseDto": {
"properties": {
"error": {
@@ -8284,6 +8363,15 @@
"password": {
"type": "string"
},
"permissionPreset": {
"$ref": "#/components/schemas/PermissionPreset"
},
"permissions": {
"items": {
"$ref": "#/components/schemas/AuthorizationPermission"
},
"type": "array"
},
"quotaSizeInBytes": {
"format": "int64",
"nullable": true,
@@ -8300,7 +8388,8 @@
"required": [
"email",
"name",
"password"
"password",
"permissionPreset"
],
"type": "object"
},
@@ -9364,6 +9453,106 @@
"oauthId": {
"type": "string"
},
"permissions": {
"items": {
"enum": [
"activity.create",
"activity.read",
"activity.update",
"activity.delete",
"album.create",
"album.read",
"album.update",
"album.delete",
"asset.create",
"asset.read",
"asset.update",
"asset.delete",
"apiKey.create",
"apiKey.read",
"apiKey.update",
"apiKey.delete",
"authDevice.create",
"authDevice.read",
"authDevice.update",
"authDevice.delete",
"face.create",
"face.read",
"face.update",
"face.delete",
"library.create",
"library.read",
"library.update",
"library.delete",
"memory.create",
"memory.read",
"memory.update",
"memory.delete",
"memory.addAsset",
"memory.removeAsset",
"partner.create",
"partner.read",
"partner.update",
"partner.delete",
"person.create",
"person.read",
"person.update",
"person.delete",
"report.create",
"report.read",
"report.update",
"report.delete",
"session.create",
"session.read",
"session.update",
"session.delete",
"sharedLink.create",
"sharedLink.read",
"sharedLink.update",
"sharedLink.delete",
"systemConfig.create",
"systemConfig.read",
"systemConfig.update",
"systemConfig.delete",
"systemMetadata.create",
"systemMetadata.read",
"systemMetadata.update",
"systemMetadata.delete",
"stack.create",
"stack.read",
"stack.update",
"stack.delete",
"tag.create",
"tag.read",
"tag.update",
"tag.delete",
"user.create",
"user.read",
"user.update",
"user.delete",
"auth.changePassword",
"auth.oauth",
"album.addAsset",
"album.removeAsset",
"album.addUser",
"album.removeUser",
"asset.viewThumb",
"asset.viewPreview",
"asset.viewOriginal",
"asset.upload",
"asset.download",
"job.read",
"job.run",
"map.read",
"user.readSimple",
"user.changePassword",
"server.read",
"server.setup"
],
"type": "string"
},
"type": "array"
},
"profileImagePath": {
"type": "string"
},
@@ -9497,6 +9686,14 @@
],
"type": "object"
},
"PermissionPreset": {
"enum": [
"user",
"admin",
"custom"
],
"type": "string"
},
"PersonCreateDto": {
"properties": {
"birthDate": {
@@ -11252,6 +11449,15 @@
"password": {
"type": "string"
},
"permissionPreset": {
"$ref": "#/components/schemas/PermissionPreset"
},
"permissions": {
"items": {
"$ref": "#/components/schemas/AuthorizationPermission"
},
"type": "array"
},
"quotaSizeInBytes": {
"format": "int64",
"nullable": true,
@@ -11377,6 +11583,106 @@
"oauthId": {
"type": "string"
},
"permissions": {
"items": {
"enum": [
"activity.create",
"activity.read",
"activity.update",
"activity.delete",
"album.create",
"album.read",
"album.update",
"album.delete",
"asset.create",
"asset.read",
"asset.update",
"asset.delete",
"apiKey.create",
"apiKey.read",
"apiKey.update",
"apiKey.delete",
"authDevice.create",
"authDevice.read",
"authDevice.update",
"authDevice.delete",
"face.create",
"face.read",
"face.update",
"face.delete",
"library.create",
"library.read",
"library.update",
"library.delete",
"memory.create",
"memory.read",
"memory.update",
"memory.delete",
"memory.addAsset",
"memory.removeAsset",
"partner.create",
"partner.read",
"partner.update",
"partner.delete",
"person.create",
"person.read",
"person.update",
"person.delete",
"report.create",
"report.read",
"report.update",
"report.delete",
"session.create",
"session.read",
"session.update",
"session.delete",
"sharedLink.create",
"sharedLink.read",
"sharedLink.update",
"sharedLink.delete",
"systemConfig.create",
"systemConfig.read",
"systemConfig.update",
"systemConfig.delete",
"systemMetadata.create",
"systemMetadata.read",
"systemMetadata.update",
"systemMetadata.delete",
"stack.create",
"stack.read",
"stack.update",
"stack.delete",
"tag.create",
"tag.read",
"tag.update",
"tag.delete",
"user.create",
"user.read",
"user.update",
"user.delete",
"auth.changePassword",
"auth.oauth",
"album.addAsset",
"album.removeAsset",
"album.addUser",
"album.removeUser",
"asset.viewThumb",
"asset.viewPreview",
"asset.viewOriginal",
"asset.upload",
"asset.download",
"job.read",
"job.run",
"map.read",
"user.readSimple",
"user.changePassword",
"server.read",
"server.setup"
],
"type": "string"
},
"type": "array"
},
"profileImagePath": {
"type": "string"
},

View File

@@ -71,6 +71,7 @@ export type UserResponseDto = {
memoriesEnabled?: boolean;
name: string;
oauthId: string;
permissions?: ("activity.create" | "activity.read" | "activity.update" | "activity.delete" | "album.create" | "album.read" | "album.update" | "album.delete" | "apiKey.create" | "apiKey.read" | "apiKey.update" | "apiKey.delete" | "asset.create" | "asset.read" | "asset.update" | "asset.delete" | "authDevice.create" | "authDevice.read" | "authDevice.update" | "authDevice.delete" | "face.create" | "face.read" | "face.update" | "face.delete" | "memory.create" | "memory.read" | "memory.update" | "memory.delete" | "partner.create" | "partner.read" | "partner.update" | "partner.delete" | "person.create" | "person.read" | "person.update" | "person.delete" | "sharedLink.create" | "sharedLink.read" | "sharedLink.update" | "sharedLink.delete" | "systemConfig.read" | "systemConfig.update" | "systemConfig.delete" | "stack.create" | "stack.read" | "stack.update" | "stack.delete" | "tag.create" | "tag.read" | "tag.update" | "tag.delete" | "user.create" | "user.readSimple" | "user.read" | "user.update" | "user.delete")[];
profileImagePath: string;
quotaSizeInBytes: number | null;
quotaUsageInBytes: number | null;
@@ -513,6 +514,7 @@ export type PartnerResponseDto = {
memoriesEnabled?: boolean;
name: string;
oauthId: string;
permissions?: ("activity.create" | "activity.read" | "activity.update" | "activity.delete" | "album.create" | "album.read" | "album.update" | "album.delete" | "apiKey.create" | "apiKey.read" | "apiKey.update" | "apiKey.delete" | "asset.create" | "asset.read" | "asset.update" | "asset.delete" | "authDevice.create" | "authDevice.read" | "authDevice.update" | "authDevice.delete" | "face.create" | "face.read" | "face.update" | "face.delete" | "memory.create" | "memory.read" | "memory.update" | "memory.delete" | "partner.create" | "partner.read" | "partner.update" | "partner.delete" | "person.create" | "person.read" | "person.update" | "person.delete" | "sharedLink.create" | "sharedLink.read" | "sharedLink.update" | "sharedLink.delete" | "systemConfig.read" | "systemConfig.update" | "systemConfig.delete" | "stack.create" | "stack.read" | "stack.update" | "stack.delete" | "tag.create" | "tag.read" | "tag.update" | "tag.delete" | "user.create" | "user.readSimple" | "user.read" | "user.update" | "user.delete")[];
profileImagePath: string;
quotaSizeInBytes: number | null;
quotaUsageInBytes: number | null;
@@ -1021,6 +1023,8 @@ export type CreateUserDto = {
memoriesEnabled?: boolean;
name: string;
password: string;
permissionPreset: PermissionPreset;
permissions?: AuthorizationPermission[];
quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean;
storageLabel?: string | null;
@@ -1033,6 +1037,8 @@ export type UpdateUserDto = {
memoriesEnabled?: boolean;
name?: string;
password?: string;
permissionPreset?: PermissionPreset;
permissions?: AuthorizationPermission[];
quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean;
storageLabel?: string;
@@ -3103,3 +3109,66 @@ export enum TimeBucketSize {
Day = "DAY",
Month = "MONTH"
}
export enum PermissionPreset {
User = "user",
Admin = "admin",
Custom = "custom"
}
export enum AuthorizationPermission {
ActivityCreate = "activity.create",
ActivityRead = "activity.read",
ActivityUpdate = "activity.update",
ActivityDelete = "activity.delete",
AlbumCreate = "album.create",
AlbumRead = "album.read",
AlbumUpdate = "album.update",
AlbumDelete = "album.delete",
ApiKeyCreate = "apiKey.create",
ApiKeyRead = "apiKey.read",
ApiKeyUpdate = "apiKey.update",
ApiKeyDelete = "apiKey.delete",
AssetCreate = "asset.create",
AssetRead = "asset.read",
AssetUpdate = "asset.update",
AssetDelete = "asset.delete",
AuthDeviceCreate = "authDevice.create",
AuthDeviceRead = "authDevice.read",
AuthDeviceUpdate = "authDevice.update",
AuthDeviceDelete = "authDevice.delete",
FaceCreate = "face.create",
FaceRead = "face.read",
FaceUpdate = "face.update",
FaceDelete = "face.delete",
MemoryCreate = "memory.create",
MemoryRead = "memory.read",
MemoryUpdate = "memory.update",
MemoryDelete = "memory.delete",
PartnerCreate = "partner.create",
PartnerRead = "partner.read",
PartnerUpdate = "partner.update",
PartnerDelete = "partner.delete",
PersonCreate = "person.create",
PersonRead = "person.read",
PersonUpdate = "person.update",
PersonDelete = "person.delete",
SharedLinkCreate = "sharedLink.create",
SharedLinkRead = "sharedLink.read",
SharedLinkUpdate = "sharedLink.update",
SharedLinkDelete = "sharedLink.delete",
SystemConfigRead = "systemConfig.read",
SystemConfigUpdate = "systemConfig.update",
SystemConfigDelete = "systemConfig.delete",
StackCreate = "stack.create",
StackRead = "stack.read",
StackUpdate = "stack.update",
StackDelete = "stack.delete",
TagCreate = "tag.create",
TagRead = "tag.read",
TagUpdate = "tag.update",
TagDelete = "tag.delete",
UserCreate = "user.create",
UserReadSimple = "user.readSimple",
UserRead = "user.read",
UserUpdate = "user.update",
UserDelete = "user.delete"
}

View File

@@ -8,28 +8,30 @@ import {
ActivitySearchDto,
ActivityStatisticsResponseDto,
} from 'src/dtos/activity.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { ActivityService } from 'src/services/activity.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Activity')
@Controller('activity')
@Authenticated()
export class ActivityController {
constructor(private service: ActivityService) {}
@Get()
@Authenticated(Permission.ACTIVITY_READ)
getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
return this.service.getAll(auth, dto);
}
@Get('statistics')
@Authenticated(Permission.ACTIVITY_READ)
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
return this.service.getStatistics(auth, dto);
}
@Post()
@Authenticated(Permission.ACTIVITY_CREATE)
async createActivity(
@Auth() auth: AuthDto,
@Body() dto: ActivityCreateDto,
@@ -43,6 +45,7 @@ export class ActivityController {
}
@Delete(':id')
@Authenticated(Permission.ACTIVITY_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);

View File

@@ -10,34 +10,36 @@ import {
UpdateAlbumDto,
} from 'src/dtos/album.dto';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AlbumService } from 'src/services/album.service';
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
@ApiTags('Album')
@Controller('album')
@Authenticated()
export class AlbumController {
constructor(private service: AlbumService) {}
@Get('count')
@Authenticated(Permission.ALBUM_READ)
getAlbumCount(@Auth() auth: AuthDto): Promise<AlbumCountResponseDto> {
return this.service.getCount(auth);
}
@Get()
@Authenticated(Permission.ALBUM_READ)
getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(auth, query);
}
@Post()
@Authenticated(Permission.ALBUM_CREATE)
createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> {
return this.service.create(auth, dto);
}
@SharedLinkRoute()
@Get(':id')
@Authenticated(Permission.ALBUM_READ, { sharedLink: true })
getAlbumInfo(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -47,6 +49,7 @@ export class AlbumController {
}
@Patch(':id')
@Authenticated(Permission.ALBUM_UPDATE)
updateAlbumInfo(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -56,12 +59,13 @@ export class AlbumController {
}
@Delete(':id')
@Authenticated(Permission.ALBUM_DELETE)
deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(auth, id);
}
@SharedLinkRoute()
@Put(':id/assets')
@Authenticated(Permission.ALBUM_ADD_ASSET, { sharedLink: true })
addAssetsToAlbum(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -71,6 +75,7 @@ export class AlbumController {
}
@Delete(':id/assets')
@Authenticated(Permission.ALBUM_REMOVE_ASSET)
removeAssetFromAlbum(
@Auth() auth: AuthDto,
@Body() dto: BulkIdsDto,
@@ -80,6 +85,7 @@ export class AlbumController {
}
@Put(':id/users')
@Authenticated(Permission.ALBUM_ADD_USER)
addUsersToAlbum(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -89,6 +95,7 @@ export class AlbumController {
}
@Delete(':id/user/:userId')
@Authenticated(Permission.ALBUM_REMOVE_USER, { bypassParamId: 'userId' })
removeUserFromAlbum(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,

View File

@@ -1,33 +1,36 @@
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { APIKeyService } from 'src/services/api-key.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('API Key')
@Controller('api-key')
@Authenticated()
export class APIKeyController {
constructor(private service: APIKeyService) {}
@Post()
@Authenticated(Permission.API_KEY_CREATE)
createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(auth, dto);
}
@Get()
@Authenticated(Permission.API_KEY_READ)
getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(auth);
}
@Get(':id')
@Authenticated(Permission.API_KEY_READ)
getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(auth, id);
}
@Put(':id')
@Authenticated(Permission.API_KEY_UPDATE)
updateApiKey(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -37,6 +40,7 @@ export class APIKeyController {
}
@Delete(':id')
@Authenticated(Permission.API_KEY_DELETE)
deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}

View File

@@ -1,6 +1,5 @@
import { Controller, Get, Header } from '@nestjs/common';
import { ApiExcludeEndpoint } from '@nestjs/swagger';
import { PublicRoute } from 'src/middleware/auth.guard';
import { SystemConfigService } from 'src/services/system-config.service';
@Controller()
@@ -18,7 +17,6 @@ export class AppController {
}
@ApiExcludeEndpoint()
@PublicRoute()
@Get('custom.css')
@Header('Content-Type', 'text/css')
getCustomCss() {

View File

@@ -31,8 +31,8 @@ import {
GetAssetThumbnailDto,
ServeFileDto,
} from 'src/dtos/asset-v1.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
import { AssetServiceV1 } from 'src/services/asset-v1.service';
import { sendFile } from 'src/utils/file';
@@ -46,12 +46,11 @@ interface UploadFiles {
@ApiTags('Asset')
@Controller(Route.ASSET)
@Authenticated()
export class AssetControllerV1 {
constructor(private service: AssetServiceV1) {}
@SharedLinkRoute()
@Post('upload')
@Authenticated(Permission.ASSET_UPLOAD, { sharedLink: true })
@UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data')
@ApiBody({
@@ -85,8 +84,8 @@ export class AssetControllerV1 {
return responseDto;
}
@SharedLinkRoute()
@Get('/file/:id')
@Authenticated(Permission.ASSET_VIEW_ORIGINAL, { sharedLink: true })
@FileResponse()
async serveFile(
@Res() res: Response,
@@ -98,8 +97,8 @@ export class AssetControllerV1 {
await sendFile(res, next, () => this.service.serveFile(auth, id, dto));
}
@SharedLinkRoute()
@Get('/thumbnail/:id')
@Authenticated(Permission.ASSET_VIEW_THUMB, { sharedLink: true })
@FileResponse()
async getAssetThumbnail(
@Res() res: Response,
@@ -112,16 +111,19 @@ export class AssetControllerV1 {
}
@Get('/curated-objects')
@Authenticated(Permission.ASSET_READ)
getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this.service.getCuratedObject(auth);
}
@Get('/curated-locations')
@Authenticated(Permission.ASSET_READ)
getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this.service.getCuratedLocation(auth);
}
@Get('/search-terms')
@Authenticated(Permission.ASSET_READ)
getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
return this.service.getAssetSearchTerm(auth);
}
@@ -130,6 +132,7 @@ export class AssetControllerV1 {
* Get all AssetEntity belong to the user
*/
@Get('/')
@Authenticated(Permission.ASSET_READ)
@ApiHeader({
name: 'if-none-match',
description: 'ETag of data already cached on the client',
@@ -144,6 +147,7 @@ export class AssetControllerV1 {
* Checks if multiple assets exist on the server and returns all existing - used by background backup
*/
@Post('/exist')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK)
checkExistingAssets(
@Auth() auth: AuthDto,
@@ -156,6 +160,7 @@ export class AssetControllerV1 {
* Checks if assets exist by checksums
*/
@Post('/bulk-upload-check')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK)
checkBulkUpload(
@Auth() auth: AuthDto,

View File

@@ -11,10 +11,10 @@ import {
RandomAssetsDto,
UpdateAssetDto,
} from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { Route } from 'src/middleware/file-upload.interceptor';
import { AssetService } from 'src/services/asset.service';
import { SearchService } from 'src/services/search.service';
@@ -22,11 +22,11 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Asset')
@Controller('assets')
@Authenticated()
export class AssetsController {
constructor(private searchService: SearchService) {}
@Get()
@Authenticated(Permission.ASSET_READ)
@ApiOperation({ deprecated: true })
async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<AssetResponseDto[]> {
const {
@@ -38,21 +38,23 @@ export class AssetsController {
@ApiTags('Asset')
@Controller(Route.ASSET)
@Authenticated()
export class AssetController {
constructor(private service: AssetService) {}
@Get('map-marker')
@Authenticated(Permission.ASSET_READ)
getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.service.getMapMarkers(auth, options);
}
@Get('memory-lane')
@Authenticated(Permission.MEMORY_READ)
getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(auth, dto);
}
@Get('random')
@Authenticated(Permission.ASSET_READ)
getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
return this.service.getRandom(auth, dto.count ?? 1);
}
@@ -61,46 +63,54 @@ export class AssetController {
* Get all asset of a device that are in the database, ID only.
*/
@Get('/device/:deviceId')
@Authenticated(Permission.ASSET_READ)
getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(auth, deviceId);
}
@Get('statistics')
@Authenticated(Permission.ASSET_READ)
getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(auth, dto);
}
@Post('jobs')
// TODO
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.NO_CONTENT)
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
return this.service.run(auth, dto);
}
@Put()
@Authenticated(Permission.ASSET_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT)
updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(auth, dto);
}
@Delete()
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> {
return this.service.deleteAll(auth, dto);
}
@Put('stack/parent')
@Authenticated(Permission.STACK_UPDATE)
@HttpCode(HttpStatus.OK)
updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise<void> {
return this.service.updateStackParent(auth, dto);
}
@SharedLinkRoute()
@Get(':id')
@Authenticated(Permission.ASSET_READ, { sharedLink: true })
getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.service.get(auth, id) as Promise<AssetResponseDto>;
}
@Put(':id')
@Authenticated(Permission.ASSET_UPDATE)
updateAsset(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,

View File

@@ -1,17 +1,17 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuditDeletesDto, AuditDeletesResponseDto } from 'src/dtos/audit.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AuditService } from 'src/services/audit.service';
@ApiTags('Audit')
@Controller('audit')
@Authenticated()
export class AuditController {
constructor(private service: AuditService) {}
@Get('deletes')
@Authenticated(Permission.ASSET_READ)
getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
return this.service.getDeletes(auth, dto);
}

View File

@@ -9,21 +9,20 @@ import {
LoginCredentialDto,
LoginResponseDto,
LogoutResponseDto,
Permission,
SignUpDto,
ValidateAccessTokenResponseDto,
} from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
@ApiTags('Authentication')
@Controller('auth')
@Authenticated()
export class AuthController {
constructor(private service: AuthService) {}
@PublicRoute()
@Post('login')
async login(
@Body() loginCredential: LoginCredentialDto,
@@ -41,25 +40,27 @@ export class AuthController {
});
}
@PublicRoute()
@Post('admin-sign-up')
signUpAdmin(@Body() dto: SignUpDto): Promise<UserResponseDto> {
return this.service.adminSignUp(dto);
}
@Post('validateToken')
@Authenticated(Permission.AUTH_DEVICE_READ)
@HttpCode(HttpStatus.OK)
validateAccessToken(): ValidateAccessTokenResponseDto {
return { authStatus: true };
}
@Post('change-password')
@Authenticated(Permission.AUTH_CHANGE_PASSWORD)
@HttpCode(HttpStatus.OK)
changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.service.changePassword(auth, dto).then(mapUser);
}
@Post('logout')
@Authenticated(Permission.AUTH_DEVICE_DELETE)
@HttpCode(HttpStatus.OK)
async logout(
@Req() request: Request,

View File

@@ -2,35 +2,34 @@ import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, Streama
import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { DownloadService } from 'src/services/download.service';
import { asStreamableFile, sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Download')
@Controller('download')
@Authenticated()
export class DownloadController {
constructor(private service: DownloadService) {}
@SharedLinkRoute()
@Post('info')
@Authenticated(Permission.ASSET_READ, { sharedLink: true })
getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
return this.service.getDownloadInfo(auth, dto);
}
@SharedLinkRoute()
@Post('archive')
@Authenticated(Permission.ASSET_DOWNLOAD, { sharedLink: true })
@HttpCode(HttpStatus.OK)
@FileResponse()
downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
return this.service.downloadArchive(auth, dto).then(asStreamableFile);
}
@SharedLinkRoute()
@Post('asset/:id')
@Authenticated(Permission.ASSET_DOWNLOAD, { sharedLink: true })
@HttpCode(HttpStatus.OK)
@FileResponse()
async downloadFile(

View File

@@ -1,6 +1,6 @@
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PersonService } from 'src/services/person.service';
@@ -8,16 +8,17 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Face')
@Controller('face')
@Authenticated()
export class FaceController {
constructor(private service: PersonService) {}
@Get()
@Authenticated(Permission.FACE_READ)
getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
return this.service.getFacesById(auth, dto);
}
@Put(':id')
@Authenticated(Permission.FACE_UPDATE)
reassignFacesById(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,

View File

@@ -1,29 +1,29 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto';
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
import { Permission } from 'src/dtos/auth.dto';
import { Authenticated } from 'src/middleware/auth.guard';
import { AuditService } from 'src/services/audit.service';
@ApiTags('File Report')
@Controller('report')
@Authenticated()
export class ReportController {
constructor(private service: AuditService) {}
@AdminRoute()
@Get()
@Authenticated(Permission.REPORT_READ)
getAuditFiles(): Promise<FileReportDto> {
return this.service.getFileReport();
}
@AdminRoute()
@Post('/checksum')
@Authenticated(Permission.REPORT_READ)
getFileChecksums(@Body() dto: FileChecksumDto): Promise<FileChecksumResponseDto[]> {
return this.service.getChecksums(dto);
}
@AdminRoute()
@Post('/fix')
@Authenticated(Permission.REPORT_UPDATE)
fixAuditFiles(@Body() dto: FileReportFixDto): Promise<void> {
return this.service.fixItems(dto.items);
}

View File

@@ -1,21 +1,23 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
import { Authenticated } from 'src/middleware/auth.guard';
import { JobService } from 'src/services/job.service';
@ApiTags('Job')
@Controller('jobs')
@Authenticated({ admin: true })
export class JobController {
constructor(private service: JobService) {}
@Get()
@Authenticated(Permission.JOB_READ)
getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
return this.service.getAllJobsStatus();
}
@Put(':id')
@Authenticated(Permission.JOB_RUN)
sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
return this.service.handleCommand(id, dto);
}

View File

@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import {
CreateLibraryDto,
LibraryResponseDto,
@@ -10,38 +11,41 @@ import {
ValidateLibraryDto,
ValidateLibraryResponseDto,
} from 'src/dtos/library.dto';
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
import { Authenticated } from 'src/middleware/auth.guard';
import { LibraryService } from 'src/services/library.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Library')
@Controller('library')
@Authenticated()
@AdminRoute()
export class LibraryController {
constructor(private service: LibraryService) {}
@Get()
@Authenticated(Permission.LIBRARY_READ)
getAllLibraries(@Query() dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
return this.service.getAll(dto);
}
@Post()
@Authenticated(Permission.LIBRARY_CREATE)
createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> {
return this.service.create(dto);
}
@Put(':id')
@Authenticated(Permission.LIBRARY_UPDATE)
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto);
}
@Get(':id')
@Authenticated(Permission.LIBRARY_READ)
getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> {
return this.service.get(id);
}
@Post(':id/validate')
@Authenticated(Permission.LIBRARY_READ)
@HttpCode(200)
// TODO: change endpoint to validate current settings instead
validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
@@ -49,23 +53,27 @@ export class LibraryController {
}
@Delete(':id')
@Authenticated(Permission.LIBRARY_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(id);
}
@Get(':id/statistics')
@Authenticated(Permission.LIBRARY_READ)
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(id);
}
@Post(':id/scan')
@Authenticated(Permission.LIBRARY_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT)
scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
return this.service.queueScan(id, dto);
}
@Post(':id/removeOffline')
@Authenticated(Permission.LIBRARY_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT)
removeOfflineFiles(@Param() { id }: UUIDParamDto) {
return this.service.queueRemoveOffline(id);

View File

@@ -1,7 +1,7 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { MemoryService } from 'src/services/memory.service';
@@ -9,26 +9,29 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Memory')
@Controller('memories')
@Authenticated()
export class MemoryController {
constructor(private service: MemoryService) {}
@Get()
@Authenticated(Permission.MEMORY_READ)
searchMemories(@Auth() auth: AuthDto): Promise<MemoryResponseDto[]> {
return this.service.search(auth);
}
@Post()
@Authenticated(Permission.MEMORY_CREATE)
createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> {
return this.service.create(auth, dto);
}
@Get(':id')
@Authenticated(Permission.MEMORY_READ)
getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> {
return this.service.get(auth, id);
}
@Put(':id')
@Authenticated(Permission.MEMORY_UPDATE)
updateMemory(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -38,12 +41,14 @@ export class MemoryController {
}
@Delete(':id')
@Authenticated(Permission.MEMORY_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id);
}
@Put(':id/assets')
@Authenticated(Permission.MEMORY_ADD_ASSET)
addMemoryAssets(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -53,6 +58,7 @@ export class MemoryController {
}
@Delete(':id/assets')
@Authenticated(Permission.MEMORY_REMOVE_ASSET)
@HttpCode(HttpStatus.OK)
removeMemoryAssets(
@Auth() auth: AuthDto,

View File

@@ -9,19 +9,18 @@ import {
OAuthAuthorizeResponseDto,
OAuthCallbackDto,
OAuthConfigDto,
Permission,
} from 'src/dtos/auth.dto';
import { UserResponseDto } from 'src/dtos/user.dto';
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie } from 'src/utils/response';
@ApiTags('OAuth')
@Controller('oauth')
@Authenticated()
export class OAuthController {
constructor(private service: AuthService) {}
@PublicRoute()
@Get('mobile-redirect')
@Redirect()
redirectOAuthToMobile(@Req() request: Request) {
@@ -31,13 +30,11 @@ export class OAuthController {
};
}
@PublicRoute()
@Post('authorize')
startOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto);
}
@PublicRoute()
@Post('callback')
async finishOAuth(
@Res({ passthrough: true }) res: Response,
@@ -56,11 +53,13 @@ export class OAuthController {
}
@Post('link')
@Authenticated(Permission.AUTH_OAUTH)
linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(auth, dto);
}
@Post('unlink')
@Authenticated(Permission.AUTH_OAUTH)
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.unlink(auth);
}

View File

@@ -1,6 +1,6 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { PartnerDirection } from 'src/interfaces/partner.interface';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
@@ -9,11 +9,11 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Partner')
@Controller('partner')
@Authenticated()
export class PartnerController {
constructor(private service: PartnerService) {}
@Get()
@Authenticated(Permission.PARTNER_READ)
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
// TODO: remove 'direction' and convert to full query dto
getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> {
@@ -21,11 +21,13 @@ export class PartnerController {
}
@Post(':id')
@Authenticated(Permission.PARTNER_CREATE)
createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(auth, id);
}
@Put(':id')
@Authenticated(Permission.PARTNER_UPDATE)
updatePartner(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -35,6 +37,7 @@ export class PartnerController {
}
@Delete(':id')
@Authenticated(Permission.PARTNER_DELETE)
removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id);
}

View File

@@ -3,7 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import {
AssetFaceUpdateDto,
MergePersonDto,
@@ -22,31 +22,35 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Person')
@Controller('person')
@Authenticated()
export class PersonController {
constructor(private service: PersonService) {}
@Get()
@Authenticated(Permission.PERSON_READ)
getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(auth, withHidden);
}
@Post()
@Authenticated(Permission.PERSON_CREATE)
createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> {
return this.service.create(auth, dto);
}
@Put()
@Authenticated(Permission.PERSON_UPDATE)
updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updateAll(auth, dto);
}
@Get(':id')
@Authenticated(Permission.PERSON_READ)
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(auth, id);
}
@Put(':id')
@Authenticated(Permission.PERSON_UPDATE)
updatePerson(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -56,11 +60,13 @@ export class PersonController {
}
@Get(':id/statistics')
@Authenticated(Permission.PERSON_READ)
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
return this.service.getStatistics(auth, id);
}
@Get(':id/thumbnail')
@Authenticated(Permission.PERSON_READ)
@FileResponse()
async getPersonThumbnail(
@Res() res: Response,
@@ -72,11 +78,13 @@ export class PersonController {
}
@Get(':id/assets')
@Authenticated(Permission.ASSET_READ)
getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(auth, id);
}
@Put(':id/reassign')
@Authenticated(Permission.PERSON_UPDATE)
reassignFaces(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -86,6 +94,7 @@ export class PersonController {
}
@Post(':id/merge')
@Authenticated(Permission.PERSON_UPDATE)
mergePerson(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,

View File

@@ -1,7 +1,7 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
import {
MetadataSearchDto,
@@ -19,49 +19,56 @@ import { SearchService } from 'src/services/search.service';
@ApiTags('Search')
@Controller('search')
@Authenticated()
export class SearchController {
constructor(private service: SearchService) {}
@Get()
@Authenticated(Permission.ASSET_READ)
@ApiOperation({ deprecated: true })
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
return this.service.search(auth, dto);
}
@Post('metadata')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK)
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
return this.service.searchMetadata(auth, dto);
}
@Post('smart')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK)
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
return this.service.searchSmart(auth, dto);
}
@Get('explore')
@Authenticated(Permission.ASSET_READ)
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
return this.service.getExploreData(auth) as Promise<SearchExploreResponseDto[]>;
}
@Get('person')
@Authenticated(Permission.PERSON_READ)
searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.service.searchPerson(auth, dto);
}
@Get('places')
@Authenticated(Permission.ASSET_READ)
searchPlaces(@Query() dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
return this.service.searchPlaces(dto);
}
@Get('cities')
@Authenticated(Permission.ASSET_READ)
getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> {
return this.service.getAssetsByCity(auth);
}
@Get('suggestions')
@Authenticated(Permission.ASSET_READ)
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
return this.service.getSearchSuggestions(auth, dto);
}

View File

@@ -1,5 +1,6 @@
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import {
ServerConfigDto,
ServerFeaturesDto,
@@ -10,57 +11,51 @@ import {
ServerThemeDto,
ServerVersionResponseDto,
} from 'src/dtos/server-info.dto';
import { AdminRoute, Authenticated, PublicRoute } from 'src/middleware/auth.guard';
import { Authenticated } from 'src/middleware/auth.guard';
import { ServerInfoService } from 'src/services/server-info.service';
@ApiTags('Server Info')
@Controller('server-info')
@Authenticated()
export class ServerInfoController {
constructor(private service: ServerInfoService) {}
@Get()
@Authenticated(Permission.SERVER_READ)
getServerInfo(): Promise<ServerInfoResponseDto> {
return this.service.getInfo();
}
@PublicRoute()
@Get('ping')
pingServer(): ServerPingResponse {
return this.service.ping();
}
@PublicRoute()
@Get('version')
getServerVersion(): ServerVersionResponseDto {
return this.service.getVersion();
}
@PublicRoute()
@Get('features')
getServerFeatures(): Promise<ServerFeaturesDto> {
return this.service.getFeatures();
}
@PublicRoute()
@Get('theme')
getTheme(): Promise<ServerThemeDto> {
return this.service.getTheme();
}
@PublicRoute()
@Get('config')
getServerConfig(): Promise<ServerConfigDto> {
return this.service.getConfig();
}
@AdminRoute()
@Get('statistics')
@Authenticated(Permission.SERVER_READ)
getServerStatistics(): Promise<ServerStatsResponseDto> {
return this.service.getStatistics();
}
@PublicRoute()
@Get('media-types')
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
return this.service.getSupportedMediaTypes();

View File

@@ -1,6 +1,6 @@
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { SessionResponseDto } from 'src/dtos/session.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SessionService } from 'src/services/session.service';
@@ -8,22 +8,24 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Sessions')
@Controller('sessions')
@Authenticated()
export class SessionController {
constructor(private service: SessionService) {}
@Get()
@Authenticated(Permission.SESSION_READ)
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
return this.service.getAll(auth);
}
@Delete()
@Authenticated(Permission.SESSION_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
deleteAllSessions(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteAll(auth);
}
@Delete(':id')
@Authenticated(Permission.SESSION_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);

View File

@@ -3,14 +3,14 @@ import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto';
import { AuthDto, ImmichCookie, Permission } from 'src/dtos/auth.dto';
import {
SharedLinkCreateDto,
SharedLinkEditDto,
SharedLinkPasswordDto,
SharedLinkResponseDto,
} from 'src/dtos/shared-link.dto';
import { Auth, Authenticated, GetLoginDetails, SharedLinkRoute } from 'src/middleware/auth.guard';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { LoginDetails } from 'src/services/auth.service';
import { SharedLinkService } from 'src/services/shared-link.service';
import { respondWithCookie } from 'src/utils/response';
@@ -18,17 +18,17 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Shared Link')
@Controller('shared-link')
@Authenticated()
export class SharedLinkController {
constructor(private service: SharedLinkService) {}
@Get()
@Authenticated(Permission.SHARED_LINK_READ)
getAllSharedLinks(@Auth() auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(auth);
}
@SharedLinkRoute()
@Get('me')
@Authenticated(Permission.SHARED_LINK_READ, { sharedLink: true })
async getMySharedLink(
@Auth() auth: AuthDto,
@Query() dto: SharedLinkPasswordDto,
@@ -48,16 +48,19 @@ export class SharedLinkController {
}
@Get(':id')
@Authenticated(Permission.SHARED_LINK_READ)
getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
return this.service.get(auth, id);
}
@Post()
@Authenticated(Permission.SHARED_LINK_CREATE)
createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) {
return this.service.create(auth, dto);
}
@Patch(':id')
@Authenticated(Permission.SHARED_LINK_UPDATE)
updateSharedLink(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -67,12 +70,13 @@ export class SharedLinkController {
}
@Delete(':id')
@Authenticated(Permission.SHARED_LINK_DELETE)
removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id);
}
@SharedLinkRoute()
@Put(':id/assets')
@Authenticated(Permission.SHARED_LINK_UPDATE, { sharedLink: true })
addSharedLinkAssets(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -81,8 +85,8 @@ export class SharedLinkController {
return this.service.addAssets(auth, id, dto);
}
@SharedLinkRoute()
@Delete(':id/assets')
@Authenticated(Permission.SHARED_LINK_DELETE, { sharedLink: true })
removeSharedLinkAssets(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,

View File

@@ -1,23 +1,24 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SyncService } from 'src/services/sync.service';
@ApiTags('Sync')
@Controller('sync')
@Authenticated()
export class SyncController {
constructor(private service: SyncService) {}
@Get('full-sync')
@Authenticated(Permission.ASSET_READ)
getAllForUserFullSync(@Auth() auth: AuthDto, @Query() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
return this.service.getAllAssetsForUserFullSync(auth, dto);
}
@Get('delta-sync')
@Authenticated(Permission.ASSET_READ)
getDeltaSync(@Auth() auth: AuthDto, @Query() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
return this.service.getChangesForDeltaSync(auth, dto);
}

View File

@@ -1,38 +1,41 @@
import { Body, Controller, Get, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
import { AdminRoute, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
import { Authenticated } from 'src/middleware/auth.guard';
import { SystemConfigService } from 'src/services/system-config.service';
@ApiTags('System Config')
@Controller('system-config')
@Authenticated({ admin: true })
export class SystemConfigController {
constructor(private service: SystemConfigService) {}
@Get()
@Authenticated(Permission.SYSTEM_CONFIG_READ)
getConfig(): Promise<SystemConfigDto> {
return this.service.getConfig();
}
@Get('defaults')
@Authenticated(Permission.SYSTEM_CONFIG_READ)
getConfigDefaults(): SystemConfigDto {
return this.service.getDefaults();
}
@Put()
@Authenticated(Permission.SYSTEM_CONFIG_UPDATE)
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
return this.service.updateConfig(dto);
}
@Get('storage-template-options')
@Authenticated(Permission.SYSTEM_CONFIG_READ)
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
return this.service.getStorageTemplateOptions();
}
@AdminRoute(false)
@SharedLinkRoute()
@Get('map/style.json')
@Authenticated(Permission.MAP_READ, { sharedLink: true })
getMapStyle(@Query() dto: MapThemeDto) {
return this.service.getMapStyle(dto.theme);
}

View File

@@ -1,27 +1,30 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto';
import { Authenticated } from 'src/middleware/auth.guard';
import { SystemMetadataService } from 'src/services/system-metadata.service';
@ApiTags('System Metadata')
@Controller('system-metadata')
@Authenticated({ admin: true })
export class SystemMetadataController {
constructor(private service: SystemMetadataService) {}
@Get('admin-onboarding')
@Authenticated(Permission.SYSTEM_METADATA_READ)
getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
return this.service.getAdminOnboarding();
}
@Post('admin-onboarding')
@Authenticated(Permission.SYSTEM_METADATA_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT)
updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
return this.service.updateAdminOnboarding(dto);
}
@Get('reverse-geocoding-state')
@Authenticated(Permission.SYSTEM_METADATA_READ)
getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
return this.service.getReverseGeocodingState();
}

View File

@@ -3,7 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TagService } from 'src/services/tag.service';
@@ -11,41 +11,47 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Tag')
@Controller('tag')
@Authenticated()
export class TagController {
constructor(private service: TagService) {}
@Post()
@Authenticated(Permission.TAG_CREATE)
createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> {
return this.service.create(auth, dto);
}
@Get()
@Authenticated(Permission.TAG_READ)
getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> {
return this.service.getAll(auth);
}
@Get(':id')
@Authenticated(Permission.TAG_READ)
getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.getById(auth, id);
}
@Patch(':id')
@Authenticated(Permission.TAG_UPDATE)
updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise<TagResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated(Permission.TAG_DELETE)
deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id);
}
@Get(':id/assets')
@Authenticated(Permission.TAG_READ)
getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(auth, id);
}
@Put(':id/assets')
@Authenticated(Permission.TAG_UPDATE)
tagAssets(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -55,6 +61,7 @@ export class TagController {
}
@Delete(':id/assets')
@Authenticated(Permission.TAG_UPDATE)
untagAssets(
@Auth() auth: AuthDto,
@Body() dto: AssetIdsDto,

View File

@@ -1,24 +1,23 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TimelineService } from 'src/services/timeline.service';
@ApiTags('Timeline')
@Controller('timeline')
@Authenticated()
export class TimelineController {
constructor(private service: TimelineService) {}
@Authenticated({ isShared: true })
@Authenticated(Permission.ASSET_READ, { sharedLink: true })
@Get('buckets')
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
return this.service.getTimeBuckets(auth, dto);
}
@Authenticated({ isShared: true })
@Authenticated(Permission.ASSET_READ, { sharedLink: true })
@Get('bucket')
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;

View File

@@ -1,29 +1,31 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TrashService } from 'src/services/trash.service';
@ApiTags('Trash')
@Controller('trash')
@Authenticated()
export class TrashController {
constructor(private service: TrashService) {}
@Post('empty')
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
emptyTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.empty(auth);
}
@Post('restore')
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
restoreTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.restore(auth);
}
@Post('restore/assets')
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT)
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.restoreAssets(auth, dto);

View File

@@ -16,10 +16,10 @@ import {
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
import { UserService } from 'src/services/user.service';
import { sendFile } from 'src/utils/file';
@@ -27,39 +27,42 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('User')
@Controller(Route.USER)
@Authenticated()
export class UserController {
constructor(private service: UserService) {}
@Get()
@Authenticated(Permission.USER_READ)
getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
return this.service.getAll(auth, isAll);
}
@Get('info/:id')
@Authenticated(Permission.USER_READ)
getUserById(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.get(id);
}
@Get('me')
@Authenticated(Permission.USER_READ)
getMyUserInfo(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.getMe(auth);
}
@AdminRoute()
@Post()
@Authenticated(Permission.USER_CREATE)
createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
return this.service.create(createUserDto);
}
@Delete('profile-image')
@Authenticated(Permission.USER_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT)
deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteProfileImage(auth);
}
@AdminRoute()
@Delete(':id')
@Authenticated(Permission.USER_DELETE)
deleteUser(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@@ -68,14 +71,15 @@ export class UserController {
return this.service.delete(auth, id, dto);
}
@AdminRoute()
@Post(':id/restore')
@Authenticated(Permission.USER_DELETE)
restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.restore(auth, id);
}
// TODO: replace with @Put(':id')
@Put()
@Authenticated(Permission.USER_UPDATE)
updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
return this.service.update(auth, updateUserDto);
}
@@ -84,6 +88,7 @@ export class UserController {
@ApiConsumes('multipart/form-data')
@ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto })
@Post('profile-image')
@Authenticated(Permission.USER_UPDATE)
createProfileImage(
@Auth() auth: AuthDto,
@UploadedFile() fileInfo: Express.Multer.File,
@@ -92,6 +97,7 @@ export class UserController {
}
@Get('profile-image/:id')
@Authenticated(Permission.USER_READ)
@FileResponse()
async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) {
await sendFile(res, next, () => this.service.getProfileImage(id));

View File

@@ -4,7 +4,7 @@ import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
export enum Permission {
export enum AccessPermission {
ACTIVITY_CREATE = 'activity.create',
ACTIVITY_DELETE = 'activity.delete',
@@ -74,7 +74,7 @@ export class AccessCore {
* Check if user has access to all ids, for the given permission.
* Throws error if user does not have access to any of the ids.
*/
async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) {
async requirePermission(auth: AuthDto, permission: AccessPermission, ids: string[] | string) {
ids = Array.isArray(ids) ? ids : [ids];
const allowedIds = await this.checkAccess(auth, permission, ids);
if (!setIsEqual(new Set(ids), allowedIds)) {
@@ -88,7 +88,7 @@ export class AccessCore {
*
* @returns Set<string>
*/
async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]): Promise<Set<string>> {
async checkAccess(auth: AuthDto, permission: AccessPermission, ids: Set<string> | string[]): Promise<Set<string>> {
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
if (idSet.size === 0) {
return new Set();
@@ -103,40 +103,40 @@ export class AccessCore {
private async checkAccessSharedLink(
sharedLink: SharedLinkEntity,
permission: Permission,
permission: AccessPermission,
ids: Set<string>,
): Promise<Set<string>> {
const sharedLinkId = sharedLink.id;
switch (permission) {
case Permission.ASSET_READ: {
case AccessPermission.ASSET_READ: {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ASSET_VIEW: {
case AccessPermission.ASSET_VIEW: {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ASSET_DOWNLOAD: {
case AccessPermission.ASSET_DOWNLOAD: {
return sharedLink.allowDownload
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
}
case Permission.ASSET_UPLOAD: {
case AccessPermission.ASSET_UPLOAD: {
return sharedLink.allowUpload ? ids : new Set();
}
case Permission.ASSET_SHARE: {
case AccessPermission.ASSET_SHARE: {
// TODO: fix this to not use sharedLink.userId for access control
return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids);
}
case Permission.ALBUM_READ: {
case AccessPermission.ALBUM_READ: {
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
}
case Permission.ALBUM_DOWNLOAD: {
case AccessPermission.ALBUM_DOWNLOAD: {
return sharedLink.allowDownload
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
@@ -148,15 +148,15 @@ export class AccessCore {
}
}
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>): Promise<Set<string>> {
private async checkAccessOther(auth: AuthDto, permission: AccessPermission, ids: Set<string>): Promise<Set<string>> {
switch (permission) {
// uses album id
case Permission.ACTIVITY_CREATE: {
case AccessPermission.ACTIVITY_CREATE: {
return await this.repository.activity.checkCreateAccess(auth.user.id, ids);
}
// uses activity id
case Permission.ACTIVITY_DELETE: {
case AccessPermission.ACTIVITY_DELETE: {
const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids);
const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess(
auth.user.id,
@@ -165,7 +165,7 @@ export class AccessCore {
return setUnion(isOwner, isAlbumOwner);
}
case Permission.ASSET_READ: {
case AccessPermission.ASSET_READ: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
@@ -175,13 +175,13 @@ export class AccessCore {
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_SHARE: {
case AccessPermission.ASSET_SHARE: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.ASSET_VIEW: {
case AccessPermission.ASSET_VIEW: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
@@ -191,7 +191,7 @@ export class AccessCore {
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_DOWNLOAD: {
case AccessPermission.ASSET_DOWNLOAD: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
@@ -201,101 +201,101 @@ export class AccessCore {
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_UPDATE: {
case AccessPermission.ASSET_UPDATE: {
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ASSET_DELETE: {
case AccessPermission.ASSET_DELETE: {
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ASSET_RESTORE: {
case AccessPermission.ASSET_RESTORE: {
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_READ: {
case AccessPermission.ALBUM_READ: {
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_UPDATE: {
case AccessPermission.ALBUM_UPDATE: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_DELETE: {
case AccessPermission.ALBUM_DELETE: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_SHARE: {
case AccessPermission.ALBUM_SHARE: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ALBUM_DOWNLOAD: {
case AccessPermission.ALBUM_DOWNLOAD: {
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared);
}
case Permission.ALBUM_REMOVE_ASSET: {
case AccessPermission.ALBUM_REMOVE_ASSET: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ASSET_UPLOAD: {
case AccessPermission.ASSET_UPLOAD: {
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
}
case Permission.ARCHIVE_READ: {
case AccessPermission.ARCHIVE_READ: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
}
case Permission.AUTH_DEVICE_DELETE: {
case AccessPermission.AUTH_DEVICE_DELETE: {
return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids);
}
case Permission.TIMELINE_READ: {
case AccessPermission.TIMELINE_READ: {
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.TIMELINE_DOWNLOAD: {
case AccessPermission.TIMELINE_DOWNLOAD: {
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
}
case Permission.MEMORY_READ: {
case AccessPermission.MEMORY_READ: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_WRITE: {
case AccessPermission.MEMORY_WRITE: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.MEMORY_DELETE: {
case AccessPermission.MEMORY_DELETE: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_READ: {
case AccessPermission.PERSON_READ: {
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_WRITE: {
case AccessPermission.PERSON_WRITE: {
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_MERGE: {
case AccessPermission.PERSON_MERGE: {
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_CREATE: {
case AccessPermission.PERSON_CREATE: {
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
}
case Permission.PERSON_REASSIGN: {
case AccessPermission.PERSON_REASSIGN: {
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
}
case Permission.PARTNER_UPDATE: {
case AccessPermission.PARTNER_UPDATE: {
return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
}

View File

@@ -48,6 +48,10 @@ export class UserCore {
throw new BadRequestException('The server already has an admin');
}
if (dto.permissions) {
// TODO validate granted permissions
}
if (dto.email) {
const duplicate = await this.userRepository.getByEmail(dto.email);
if (duplicate && duplicate.id !== id) {
@@ -93,6 +97,11 @@ export class UserCore {
if (payload.storageLabel) {
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
}
if (payload.permissions) {
// TODO validate permissions
}
const userEntity = await this.userRepository.create(payload);
await this.libraryRepository.create({
owner: { id: userEntity.id } as UserEntity,

View File

@@ -25,6 +25,195 @@ export type CookieResponse = {
values: Array<{ key: ImmichCookie; value: string }>;
};
export enum PermissionPreset {
USER = 'user',
ADMIN = 'admin',
CUSTOM = 'custom',
}
export enum Permission {
ACTIVITY_CREATE = 'activity.create',
ACTIVITY_READ = 'activity.read',
ACTIVITY_UPDATE = 'activity.update',
ACTIVITY_DELETE = 'activity.delete',
ALBUM_CREATE = 'album.create',
ALBUM_READ = 'album.read',
ALBUM_UPDATE = 'album.update',
ALBUM_DELETE = 'album.delete',
ASSET_CREATE = 'asset.create',
ASSET_READ = 'asset.read',
ASSET_UPDATE = 'asset.update',
ASSET_DELETE = 'asset.delete',
API_KEY_CREATE = 'apiKey.create',
API_KEY_READ = 'apiKey.read',
API_KEY_UPDATE = 'apiKey.update',
API_KEY_DELETE = 'apiKey.delete',
AUTH_DEVICE_CREATE = 'authDevice.create',
AUTH_DEVICE_READ = 'authDevice.read',
AUTH_DEVICE_UPDATE = 'authDevice.update',
AUTH_DEVICE_DELETE = 'authDevice.delete',
FACE_CREATE = 'face.create',
FACE_READ = 'face.read',
FACE_UPDATE = 'face.update',
FACE_DELETE = 'face.delete',
LIBRARY_CREATE = 'library.create',
LIBRARY_READ = 'library.read',
LIBRARY_UPDATE = 'library.update',
LIBRARY_DELETE = 'library.delete',
MEMORY_CREATE = 'memory.create',
MEMORY_READ = 'memory.read',
MEMORY_UPDATE = 'memory.update',
MEMORY_DELETE = 'memory.delete',
MEMORY_ADD_ASSET = 'memory.addAsset',
MEMORY_REMOVE_ASSET = 'memory.removeAsset',
PARTNER_CREATE = 'partner.create',
PARTNER_READ = 'partner.read',
PARTNER_UPDATE = 'partner.update',
PARTNER_DELETE = 'partner.delete',
PERSON_CREATE = 'person.create',
PERSON_READ = 'person.read',
PERSON_UPDATE = 'person.update',
PERSON_DELETE = 'person.delete',
REPORT_CREATE = 'report.create',
REPORT_READ = 'report.read',
REPORT_UPDATE = 'report.update',
REPORT_DELETE = 'report.delete',
SESSION_CREATE = 'session.create',
SESSION_READ = 'session.read',
SESSION_UPDATE = 'session.update',
SESSION_DELETE = 'session.delete',
SHARED_LINK_CREATE = 'sharedLink.create',
SHARED_LINK_READ = 'sharedLink.read',
SHARED_LINK_UPDATE = 'sharedLink.update',
SHARED_LINK_DELETE = 'sharedLink.delete',
SYSTEM_CONFIG_CREATE = 'systemConfig.create',
SYSTEM_CONFIG_READ = 'systemConfig.read',
SYSTEM_CONFIG_UPDATE = 'systemConfig.update',
SYSTEM_CONFIG_DELETE = 'systemConfig.delete',
SYSTEM_METADATA_CREATE = 'systemMetadata.create',
SYSTEM_METADATA_READ = 'systemMetadata.read',
SYSTEM_METADATA_UPDATE = 'systemMetadata.update',
SYSTEM_METADATA_DELETE = 'systemMetadata.delete',
STACK_CREATE = 'stack.create',
STACK_READ = 'stack.read',
STACK_UPDATE = 'stack.update',
STACK_DELETE = 'stack.delete',
TAG_CREATE = 'tag.create',
TAG_READ = 'tag.read',
TAG_UPDATE = 'tag.update',
TAG_DELETE = 'tag.delete',
USER_CREATE = 'user.create',
USER_READ = 'user.read',
USER_UPDATE = 'user.update',
USER_DELETE = 'user.delete',
// other
AUTH_CHANGE_PASSWORD = 'auth.changePassword',
AUTH_OAUTH = 'auth.oauth',
ALBUM_ADD_ASSET = 'album.addAsset',
ALBUM_REMOVE_ASSET = 'album.removeAsset',
ALBUM_ADD_USER = 'album.addUser',
ALBUM_REMOVE_USER = 'album.removeUser',
ASSET_VIEW_THUMB = 'asset.viewThumb',
ASSET_VIEW_PREVIEW = 'asset.viewPreview',
ASSET_VIEW_ORIGINAL = 'asset.viewOriginal',
ASSET_UPLOAD = 'asset.upload',
ASSET_DOWNLOAD = 'asset.download',
JOB_READ = 'job.read',
JOB_RUN = 'job.run',
MAP_READ = 'map.read',
USER_READ_SIMPLE = 'user.readSimple',
USER_CHANGE_PASSWORD = 'user.changePassword',
SERVER_READ = 'server.read',
SERVER_SETUP = 'server.setup',
}
export const presetToPermissions = ({
permissionPreset: preset,
permissions,
}: {
permissionPreset?: PermissionPreset;
permissions?: Permission[];
}) => {
switch (preset) {
case PermissionPreset.ADMIN: {
return ALL_PERMISSIONS;
}
case PermissionPreset.USER: {
return USER_PERMISSIONS;
}
case PermissionPreset.CUSTOM: {
return permissions ?? [];
}
default: {
return;
}
}
};
export const ALL_PERMISSIONS = Object.values(Permission);
export const USER_PERMISSIONS = ALL_PERMISSIONS.filter((permission) => {
switch (permission) {
case Permission.JOB_READ:
case Permission.JOB_RUN:
case Permission.LIBRARY_READ:
case Permission.LIBRARY_CREATE:
case Permission.LIBRARY_UPDATE:
case Permission.LIBRARY_DELETE:
// TODO this can't be an admin permission yet because non-admins still use it
case Permission.USER_READ:
case Permission.USER_CREATE:
case Permission.USER_UPDATE:
case Permission.USER_DELETE:
case Permission.SYSTEM_CONFIG_CREATE:
case Permission.SYSTEM_CONFIG_READ:
case Permission.SYSTEM_CONFIG_UPDATE:
case Permission.SYSTEM_CONFIG_DELETE:
case Permission.SYSTEM_METADATA_CREATE:
case Permission.SYSTEM_METADATA_READ:
case Permission.SYSTEM_METADATA_UPDATE:
case Permission.SYSTEM_METADATA_DELETE: {
return false;
}
default: {
return true;
}
}
});
export type AuthorizationPermissions = Set<Permission>;
export class AuthDto {
user!: UserEntity;

View File

@@ -1,5 +1,6 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { PermissionPreset } from 'src/dtos/auth.dto';
import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
describe('update user DTO', () => {
@@ -22,6 +23,7 @@ describe('create user DTO', () => {
email: undefined,
password: 'password',
name: 'name',
permissionPreset: PermissionPreset.USER,
};
let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
let errors = await validate(dto);
@@ -45,6 +47,7 @@ describe('create user DTO', () => {
email: someEmail,
password: 'some password',
name: 'some name',
permissionPreset: 'user',
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);

View File

@@ -1,10 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID, ValidateIf } from 'class-validator';
import { Permission, PermissionPreset } from 'src/dtos/auth.dto';
import { getRandomAvatarColor } from 'src/dtos/user-profile.dto';
import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
const isCustomPreset = ({ permissionPreset }: CreateUserDto) =>
permissionPreset && permissionPreset === PermissionPreset.CUSTOM;
export class CreateUserDto {
@IsEmail({ require_tld: false })
@Transform(toEmail)
@@ -34,6 +38,15 @@ export class CreateUserDto {
@ValidateBoolean({ optional: true })
shouldChangePassword?: boolean;
@IsEnum(PermissionPreset)
@ApiProperty({ enum: PermissionPreset, enumName: 'PermissionPreset' })
permissionPreset!: PermissionPreset;
@ValidateIf(isCustomPreset)
@IsEnum(Permission, { each: true })
@ApiPropertyOptional({ enum: Permission, enumName: 'AuthorizationPermission' })
permissions?: Permission[];
}
export class CreateAdminDto {
@@ -112,6 +125,16 @@ export class UpdateUserDto {
@IsPositive()
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
@Optional()
@IsEnum(PermissionPreset)
@ApiProperty({ enum: PermissionPreset, enumName: 'PermissionPreset' })
permissionPreset?: PermissionPreset;
@ValidateIf(isCustomPreset)
@IsEnum(Permission, { each: true })
@ApiPropertyOptional({ enum: Permission, enumName: 'AuthorizationPermission' })
permissions?: Permission[];
}
export class UserDto {
@@ -139,6 +162,7 @@ export class UserResponseDto extends UserDto {
quotaUsageInBytes!: number | null;
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
status!: string;
permissions?: Permission[];
}
export const mapSimpleUser = (entity: UserEntity): UserDto => {
@@ -165,5 +189,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
quotaSizeInBytes: entity.quotaSizeInBytes,
quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status,
permissions: entity.permissions,
};
}

View File

@@ -1,3 +1,4 @@
import { Permission } from 'src/dtos/auth.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { TagEntity } from 'src/entities/tag.entity';
import {
@@ -87,4 +88,7 @@ export class UserEntity {
@Column({ type: 'bigint', default: 0 })
quotaUsageInBytes!: number;
@Column({ type: 'varchar', array: true })
permissions!: Permission[];
}

View File

@@ -10,49 +10,40 @@ import {
import { Reflector } from '@nestjs/core';
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
import { Request } from 'express';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { UAParser } from 'ua-parser-js';
export enum Metadata {
AUTH_ROUTE = 'auth_route',
ADMIN_ROUTE = 'admin_route',
SHARED_ROUTE = 'shared_route',
PUBLIC_SECURITY = 'public_security',
API_KEY_SECURITY = 'api_key',
PERMISSION = 'auth_permission',
}
export interface AuthenticatedOptions {
admin?: true;
isShared?: true;
}
type AuthenticatedOptions = {
sharedLink?: true;
/** skip permission check when param id matches calling user */
bypassParamId?: string;
};
export const Authenticated = (options: AuthenticatedOptions = {}) => {
const decorators: MethodDecorator[] = [
export const Authenticated = (permission: Permission, options?: AuthenticatedOptions) => {
const { sharedLink } = { sharedLink: false, ...options };
const decorators = sharedLink
? [SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false })]
: [];
return applyDecorators(
ApiBearerAuth(),
ApiCookieAuth(),
ApiSecurity(Metadata.API_KEY_SECURITY),
SetMetadata(Metadata.AUTH_ROUTE, true),
];
if (options.admin) {
decorators.push(AdminRoute());
}
if (options.isShared) {
decorators.push(SharedLinkRoute());
}
return applyDecorators(...decorators);
SetMetadata(Metadata.PERMISSION, permission),
...decorators,
);
};
export const PublicRoute = () =>
applyDecorators(SetMetadata(Metadata.AUTH_ROUTE, false), ApiSecurity(Metadata.PUBLIC_SECURITY));
export const SharedLinkRoute = () =>
applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false }));
export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value);
export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => {
return context.switchToHttp().getRequest<{ user: AuthDto }>().user;
});
@@ -89,26 +80,29 @@ export class AuthGuard implements CanActivate {
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const targets = [context.getHandler(), context.getClass()];
const method = context.getHandler();
const isAuthRoute = this.reflector.getAllAndOverride(Metadata.AUTH_ROUTE, targets);
const isAdminRoute = this.reflector.getAllAndOverride(Metadata.ADMIN_ROUTE, targets);
const isSharedRoute = this.reflector.getAllAndOverride(Metadata.SHARED_ROUTE, targets);
const permission = this.reflector.get<Permission>(Metadata.PERMISSION, method);
const isSharedRoute = this.reflector.get<boolean>(Metadata.SHARED_ROUTE, method);
if (!isAuthRoute) {
// public
if (!permission) {
return true;
}
const request = context.switchToHttp().getRequest<AuthRequest>();
const authDto = await this.authService.validate(request.headers, request.query as Record<string, string>);
const isApiKey = !!authDto.apiKey;
const isUserToken = !!authDto.session;
if (authDto.sharedLink && !isSharedRoute) {
this.logger.warn(`Denied access to non-shared route: ${request.path}`);
return false;
}
if (isAdminRoute && !authDto.user.isAdmin) {
this.logger.warn(`Denied access to admin only route: ${request.path}`);
if ((isApiKey || isUserToken) && !authDto.user.permissions.includes(permission)) {
this.logger.warn(`Denied access to route: no ${permission} permission: ${request.path}. `);
return false;
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserPermissions1713389653857 implements MigrationInterface {
name = 'AddUserPermissions1713389653857'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "permissions" character varying array NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "permissions"`);
}
}

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import {
ActivityCreateDto,
ActivityDto,
@@ -28,7 +28,7 @@ export class ActivityService {
}
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
await this.access.requirePermission(auth, AccessPermission.ALBUM_READ, dto.albumId);
const activities = await this.repository.search({
userId: dto.userId,
albumId: dto.albumId,
@@ -40,12 +40,12 @@ export class ActivityService {
}
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
await this.access.requirePermission(auth, AccessPermission.ALBUM_READ, dto.albumId);
return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
}
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId);
await this.access.requirePermission(auth, AccessPermission.ACTIVITY_CREATE, dto.albumId);
const common = {
userId: auth.user.id,
@@ -79,7 +79,7 @@ export class ActivityService {
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id);
await this.access.requirePermission(auth, AccessPermission.ACTIVITY_DELETE, id);
await this.repository.delete(id);
}
}

View File

@@ -1,5 +1,5 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import {
AddUsersDto,
AlbumCountResponseDto,
@@ -97,7 +97,7 @@ export class AlbumService {
}
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
await this.access.requirePermission(auth, AccessPermission.ALBUM_READ, id);
await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets });
@@ -119,7 +119,7 @@ export class AlbumService {
}
}
const allowedAssetIdsSet = await this.access.checkAccess(auth, Permission.ASSET_SHARE, new Set(dto.assetIds));
const allowedAssetIdsSet = await this.access.checkAccess(auth, AccessPermission.ASSET_SHARE, new Set(dto.assetIds));
const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity);
const album = await this.albumRepository.create({
@@ -135,7 +135,7 @@ export class AlbumService {
}
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id);
await this.access.requirePermission(auth, AccessPermission.ALBUM_UPDATE, id);
const album = await this.findOrFail(id, { withAssets: true });
@@ -158,7 +158,7 @@ export class AlbumService {
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id);
await this.access.requirePermission(auth, AccessPermission.ALBUM_DELETE, id);
const album = await this.findOrFail(id, { withAssets: false });
@@ -167,7 +167,7 @@ export class AlbumService {
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false });
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
await this.access.requirePermission(auth, AccessPermission.ALBUM_READ, id);
const results = await addAssets(
auth,
@@ -190,12 +190,12 @@ export class AlbumService {
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false });
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
await this.access.requirePermission(auth, AccessPermission.ALBUM_READ, id);
const results = await removeAssets(
auth,
{ accessRepository: this.accessRepository, repository: this.albumRepository },
{ id, assetIds: dto.ids, permissions: [Permission.ASSET_SHARE, Permission.ALBUM_REMOVE_ASSET] },
{ id, assetIds: dto.ids, permissions: [AccessPermission.ASSET_SHARE, AccessPermission.ALBUM_REMOVE_ASSET] },
);
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
@@ -210,7 +210,7 @@ export class AlbumService {
}
async addUsers(auth: AuthDto, id: string, dto: AddUsersDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
await this.access.requirePermission(auth, AccessPermission.ALBUM_SHARE, id);
const album = await this.findOrFail(id, { withAssets: false });
@@ -259,7 +259,7 @@ export class AlbumService {
// non-admin can remove themselves
if (auth.user.id !== userId) {
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
await this.access.requirePermission(auth, AccessPermission.ALBUM_SHARE, id);
}
await this.albumRepository.update({

View File

@@ -5,7 +5,7 @@ import {
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import {
AssetBulkUploadCheckResponseDto,
@@ -78,7 +78,7 @@ export class AssetServiceV1 {
try {
const libraryId = await this.getLibraryId(auth, dto.libraryId);
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
await this.access.requirePermission(auth, AccessPermission.ASSET_UPLOAD, libraryId);
this.requireQuota(auth, file.size);
if (livePhotoFile) {
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
@@ -111,13 +111,13 @@ export class AssetServiceV1 {
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
await this.access.requirePermission(auth, AccessPermission.TIMELINE_READ, userId);
const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
return assets.map((asset) => mapAsset(asset, { withStack: true, auth }));
}
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
await this.access.requirePermission(auth, AccessPermission.ASSET_VIEW, assetId);
const asset = await this.assetRepositoryV1.get(assetId);
if (!asset) {
@@ -135,7 +135,7 @@ export class AssetServiceV1 {
public async serveFile(auth: AuthDto, assetId: string, dto: ServeFileDto): Promise<ImmichFileResponse> {
// this is not quite right as sometimes this returns the original still
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
await this.access.requirePermission(auth, AccessPermission.ASSET_VIEW, assetId);
const asset = await this.assetRepository.getById(assetId);
if (!asset) {

View File

@@ -3,7 +3,7 @@ import _ from 'lodash';
import { DateTime, Duration } from 'luxon';
import { extname } from 'node:path';
import sanitize from 'sanitize-filename';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import {
@@ -210,7 +210,7 @@ export class AssetService {
}
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_READ, id);
await this.access.requirePermission(auth, AccessPermission.ASSET_READ, id);
const asset = await this.assetRepository.getById(id, {
exifInfo: true,
@@ -250,7 +250,7 @@ export class AssetService {
}
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
await this.access.requirePermission(auth, AccessPermission.ASSET_UPDATE, id);
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
@@ -273,7 +273,7 @@ export class AssetService {
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto;
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids);
await this.access.requirePermission(auth, AccessPermission.ASSET_UPDATE, ids);
// TODO: refactor this logic into separate API calls POST /stack, PUT /stack, etc.
const stackIdsToCheckForDelete: string[] = [];
@@ -289,7 +289,7 @@ export class AssetService {
);
} else if (options.stackParentId) {
//Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId);
await this.access.requirePermission(auth, AccessPermission.ASSET_UPDATE, options.stackParentId);
const primaryAsset = await this.assetRepository.getById(options.stackParentId, { stack: { assets: true } });
if (!primaryAsset) {
throw new BadRequestException('Asset not found for given stackParentId');
@@ -418,7 +418,7 @@ export class AssetService {
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto;
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
await this.access.requirePermission(auth, AccessPermission.ASSET_DELETE, ids);
if (force) {
await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.ASSET_DELETION, data: { id } })));
@@ -430,8 +430,8 @@ export class AssetService {
async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise<void> {
const { oldParentId, newParentId } = dto;
await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId);
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId);
await this.access.requirePermission(auth, AccessPermission.ASSET_READ, oldParentId);
await this.access.requirePermission(auth, AccessPermission.ASSET_UPDATE, newParentId);
const childIds: string[] = [];
const oldParent = await this.assetRepository.getById(oldParentId, {
@@ -464,7 +464,7 @@ export class AssetService {
}
async run(auth: AuthDto, dto: AssetJobsDto) {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds);
await this.access.requirePermission(auth, AccessPermission.ASSET_UPDATE, dto.assetIds);
const jobs: JobItem[] = [];

View File

@@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { resolve } from 'node:path';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import {
AuditDeletesDto,
@@ -51,7 +51,7 @@ export class AuditService {
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
await this.access.requirePermission(auth, AccessPermission.TIMELINE_READ, userId);
const audits = await this.repository.getAfter(dto.after, {
userIds: [userId],

View File

@@ -55,6 +55,7 @@ const oauthUserWithDefaultQuota = {
oauthId: sub,
quotaSizeInBytes: 1_073_741_824,
storageLabel: null,
permissions: expect.any(Array),
};
describe('AuthService', () => {
@@ -492,6 +493,7 @@ describe('AuthService', () => {
oauthId: sub,
quotaSizeInBytes: null,
storageLabel: null,
permissions: expect.any(Array),
});
});
@@ -512,6 +514,7 @@ describe('AuthService', () => {
oauthId: sub,
quotaSizeInBytes: 5_368_709_120,
storageLabel: null,
permissions: expect.any(Array),
});
});
});

View File

@@ -15,6 +15,7 @@ import { AccessCore } from 'src/cores/access.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core';
import {
ALL_PERMISSIONS,
AuthDto,
ChangePasswordDto,
ImmichCookie,
@@ -25,6 +26,7 @@ import {
OAuthCallbackDto,
OAuthConfigDto,
SignUpDto,
USER_PERMISSIONS,
mapLoginResponse,
} from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
@@ -143,6 +145,7 @@ export class AuthService {
name: dto.name,
password: dto.password,
storageLabel: 'admin',
permissions: ALL_PERMISSIONS,
});
return mapUser(admin);
@@ -238,6 +241,7 @@ export class AuthService {
oauthId: profile.sub,
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
storageLabel: storageLabel || null,
permissions: USER_PERMISSIONS,
});
}

View File

@@ -1,6 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { parse } from 'node:path';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
@@ -26,7 +26,7 @@ export class DownloadService {
}
async downloadFile(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id);
await this.access.requirePermission(auth, AccessPermission.ASSET_DOWNLOAD, id);
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) {
@@ -81,7 +81,7 @@ export class DownloadService {
}
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds);
await this.access.requirePermission(auth, AccessPermission.ASSET_DOWNLOAD, dto.assetIds);
const zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds);
@@ -117,20 +117,20 @@ export class DownloadService {
if (dto.assetIds) {
const assetIds = dto.assetIds;
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds);
await this.access.requirePermission(auth, AccessPermission.ASSET_DOWNLOAD, assetIds);
const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
}
if (dto.albumId) {
const albumId = dto.albumId;
await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId);
await this.access.requirePermission(auth, AccessPermission.ALBUM_DOWNLOAD, albumId);
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
}
if (dto.userId) {
const userId = dto.userId;
await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId);
await this.access.requirePermission(auth, AccessPermission.TIMELINE_DOWNLOAD, userId);
return usePagination(PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
);

View File

@@ -1,5 +1,5 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
@@ -25,7 +25,7 @@ export class MemoryService {
}
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
await this.access.requirePermission(auth, Permission.MEMORY_READ, id);
await this.access.requirePermission(auth, AccessPermission.MEMORY_READ, id);
const memory = await this.findOrFail(id);
return mapMemory(memory);
}
@@ -34,7 +34,7 @@ export class MemoryService {
// TODO validate type/data combination
const assetIds = dto.assetIds || [];
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds);
const allowedAssetIds = await this.access.checkAccess(auth, AccessPermission.ASSET_SHARE, assetIds);
const memory = await this.repository.create({
ownerId: auth.user.id,
type: dto.type,
@@ -49,7 +49,7 @@ export class MemoryService {
}
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id);
await this.access.requirePermission(auth, AccessPermission.MEMORY_WRITE, id);
const memory = await this.repository.update({
id,
@@ -62,12 +62,12 @@ export class MemoryService {
}
async remove(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id);
await this.access.requirePermission(auth, AccessPermission.MEMORY_DELETE, id);
await this.repository.delete(id);
}
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
await this.access.requirePermission(auth, Permission.MEMORY_READ, id);
await this.access.requirePermission(auth, AccessPermission.MEMORY_READ, id);
const repos = { accessRepository: this.accessRepository, repository: this.repository };
const results = await addAssets(auth, repos, { id, assetIds: dto.ids });
@@ -81,10 +81,10 @@ export class MemoryService {
}
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id);
await this.access.requirePermission(auth, AccessPermission.MEMORY_WRITE, id);
const repos = { accessRepository: this.accessRepository, repository: this.repository };
const permissions = [Permission.ASSET_SHARE];
const permissions = [AccessPermission.ASSET_SHARE];
const results = await removeAssets(auth, repos, { id, assetIds: dto.ids, permissions });
const hasSuccess = results.find(({ success }) => success);

View File

@@ -1,5 +1,5 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.dto';
@@ -48,7 +48,7 @@ export class PartnerService {
}
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById);
await this.access.requirePermission(auth, AccessPermission.PARTNER_UPDATE, sharedById);
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });

View File

@@ -1,6 +1,6 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
@@ -101,7 +101,7 @@ export class PersonService {
}
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
await this.access.requirePermission(auth, AccessPermission.PERSON_WRITE, personId);
const person = await this.findOrFail(personId);
const result: PersonResponseDto[] = [];
const changeFeaturePhoto: string[] = [];
@@ -109,7 +109,7 @@ export class PersonService {
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
for (const face of faces) {
await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id);
await this.access.requirePermission(auth, AccessPermission.PERSON_CREATE, face.id);
if (person.faceAssetId === null) {
changeFeaturePhoto.push(person.id);
}
@@ -130,9 +130,9 @@ export class PersonService {
}
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
await this.access.requirePermission(auth, AccessPermission.PERSON_WRITE, personId);
await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id);
await this.access.requirePermission(auth, AccessPermission.PERSON_CREATE, dto.id);
const face = await this.repository.getFaceById(dto.id);
const person = await this.findOrFail(personId);
@@ -148,7 +148,7 @@ export class PersonService {
}
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id);
await this.access.requirePermission(auth, AccessPermission.ASSET_READ, dto.id);
const faces = await this.repository.getFaces(dto.id);
return faces.map((asset) => mapFaces(asset, auth));
}
@@ -175,17 +175,17 @@ export class PersonService {
}
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
await this.access.requirePermission(auth, AccessPermission.PERSON_READ, id);
return this.findOrFail(id).then(mapPerson);
}
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
await this.access.requirePermission(auth, AccessPermission.PERSON_READ, id);
return this.repository.getStatistics(id);
}
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
await this.access.requirePermission(auth, AccessPermission.PERSON_READ, id);
const person = await this.repository.getById(id);
if (!person || !person.thumbnailPath) {
throw new NotFoundException();
@@ -199,7 +199,7 @@ export class PersonService {
}
async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
await this.access.requirePermission(auth, Permission.PERSON_READ, id);
await this.access.requirePermission(auth, AccessPermission.PERSON_READ, id);
const assets = await this.repository.getAssets(id);
return assets.map((asset) => mapAsset(asset));
}
@@ -214,13 +214,13 @@ export class PersonService {
}
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, id);
await this.access.requirePermission(auth, AccessPermission.PERSON_WRITE, id);
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
// TODO: set by faceId directly
let faceId: string | undefined = undefined;
if (assetId) {
await this.access.requirePermission(auth, Permission.ASSET_READ, assetId);
await this.access.requirePermission(auth, AccessPermission.ASSET_READ, assetId);
const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
if (!face) {
throw new BadRequestException('Invalid assetId for feature face');
@@ -555,13 +555,13 @@ export class PersonService {
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
const mergeIds = dto.ids;
await this.access.requirePermission(auth, Permission.PERSON_WRITE, id);
await this.access.requirePermission(auth, AccessPermission.PERSON_WRITE, id);
let primaryPerson = await this.findOrFail(id);
const primaryName = primaryPerson.name || primaryPerson.id;
const results: BulkIdResponseDto[] = [];
const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds);
const allowedIds = await this.access.checkAccess(auth, AccessPermission.PERSON_MERGE, mergeIds);
for (const mergeId of mergeIds) {
const hasAccess = allowedIds.has(mergeId);

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { IAccessRepository } from 'src/interfaces/access.interface';
@@ -25,7 +25,7 @@ export class SessionService {
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id);
await this.access.requirePermission(auth, AccessPermission.AUTH_DEVICE_DELETE, id);
await this.sessionRepository.delete(id);
}

View File

@@ -1,5 +1,5 @@
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -59,7 +59,7 @@ export class SharedLinkService {
if (!dto.albumId) {
throw new BadRequestException('Invalid albumId');
}
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId);
await this.access.requirePermission(auth, AccessPermission.ALBUM_SHARE, dto.albumId);
break;
}
@@ -68,7 +68,7 @@ export class SharedLinkService {
throw new BadRequestException('Invalid assetIds');
}
await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds);
await this.access.requirePermission(auth, AccessPermission.ASSET_SHARE, dto.assetIds);
break;
}
@@ -129,7 +129,7 @@ export class SharedLinkService {
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
const allowedAssetIds = await this.access.checkAccess(auth, AccessPermission.ASSET_SHARE, notPresentAssetIds);
const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) {

View File

@@ -2,7 +2,7 @@ import { Inject } from '@nestjs/common';
import _ from 'lodash';
import { DateTime } from 'luxon';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
@@ -26,7 +26,7 @@ export class SyncService {
async getAllAssetsForUserFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
await this.access.requirePermission(auth, AccessPermission.TIMELINE_READ, userId);
const assets = await this.assetRepository.getAllForUserFullSync({
ownerId: userId,
lastCreationDate: dto.lastCreationDate,
@@ -39,7 +39,7 @@ export class SyncService {
}
async getChangesForDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds);
await this.access.requirePermission(auth, AccessPermission.TIMELINE_READ, dto.userIds);
const partner = await this.partnerRepository.getAll(auth.user.id);
const userIds = [auth.user.id, ...partner.filter((p) => p.sharedWithId == auth.user.id).map((p) => p.sharedById)];
userIds.sort();

View File

@@ -1,5 +1,5 @@
import { BadRequestException, Inject } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
@@ -59,15 +59,15 @@ export class TimelineService {
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
if (dto.albumId) {
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
await this.accessCore.requirePermission(auth, AccessPermission.ALBUM_READ, [dto.albumId]);
} else {
dto.userId = dto.userId || auth.user.id;
}
if (dto.userId) {
await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
await this.accessCore.requirePermission(auth, AccessPermission.TIMELINE_READ, [dto.userId]);
if (dto.isArchived !== false) {
await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
await this.accessCore.requirePermission(auth, AccessPermission.ARCHIVE_READ, [dto.userId]);
}
}

View File

@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { IAccessRepository } from 'src/interfaces/access.interface';
@@ -23,7 +23,7 @@ export class TrashService {
async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
const { ids } = dto;
await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids);
await this.access.requirePermission(auth, AccessPermission.ASSET_RESTORE, ids);
await this.restoreAndSend(auth, ids);
}

View File

@@ -3,7 +3,7 @@ import { DateTime } from 'luxon';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { AuthDto, presetToPermissions } from 'src/dtos/auth.dto';
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { UserEntity, UserStatus } from 'src/entities/user.entity';
@@ -60,8 +60,9 @@ export class UserService {
return this.findOrFail(auth.user.id, {}).then(mapUser);
}
create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
return this.userCore.createUser(createUserDto).then(mapUser);
create(dto: CreateUserDto): Promise<UserResponseDto> {
const permissions = presetToPermissions(dto);
return this.userCore.createUser({ ...dto, permissions }).then(mapUser);
}
async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> {
@@ -71,7 +72,8 @@ export class UserService {
await this.userRepository.syncUsage(dto.id);
}
return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser);
const permissions = presetToPermissions(dto);
return this.userCore.updateUser(auth.user, dto.id, { ...dto, permissions }).then(mapUser);
}
async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {

View File

@@ -1,4 +1,4 @@
import { AccessCore, Permission } from 'src/cores/access.core';
import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { IAccessRepository } from 'src/interfaces/access.interface';
@@ -20,7 +20,7 @@ export const addAssets = async (
const existingAssetIds = await repository.getAssetIds(dto.id, dto.assetIds);
const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id));
const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
const allowedAssetIds = await access.checkAccess(auth, AccessPermission.ASSET_SHARE, notPresentAssetIds);
const results: BulkIdResponseDto[] = [];
for (const assetId of dto.assetIds) {
@@ -50,7 +50,7 @@ export const addAssets = async (
export const removeAssets = async (
auth: AuthDto,
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },
dto: { id: string; assetIds: string[]; permissions: Permission[] },
dto: { id: string; assetIds: string[]; permissions: AccessPermission[] },
) => {
const { accessRepository, repository } = repositories;
const access = AccessCore.create(accessRepository);

View File

@@ -103,10 +103,6 @@ const patchOpenAPI = (document: OpenAPIObject) => {
continue;
}
if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) {
delete operation.security;
}
if (operation.summary === '') {
delete operation.summary;
}

View File

@@ -43,7 +43,7 @@
'text-immich-primary dark:text-immich-dark-primary enabled:dark:hover:bg-immich-dark-primary/10 enabled:hover:bg-immich-primary/10',
'light-red': 'bg-[#F9DEDC] text-[#410E0B] enabled:hover:bg-red-50',
red: 'bg-red-500 text-white enabled:hover:bg-red-400',
green: 'bg-green-600 text-white enabled:hover:bg-green-400/90',
green: 'bg-green-500 text-gray-800 enabled:hover:bg-green-400/90',
gray: 'bg-gray-500 dark:bg-gray-200 enabled:hover:bg-gray-500/75 enabled:dark:hover:bg-gray-200/80 text-white dark:text-immich-dark-gray',
'transparent-gray':
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',

View File

@@ -15,7 +15,6 @@
mdiMagnify,
mdiMap,
mdiTrashCanOutline,
mdiTuneVariant,
} from '@mdi/js';
import LoadingSpinner from '../loading-spinner.svelte';
import StatusBox from '../status-box.svelte';
@@ -146,12 +145,6 @@
</svelte:fragment>
</SideBarLink>
{/if}
<div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
<p class="hidden p-6 group-hover:sm:block md:block">AUTOMATION</p>
<hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
</div>
<SideBarLink title="Workflows" routeId="/(user)/workflows" icon={mdiTuneVariant} />
</nav>
<!-- Status Box -->

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { mdiPencil, mdiTrashCan } from '@mdi/js';
</script>
<div
class="bg-zinc-300 dark:bg-zinc-800 text-black dark:text-white min-h-[60px] grid grid-cols-[15%_70%_15%] place-items-center place-content-center rounded-2xl mx-4 mt-2 p-2"
>
<p class="col-start-2 col-span-1">Add to album "RANDOM"</p>
<div class="col-start-3 col-span-1 flex gap-2 justify-self-end">
<CircleIconButton size="20" padding="2" title="Remove rule" icon={mdiTrashCan} />
<CircleIconButton size="20" padding="2" title="Edit rule" icon={mdiPencil} />
</div>
</div>

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { mdiPencil, mdiTrashCan } from '@mdi/js';
</script>
<div
class="bg-zinc-200 text-black dark:bg-zinc-700 dark:text-white min-h-[60px] grid grid-cols-[15%_70%_15%] place-items-center place-content-center rounded-2xl mx-4 mt-2 p-2"
>
<p class="col-start-2 col-span-1">And has Alex and Henry and Nate</p>
<div class="col-start-3 col-span-1 flex gap-2 justify-self-end">
<CircleIconButton size="20" padding="2" title="Remove rule" icon={mdiTrashCan} />
<CircleIconButton size="20" padding="2" title="Edit rule" icon={mdiPencil} />
</div>
</div>

View File

@@ -1,5 +0,0 @@
<div
class="bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-black min-h-[60px] flex place-items-center place-content-center mx-4 mt-2 rounded-2xl"
>
When an asset is uploaded
</div>

View File

@@ -1,10 +0,0 @@
<script lang="ts">
// export let onSelect: () => void;
</script>
<div
class="rounded-3xl mr-2 my-4 min-h-[60px] grid grid-cols-[32px_1fr] gap-2 place-items-center p-4 place-content-center bg-gray-100 hover:bg-gray-200 dark:bg-gray-900 hover:dark:bg-gray-800"
>
<div class="w-4 h-4 rounded-full bg-green-400"></div>
<p class="dark:text-gray-300">Add this photo to every albums and notify everybody about this glorious asset</p>
</div>

View File

@@ -1,82 +0,0 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import WorkflowActionCard from '$lib/components/workflow-page/editor/workflow-action-card.svelte';
import WorkflowRuleCard from '$lib/components/workflow-page/editor/workflow-rule-card.svelte';
import WorkflowTriggerCard from '$lib/components/workflow-page/editor/workflow-trigger-card.svelte';
import { mdiPlus } from '@mdi/js';
</script>
<section class="h-full overflow-scroll">
<div
id="workflow-control-bar"
class="sticky top-0 flex justify-between place-items-center border-b border-gray-200 dark:border-gray-800 p-4 bg-zinc-50 dark:bg-zinc-900 z-20"
>
<p class="uppercase text-lg dark:text-white font-medium">
Add this photo to every albums and notify everybody about this glorious asset
</p>
<div class="flex gap-2">
<Button size="sm" color="red">Discard</Button>
<Button size="sm">Disable</Button>
<Button size="sm" color="green">Save</Button>
</div>
</div>
<div id="workflows-selection">
<!-- TRIGGER BLOCK -->
<div class="translate-y-3">
<p class="pl-4 text-xs dark:text-gray-300">TRIGGER</p>
<WorkflowTriggerCard />
</div>
<!-- VISUAL CONNECTOR -->
<div class="relative w-full grid grid-cols-3 place-items-center place-content-center z-10">
<p class="col-start-1 col-span-1 justify-self-start self-end pl-4 pb-6 text-xs dark:text-gray-300">RULES</p>
<div class="col-start-2 col-span-1 flex flex-col place-items-center">
<div
class="rounded-full border-[6px] border-immich-primary dark:border-immich-dark-primary h-[20px] w-[20px] bg-white translate-y-1"
></div>
<div
class="h-[60px] w-[5px] bg-white bg-gradient-to-b from-immich-primary dark:from-immich-dark-primary via-zinc-300 dark:via-gray-600 dark:to-zinc-700"
></div>
<div
class="rounded-full border-[6px] border-zinc-200 dark:border-gray-700 h-[20px] w-[20px] bg-white -translate-y-1"
></div>
</div>
</div>
<!-- RULES BLOCK -->
<div id="rule-block" class="-translate-y-6">
<WorkflowRuleCard />
<WorkflowRuleCard />
<div
class="border-2 dark:border-gray-700 dark:text-white min-h-[60px] flex place-items-center place-content-center rounded-2xl mx-4 mt-2"
>
<span><Icon path={mdiPlus} /></span> ADD RULE
</div>
</div>
<!-- VISUAL CONNECTOR -->
<div class="relative w-full grid grid-cols-3 place-items-center place-content-center -translate-y-9 z-10">
<p class="col-start-1 col-span-1 justify-self-start self-end pl-4 pb-6 text-xs dark:text-gray-300">ACTIONS</p>
<div class="col-start-2 col-span-1 flex flex-col place-items-center">
<div class="rounded-full border-[6px] border-gray-900 h-[20px] w-[20px] bg-white translate-y-1"></div>
<div class="h-[60px] w-[5px] bg-white bg-gradient-to-b from-gray-900 via-indigo-800 to-gray-700"></div>
<div class="rounded-full border-[6px] border-gray-700 h-[20px] w-[20px] bg-white -translate-y-1"></div>
</div>
</div>
<!-- ACTION BLOCK -->
<div id="action-block" class="-translate-y-14">
<WorkflowActionCard />
<WorkflowActionCard />
<div
class="border-2 dark:border-gray-500 dark:text-white min-h-[60px] flex place-items-center place-content-center rounded-2xl mx-4 mt-2"
>
<span><Icon path={mdiPlus} /></span> ADD ACTION
</div>
</div>
</div>
</section>

View File

@@ -1,17 +0,0 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import WorkflowCard from '$lib/components/workflow-page/workflow-card.svelte';
</script>
<section id="workflow-list" class="border-r border-gray-200 dark:border-gray-800 h-full relative overflow-scroll pr-2">
<div class="sticky top-0 dark:bg-immich-dark-bg flex justify-between place-items-center pr-2 py-4 bg-immich-bg">
<p class="text-xs dark:text-white">CURRENT WORKFLOWS</p>
<Button size="sm">New Workflow</Button>
</div>
<div>
{#each Array.from({ length: 50 }) as _}
<WorkflowCard />
{/each}
</div>
</section>

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import WorkflowEditor from '$lib/components/workflow-page/workflow-editor.svelte';
import WorkflowList from '$lib/components/workflow-page/workflow-list.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<UserPageLayout title={data.meta.title} scrollbar={false}>
<section class="grid grid-cols-[25%_1fr] h-full">
<WorkflowList />
<WorkflowEditor />
</section>
</UserPageLayout>

View File

@@ -1,12 +0,0 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
return {
meta: {
title: 'Workflows',
},
};
}) satisfies PageLoad;