Compare commits

...

12 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
Alex be4a783845 fix(web): wrong month on timeline scrollbar cursor (#8996)
* fix(web): wrong month on timeline scrollbar cursor

* revert unnesessary change
2024-04-22 06:22:59 -05:00
Mert c30cd3b378 chore: test more formats in e2e (#9001) 2024-04-22 01:35:27 -04:00
TruongSinh Tran-Nguyen 0d3cc28f45 feat(web): support 360 video (equirectangular) (#8762)
* [web]: support 360 video

* lint

* lint

* fix typing

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-21 19:14:54 +00:00
Conner Hnatiuk f004487be0 fix(web): trash page now auto refreshes (#8978)
* fix(web): the trash page now auto refreshes when restore all or empty trash is clicked. Also shows number of assets affected.

* formatting
2024-04-21 14:07:17 -05:00
clementdelestre 21231d53a5 feat(mobile): add i18n in multiselect-grid and update translation (en and fr) (#8993)
* add i18n in multiselect grid (en-fr)

* add FR translations from (haptic feedback)

* revert settings
2024-04-21 13:26:19 -05:00
Daniel Dietzler a99862120d feat: mobile label for renovate pull requests (#8991)
mobile lable for renovate pull requests
2024-04-21 13:11:03 -05:00
shenlong 776023b149 dep(mobile): upgrade gradle (#8409)
* dep(mobile): upgrade gradle

* chore(deps): update kotlin & guava

* build: change java version and flutter test version

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2024-04-20 23:07:32 -05:00
martin 7d4187962a feat(web): new look option for slideshow (#8924)
feat: new look option for slideshow
2024-04-20 23:06:49 -05:00
Jason Rasmussen a93534fc3c refactor(server): session interface types (#8977) 2024-04-20 23:45:55 -04:00
Alex cef84f6ced chore(mobile): override appbundle on PlayStore before getting released (#8960) 2024-04-20 19:56:03 -05:00
94 changed files with 1404 additions and 382 deletions
+5 -5
View File
@@ -37,15 +37,15 @@ jobs:
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: "zulu" distribution: 'zulu'
java-version: "11.0.21+9" java-version: '17'
cache: "gradle" cache: 'gradle'
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: 'stable'
flutter-version: "3.19.3" flutter-version: '3.19.3'
cache: true cache: true
- name: Create the Keystore - name: Create the Keystore
+1 -1
View File
@@ -208,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.16.9' flutter-version: '3.19.3'
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1
+8 -5
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. 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. 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) 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 ```sql
// Reassign albums -- reassign albums
UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>'; UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
// Reassign people -- reassign people
UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>'; UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
// reassign assets -- reassign assets
UPDATE assets SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>' UPDATE assets SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>'
AND CHECKSUM NOT IN (SELECT CHECKSUM FROM assets WHERE "ownerId" = '<destinationId>'); 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. 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.
+106
View File
@@ -572,6 +572,22 @@ describe('/asset', () => {
} }
const tests = [ const tests = [
{
input: 'formats/avif/8bit-sRGB.avif',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.avif',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
exifImageWidth: 1617,
fileSizeInByte: 862_424,
latitude: null,
longitude: null,
},
},
},
{ {
input: 'formats/jpg/el_torcal_rocks.jpg', input: 'formats/jpg/el_torcal_rocks.jpg',
expected: { expected: {
@@ -596,6 +612,22 @@ describe('/asset', () => {
}, },
}, },
}, },
{
input: 'formats/jxl/8bit-sRGB.jxl',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.jxl',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
exifImageWidth: 1440,
fileSizeInByte: 1_780_777,
latitude: null,
longitude: null,
},
},
},
{ {
input: 'formats/heic/IMG_2682.heic', input: 'formats/heic/IMG_2682.heic',
expected: { expected: {
@@ -681,6 +713,80 @@ describe('/asset', () => {
}, },
}, },
}, },
{
input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '4_3.rw2',
resized: true,
fileCreatedAt: '2018-05-10T08:42:37.842Z',
exifInfo: {
make: 'Panasonic',
model: 'DMC-GH4',
exifImageHeight: 3456,
exifImageWidth: 4608,
exposureTime: '1/100',
fNumber: 3.2,
focalLength: 35,
iso: 400,
fileSizeInByte: 19_587_072,
dateTimeOriginal: '2018-05-10T08:42:37.842Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '12bit-compressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-09-27T10:51:44.000Z',
exifInfo: {
make: 'SONY',
model: 'ILCE-6300',
exifImageHeight: 4024,
exifImageWidth: 6048,
exposureTime: '1/320',
fNumber: 8,
focalLength: 97,
iso: 100,
lensModel: 'E PZ 18-105mm F4 G OSS',
fileSizeInByte: 25_001_984,
dateTimeOriginal: '2016-09-27T10:51:44.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '14bit-uncompressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-01-08T15:08:01.000Z',
exifInfo: {
make: 'SONY',
model: 'ILCE-7M2',
exifImageHeight: 4024,
exifImageWidth: 6048,
exposureTime: '1.3',
fNumber: 22,
focalLength: 25,
iso: 100,
lensModel: 'E 25mm F2',
fileSizeInByte: 49_512_448,
dateTimeOriginal: '2016-01-08T15:08:01.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
]; ];
for (const { input, expected } of tests) { for (const { input, expected } of tests) {
+3 -3
View File
@@ -8,7 +8,7 @@ import {
} from '@immich/sdk'; } from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs'; import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils'; import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
import request from 'supertest'; 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 = {}) => const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) }); scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/library', () => { describe.skip('/library', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user: LoginResponseDto; let user: LoginResponseDto;
let library: LibraryResponseDto; let library: LibraryResponseDto;
@@ -28,7 +28,7 @@ describe('/library', () => {
await utils.resetDatabase(); await utils.resetDatabase();
admin = await utils.adminSetup(); admin = await utils.adminSetup();
await utils.resetAdminConfig(admin.accessToken); 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 }); library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
websocket = await utils.connectWebsocket(admin.accessToken); websocket = await utils.connectWebsocket(admin.accessToken);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`); utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
+14 -1
View File
@@ -135,7 +135,7 @@ describe('/user', () => {
expect(body).toEqual(errorDto.unauthorized); 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 () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/user`) .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 () => { it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/user`) .post(`/user`)
@@ -154,6 +165,7 @@ describe('/user', () => {
email: 'user5@immich.cloud', email: 'user5@immich.cloud',
password: 'password123', password: 'password123',
name: 'Immich', name: 'Immich',
permissionPreset: 'user',
}) })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({ expect(body).toMatchObject({
@@ -172,6 +184,7 @@ describe('/user', () => {
password: 'Password123', password: 'Password123',
name: 'No Memories', name: 'No Memories',
memoriesEnabled: false, memoriesEnabled: false,
permissionPreset: 'user',
}) })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({ expect(body).toMatchObject({
+7 -1
View File
@@ -1,4 +1,4 @@
import { UserAvatarColor } from '@immich/sdk'; import { PermissionPreset, UserAvatarColor } from '@immich/sdk';
export const uuidDto = { export const uuidDto = {
invalid: 'invalid-uuid', invalid: 'invalid-uuid',
@@ -26,33 +26,39 @@ export const createUserDto = {
email: `${key}@immich.cloud`, email: `${key}@immich.cloud`,
name: `Generated User ${key}`, name: `Generated User ${key}`,
password: `password-${key}`, password: `password-${key}`,
permissionPreset: PermissionPreset.User,
}; };
}, },
user1: { user1: {
email: 'user1@immich.cloud', email: 'user1@immich.cloud',
name: 'User 1', name: 'User 1',
password: 'password1', password: 'password1',
permissionPreset: PermissionPreset.User,
}, },
user2: { user2: {
email: 'user2@immich.cloud', email: 'user2@immich.cloud',
name: 'User 2', name: 'User 2',
password: 'password12', password: 'password12',
permissionPreset: PermissionPreset.User,
}, },
user3: { user3: {
email: 'user3@immich.cloud', email: 'user3@immich.cloud',
name: 'User 3', name: 'User 3',
permissionPreset: PermissionPreset.User,
password: 'password123', password: 'password123',
}, },
user4: { user4: {
email: 'user4@immich.cloud', email: 'user4@immich.cloud',
name: 'User 4', name: 'User 4',
password: 'password123', password: 'password123',
permissionPreset: PermissionPreset.User,
}, },
userQuota: { userQuota: {
email: 'user-quota@immich.cloud', email: 'user-quota@immich.cloud',
name: 'User Quota', name: 'User Quota',
password: 'password-quota', password: 'password-quota',
quotaSizeInBytes: 512, quotaSizeInBytes: 512,
permissionPreset: PermissionPreset.User,
}, },
}; };
+85
View File
@@ -77,6 +77,91 @@ export const signupResponseDto = {
quotaUsageInBytes: 0, quotaUsageInBytes: 0,
quotaSizeInBytes: null, quotaSizeInBytes: null,
status: 'active', 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',
],
}, },
}; };
+16 -16
View File
@@ -1,14 +1,14 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id "kotlin-kapt"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader -> localPropertiesFile.withInputStream { localProperties.load(it) }
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
} }
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -21,18 +21,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
} }
android { android {
compileSdkVersion 34 compileSdkVersion 34
@@ -50,7 +44,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.alextran.immich" applicationId "app.alextran.immich"
minSdkVersion 26 minSdkVersion 26
targetSdkVersion 33 targetSdkVersion 33
@@ -88,6 +81,13 @@ flutter {
} }
dependencies { dependencies {
def kotlin_version = '1.9.23'
def kotlin_coroutines_version = '1.8.0'
def work_version = '2.9.0'
def concurrent_version = '1.1.0'
def guava_version = '33.1.0-android'
def glide_version = '4.16.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version"
@@ -276,7 +276,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
private const val NOTIFICATION_DEFAULT_TITLE = "Immich" private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1 private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2 private const val NOTIFICATION_ERROR_ID = 2
private const val NOTIFICATION_DETAIL_ID = 3 private const val NOTIFICATION_DETAIL_ID = 3
private const val ONE_MINUTE = 60000L private const val ONE_MINUTE = 60000L
@@ -304,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
if (workInfoList != null) { if (workInfoList != null) {
for (workInfo in workInfoList) { for (workInfo in workInfoList) {
if (workInfo.getState() == WorkInfo.State.ENQUEUED) { if (workInfo.state == WorkInfo.State.ENQUEUED) {
val workRequest = buildWorkRequest(requireWifi, requireCharging) val workRequest = buildWorkRequest(requireWifi, requireCharging)
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
@@ -346,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.setRequiresCharging(requireCharging) .setRequiresCharging(requireCharging)
.build(); .build();
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
.setConstraints(constraints) .setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
@@ -359,4 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
} }
} }
private const val TAG = "BackupWorker" private const val TAG = "BackupWorker"
+4 -18
View File
@@ -1,21 +1,3 @@
buildscript {
ext.kotlin_version = '1.8.20'
ext.kotlin_coroutines_version = '1.7.1'
ext.work_version = '2.7.1'
ext.concurrent_version = '1.1.0'
ext.guava_version = '33.0.0-android'
ext.glide_version = '4.14.2'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects { allprojects {
repositories { repositories {
google() google()
@@ -34,3 +16,7 @@ subprojects {
tasks.register("clean", Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }
tasks.named('wrapper') {
distributionType = Wrapper.DistributionType.ALL
}
+1 -1
View File
@@ -35,7 +35,7 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 134, "android.injected.version.code" => 136,
"android.injected.version.name" => "1.102.3", "android.injected.version.name" => "1.102.3",
} }
) )
+3 -3
View File
@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000425"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000261">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="63.658719"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="32.48099">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="36.312519"> <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.236974">
</testcase> </testcase>
+3 -2
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80 distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12
+23 -8
View File
@@ -1,11 +1,26 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties") includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
def properties = new Properties()
assert localPropertiesFile.exists() repositories {
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk") plugins {
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" id "com.android.application" version "7.4.2" apply false
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false
}
include ":app"
+1
View File
@@ -296,6 +296,7 @@
"motion_photos_page_title": "Motion Photos", "motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"no_assets_to_show" : "No assets to show",
"notification_permission_dialog_cancel": "Cancel", "notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings", "notification_permission_dialog_settings": "Settings",
+4 -1
View File
@@ -296,6 +296,7 @@
"motion_photos_page_title": "Photos avec mouvement", "motion_photos_page_title": "Photos avec mouvement",
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.", "multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.", "multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
"no_assets_to_show" : "Aucun élément à afficher",
"notification_permission_dialog_cancel": "Annuler", "notification_permission_dialog_cancel": "Annuler",
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
"notification_permission_dialog_settings": "Paramètres", "notification_permission_dialog_settings": "Paramètres",
@@ -509,5 +510,7 @@
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89", "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
"viewer_remove_from_stack": "Retirer de la pile", "viewer_remove_from_stack": "Retirer de la pile",
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal", "viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
"viewer_unstack": "Désempiler" "viewer_unstack": "Désempiler",
"haptic_feedback_title": "Retour haptique",
"haptic_feedback_switch": "Activer le retour haptique"
} }
@@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget {
const Center(child: ImmichLoadingIndicator()); const Center(child: ImmichLoadingIndicator());
Widget buildEmptyIndicator() => Widget buildEmptyIndicator() =>
emptyIndicator ?? const Center(child: Text("No assets to show")); emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr());
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
+1 -1
View File
@@ -1804,5 +1804,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.2.0 <4.0.0" dart: ">=3.3.0 <4.0.0"
flutter: ">=3.16.0" flutter: ">=3.16.0"
+2 -2
View File
@@ -2,10 +2,10 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.102.3+134 version: 1.102.3+136
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
dependencies: dependencies:
flutter: flutter:
+325 -19
View File
@@ -6428,15 +6428,6 @@
} }
}, },
"security": [ "security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
},
{ {
"bearer": [] "bearer": []
}, },
@@ -6564,15 +6555,6 @@
} }
}, },
"security": [ "security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
},
{ {
"bearer": [] "bearer": []
}, },
@@ -7975,6 +7957,103 @@
], ],
"type": "object" "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": { "BulkIdResponseDto": {
"properties": { "properties": {
"error": { "error": {
@@ -8284,6 +8363,15 @@
"password": { "password": {
"type": "string" "type": "string"
}, },
"permissionPreset": {
"$ref": "#/components/schemas/PermissionPreset"
},
"permissions": {
"items": {
"$ref": "#/components/schemas/AuthorizationPermission"
},
"type": "array"
},
"quotaSizeInBytes": { "quotaSizeInBytes": {
"format": "int64", "format": "int64",
"nullable": true, "nullable": true,
@@ -8300,7 +8388,8 @@
"required": [ "required": [
"email", "email",
"name", "name",
"password" "password",
"permissionPreset"
], ],
"type": "object" "type": "object"
}, },
@@ -9364,6 +9453,106 @@
"oauthId": { "oauthId": {
"type": "string" "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": { "profileImagePath": {
"type": "string" "type": "string"
}, },
@@ -9497,6 +9686,14 @@
], ],
"type": "object" "type": "object"
}, },
"PermissionPreset": {
"enum": [
"user",
"admin",
"custom"
],
"type": "string"
},
"PersonCreateDto": { "PersonCreateDto": {
"properties": { "properties": {
"birthDate": { "birthDate": {
@@ -11252,6 +11449,15 @@
"password": { "password": {
"type": "string" "type": "string"
}, },
"permissionPreset": {
"$ref": "#/components/schemas/PermissionPreset"
},
"permissions": {
"items": {
"$ref": "#/components/schemas/AuthorizationPermission"
},
"type": "array"
},
"quotaSizeInBytes": { "quotaSizeInBytes": {
"format": "int64", "format": "int64",
"nullable": true, "nullable": true,
@@ -11377,6 +11583,106 @@
"oauthId": { "oauthId": {
"type": "string" "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": { "profileImagePath": {
"type": "string" "type": "string"
}, },
@@ -71,6 +71,7 @@ export type UserResponseDto = {
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
name: string; name: string;
oauthId: 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; profileImagePath: string;
quotaSizeInBytes: number | null; quotaSizeInBytes: number | null;
quotaUsageInBytes: number | null; quotaUsageInBytes: number | null;
@@ -513,6 +514,7 @@ export type PartnerResponseDto = {
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
name: string; name: string;
oauthId: 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; profileImagePath: string;
quotaSizeInBytes: number | null; quotaSizeInBytes: number | null;
quotaUsageInBytes: number | null; quotaUsageInBytes: number | null;
@@ -1021,6 +1023,8 @@ export type CreateUserDto = {
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
name: string; name: string;
password: string; password: string;
permissionPreset: PermissionPreset;
permissions?: AuthorizationPermission[];
quotaSizeInBytes?: number | null; quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
storageLabel?: string | null; storageLabel?: string | null;
@@ -1033,6 +1037,8 @@ export type UpdateUserDto = {
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
name?: string; name?: string;
password?: string; password?: string;
permissionPreset?: PermissionPreset;
permissions?: AuthorizationPermission[];
quotaSizeInBytes?: number | null; quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
storageLabel?: string; storageLabel?: string;
@@ -3103,3 +3109,66 @@ export enum TimeBucketSize {
Day = "DAY", Day = "DAY",
Month = "MONTH" 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"
}
+2 -1
View File
@@ -27,7 +27,8 @@
"matchFileNames": ["mobile/**"], "matchFileNames": ["mobile/**"],
"groupName": "mobile", "groupName": "mobile",
"matchUpdateTypes": ["minor", "patch"], "matchUpdateTypes": ["minor", "patch"],
"schedule": "on tuesday" "schedule": "on tuesday",
"addLabels": ["📱mobile"]
}, },
{ {
"groupName": "exiftool", "groupName": "exiftool",
@@ -8,28 +8,30 @@ import {
ActivitySearchDto, ActivitySearchDto,
ActivityStatisticsResponseDto, ActivityStatisticsResponseDto,
} from 'src/dtos/activity.dto'; } 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 { Auth, Authenticated } from 'src/middleware/auth.guard';
import { ActivityService } from 'src/services/activity.service'; import { ActivityService } from 'src/services/activity.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Activity') @ApiTags('Activity')
@Controller('activity') @Controller('activity')
@Authenticated()
export class ActivityController { export class ActivityController {
constructor(private service: ActivityService) {} constructor(private service: ActivityService) {}
@Get() @Get()
@Authenticated(Permission.ACTIVITY_READ)
getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
return this.service.getAll(auth, dto); return this.service.getAll(auth, dto);
} }
@Get('statistics') @Get('statistics')
@Authenticated(Permission.ACTIVITY_READ)
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
return this.service.getStatistics(auth, dto); return this.service.getStatistics(auth, dto);
} }
@Post() @Post()
@Authenticated(Permission.ACTIVITY_CREATE)
async createActivity( async createActivity(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: ActivityCreateDto, @Body() dto: ActivityCreateDto,
@@ -43,6 +45,7 @@ export class ActivityController {
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.ACTIVITY_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
+12 -5
View File
@@ -10,34 +10,36 @@ import {
UpdateAlbumDto, UpdateAlbumDto,
} from 'src/dtos/album.dto'; } from 'src/dtos/album.dto';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AlbumService } from 'src/services/album.service'; import { AlbumService } from 'src/services/album.service';
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
@ApiTags('Album') @ApiTags('Album')
@Controller('album') @Controller('album')
@Authenticated()
export class AlbumController { export class AlbumController {
constructor(private service: AlbumService) {} constructor(private service: AlbumService) {}
@Get('count') @Get('count')
@Authenticated(Permission.ALBUM_READ)
getAlbumCount(@Auth() auth: AuthDto): Promise<AlbumCountResponseDto> { getAlbumCount(@Auth() auth: AuthDto): Promise<AlbumCountResponseDto> {
return this.service.getCount(auth); return this.service.getCount(auth);
} }
@Get() @Get()
@Authenticated(Permission.ALBUM_READ)
getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> { getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(auth, query); return this.service.getAll(auth, query);
} }
@Post() @Post()
@Authenticated(Permission.ALBUM_CREATE)
createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> { createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@SharedLinkRoute()
@Get(':id') @Get(':id')
@Authenticated(Permission.ALBUM_READ, { sharedLink: true })
getAlbumInfo( getAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -47,6 +49,7 @@ export class AlbumController {
} }
@Patch(':id') @Patch(':id')
@Authenticated(Permission.ALBUM_UPDATE)
updateAlbumInfo( updateAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -56,12 +59,13 @@ export class AlbumController {
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.ALBUM_DELETE)
deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@SharedLinkRoute()
@Put(':id/assets') @Put(':id/assets')
@Authenticated(Permission.ALBUM_ADD_ASSET, { sharedLink: true })
addAssetsToAlbum( addAssetsToAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -71,6 +75,7 @@ export class AlbumController {
} }
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated(Permission.ALBUM_REMOVE_ASSET)
removeAssetFromAlbum( removeAssetFromAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: BulkIdsDto, @Body() dto: BulkIdsDto,
@@ -80,6 +85,7 @@ export class AlbumController {
} }
@Put(':id/users') @Put(':id/users')
@Authenticated(Permission.ALBUM_ADD_USER)
addUsersToAlbum( addUsersToAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -89,6 +95,7 @@ export class AlbumController {
} }
@Delete(':id/user/:userId') @Delete(':id/user/:userId')
@Authenticated(Permission.ALBUM_REMOVE_USER, { bypassParamId: 'userId' })
removeUserFromAlbum( removeUserFromAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
+6 -2
View File
@@ -1,33 +1,36 @@
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { APIKeyService } from 'src/services/api-key.service'; import { APIKeyService } from 'src/services/api-key.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('API Key') @ApiTags('API Key')
@Controller('api-key') @Controller('api-key')
@Authenticated()
export class APIKeyController { export class APIKeyController {
constructor(private service: APIKeyService) {} constructor(private service: APIKeyService) {}
@Post() @Post()
@Authenticated(Permission.API_KEY_CREATE)
createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated(Permission.API_KEY_READ)
getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> { getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Get(':id') @Get(':id')
@Authenticated(Permission.API_KEY_READ)
getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> { getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(auth, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.API_KEY_UPDATE)
updateApiKey( updateApiKey(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -37,6 +40,7 @@ export class APIKeyController {
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.API_KEY_DELETE)
deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
-2
View File
@@ -1,6 +1,5 @@
import { Controller, Get, Header } from '@nestjs/common'; import { Controller, Get, Header } from '@nestjs/common';
import { ApiExcludeEndpoint } from '@nestjs/swagger'; import { ApiExcludeEndpoint } from '@nestjs/swagger';
import { PublicRoute } from 'src/middleware/auth.guard';
import { SystemConfigService } from 'src/services/system-config.service'; import { SystemConfigService } from 'src/services/system-config.service';
@Controller() @Controller()
@@ -18,7 +17,6 @@ export class AppController {
} }
@ApiExcludeEndpoint() @ApiExcludeEndpoint()
@PublicRoute()
@Get('custom.css') @Get('custom.css')
@Header('Content-Type', 'text/css') @Header('Content-Type', 'text/css')
getCustomCss() { getCustomCss() {
+11 -6
View File
@@ -31,8 +31,8 @@ import {
GetAssetThumbnailDto, GetAssetThumbnailDto,
ServeFileDto, ServeFileDto,
} from 'src/dtos/asset-v1.dto'; } from 'src/dtos/asset-v1.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor'; import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
import { AssetServiceV1 } from 'src/services/asset-v1.service'; import { AssetServiceV1 } from 'src/services/asset-v1.service';
import { sendFile } from 'src/utils/file'; import { sendFile } from 'src/utils/file';
@@ -46,12 +46,11 @@ interface UploadFiles {
@ApiTags('Asset') @ApiTags('Asset')
@Controller(Route.ASSET) @Controller(Route.ASSET)
@Authenticated()
export class AssetControllerV1 { export class AssetControllerV1 {
constructor(private service: AssetServiceV1) {} constructor(private service: AssetServiceV1) {}
@SharedLinkRoute()
@Post('upload') @Post('upload')
@Authenticated(Permission.ASSET_UPLOAD, { sharedLink: true })
@UseInterceptors(FileUploadInterceptor) @UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiBody({ @ApiBody({
@@ -85,8 +84,8 @@ export class AssetControllerV1 {
return responseDto; return responseDto;
} }
@SharedLinkRoute()
@Get('/file/:id') @Get('/file/:id')
@Authenticated(Permission.ASSET_VIEW_ORIGINAL, { sharedLink: true })
@FileResponse() @FileResponse()
async serveFile( async serveFile(
@Res() res: Response, @Res() res: Response,
@@ -98,8 +97,8 @@ export class AssetControllerV1 {
await sendFile(res, next, () => this.service.serveFile(auth, id, dto)); await sendFile(res, next, () => this.service.serveFile(auth, id, dto));
} }
@SharedLinkRoute()
@Get('/thumbnail/:id') @Get('/thumbnail/:id')
@Authenticated(Permission.ASSET_VIEW_THUMB, { sharedLink: true })
@FileResponse() @FileResponse()
async getAssetThumbnail( async getAssetThumbnail(
@Res() res: Response, @Res() res: Response,
@@ -112,16 +111,19 @@ export class AssetControllerV1 {
} }
@Get('/curated-objects') @Get('/curated-objects')
@Authenticated(Permission.ASSET_READ)
getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> { getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this.service.getCuratedObject(auth); return this.service.getCuratedObject(auth);
} }
@Get('/curated-locations') @Get('/curated-locations')
@Authenticated(Permission.ASSET_READ)
getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> { getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this.service.getCuratedLocation(auth); return this.service.getCuratedLocation(auth);
} }
@Get('/search-terms') @Get('/search-terms')
@Authenticated(Permission.ASSET_READ)
getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> { getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
return this.service.getAssetSearchTerm(auth); return this.service.getAssetSearchTerm(auth);
} }
@@ -130,6 +132,7 @@ export class AssetControllerV1 {
* Get all AssetEntity belong to the user * Get all AssetEntity belong to the user
*/ */
@Get('/') @Get('/')
@Authenticated(Permission.ASSET_READ)
@ApiHeader({ @ApiHeader({
name: 'if-none-match', name: 'if-none-match',
description: 'ETag of data already cached on the client', 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 * Checks if multiple assets exist on the server and returns all existing - used by background backup
*/ */
@Post('/exist') @Post('/exist')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
checkExistingAssets( checkExistingAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@@ -156,6 +160,7 @@ export class AssetControllerV1 {
* Checks if assets exist by checksums * Checks if assets exist by checksums
*/ */
@Post('/bulk-upload-check') @Post('/bulk-upload-check')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
checkBulkUpload( checkBulkUpload(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
+15 -5
View File
@@ -11,10 +11,10 @@ import {
RandomAssetsDto, RandomAssetsDto,
UpdateAssetDto, UpdateAssetDto,
} from 'src/dtos/asset.dto'; } 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 { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
import { UpdateStackParentDto } from 'src/dtos/stack.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 { Route } from 'src/middleware/file-upload.interceptor';
import { AssetService } from 'src/services/asset.service'; import { AssetService } from 'src/services/asset.service';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
@@ -22,11 +22,11 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Asset') @ApiTags('Asset')
@Controller('assets') @Controller('assets')
@Authenticated()
export class AssetsController { export class AssetsController {
constructor(private searchService: SearchService) {} constructor(private searchService: SearchService) {}
@Get() @Get()
@Authenticated(Permission.ASSET_READ)
@ApiOperation({ deprecated: true }) @ApiOperation({ deprecated: true })
async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<AssetResponseDto[]> { async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<AssetResponseDto[]> {
const { const {
@@ -38,21 +38,23 @@ export class AssetsController {
@ApiTags('Asset') @ApiTags('Asset')
@Controller(Route.ASSET) @Controller(Route.ASSET)
@Authenticated()
export class AssetController { export class AssetController {
constructor(private service: AssetService) {} constructor(private service: AssetService) {}
@Get('map-marker') @Get('map-marker')
@Authenticated(Permission.ASSET_READ)
getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> { getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.service.getMapMarkers(auth, options); return this.service.getMapMarkers(auth, options);
} }
@Get('memory-lane') @Get('memory-lane')
@Authenticated(Permission.MEMORY_READ)
getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> { getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(auth, dto); return this.service.getMemoryLane(auth, dto);
} }
@Get('random') @Get('random')
@Authenticated(Permission.ASSET_READ)
getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> { getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
return this.service.getRandom(auth, dto.count ?? 1); return this.service.getRandom(auth, dto.count ?? 1);
} }
@@ -61,46 +63,54 @@ export class AssetController {
* Get all asset of a device that are in the database, ID only. * Get all asset of a device that are in the database, ID only.
*/ */
@Get('/device/:deviceId') @Get('/device/:deviceId')
@Authenticated(Permission.ASSET_READ)
getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) { getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(auth, deviceId); return this.service.getUserAssetsByDeviceId(auth, deviceId);
} }
@Get('statistics') @Get('statistics')
@Authenticated(Permission.ASSET_READ)
getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> { getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(auth, dto); return this.service.getStatistics(auth, dto);
} }
@Post('jobs') @Post('jobs')
// TODO
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> { runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
return this.service.run(auth, dto); return this.service.run(auth, dto);
} }
@Put() @Put()
@Authenticated(Permission.ASSET_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> { updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(auth, dto); return this.service.updateAll(auth, dto);
} }
@Delete() @Delete()
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> { deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Put('stack/parent') @Put('stack/parent')
@Authenticated(Permission.STACK_UPDATE)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise<void> { updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise<void> {
return this.service.updateStackParent(auth, dto); return this.service.updateStackParent(auth, dto);
} }
@SharedLinkRoute()
@Get(':id') @Get(':id')
@Authenticated(Permission.ASSET_READ, { sharedLink: true })
getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> { getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.service.get(auth, id) as Promise<AssetResponseDto>; return this.service.get(auth, id) as Promise<AssetResponseDto>;
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.ASSET_UPDATE)
updateAsset( updateAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
+2 -2
View File
@@ -1,17 +1,17 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuditDeletesDto, AuditDeletesResponseDto } from 'src/dtos/audit.dto'; 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 { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AuditService } from 'src/services/audit.service'; import { AuditService } from 'src/services/audit.service';
@ApiTags('Audit') @ApiTags('Audit')
@Controller('audit') @Controller('audit')
@Authenticated()
export class AuditController { export class AuditController {
constructor(private service: AuditService) {} constructor(private service: AuditService) {}
@Get('deletes') @Get('deletes')
@Authenticated(Permission.ASSET_READ)
getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> { getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
return this.service.getDeletes(auth, dto); return this.service.getDeletes(auth, dto);
} }
+5 -4
View File
@@ -9,21 +9,20 @@ import {
LoginCredentialDto, LoginCredentialDto,
LoginResponseDto, LoginResponseDto,
LogoutResponseDto, LogoutResponseDto,
Permission,
SignUpDto, SignUpDto,
ValidateAccessTokenResponseDto, ValidateAccessTokenResponseDto,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.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 { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
@ApiTags('Authentication') @ApiTags('Authentication')
@Controller('auth') @Controller('auth')
@Authenticated()
export class AuthController { export class AuthController {
constructor(private service: AuthService) {} constructor(private service: AuthService) {}
@PublicRoute()
@Post('login') @Post('login')
async login( async login(
@Body() loginCredential: LoginCredentialDto, @Body() loginCredential: LoginCredentialDto,
@@ -41,25 +40,27 @@ export class AuthController {
}); });
} }
@PublicRoute()
@Post('admin-sign-up') @Post('admin-sign-up')
signUpAdmin(@Body() dto: SignUpDto): Promise<UserResponseDto> { signUpAdmin(@Body() dto: SignUpDto): Promise<UserResponseDto> {
return this.service.adminSignUp(dto); return this.service.adminSignUp(dto);
} }
@Post('validateToken') @Post('validateToken')
@Authenticated(Permission.AUTH_DEVICE_READ)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
validateAccessToken(): ValidateAccessTokenResponseDto { validateAccessToken(): ValidateAccessTokenResponseDto {
return { authStatus: true }; return { authStatus: true };
} }
@Post('change-password') @Post('change-password')
@Authenticated(Permission.AUTH_CHANGE_PASSWORD)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> { changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.service.changePassword(auth, dto).then(mapUser); return this.service.changePassword(auth, dto).then(mapUser);
} }
@Post('logout') @Post('logout')
@Authenticated(Permission.AUTH_DEVICE_DELETE)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async logout( async logout(
@Req() request: Request, @Req() request: Request,
@@ -2,35 +2,34 @@ import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, Streama
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { AssetIdsDto } from 'src/dtos/asset.dto'; 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 { 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 { DownloadService } from 'src/services/download.service';
import { asStreamableFile, sendFile } from 'src/utils/file'; import { asStreamableFile, sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Download') @ApiTags('Download')
@Controller('download') @Controller('download')
@Authenticated()
export class DownloadController { export class DownloadController {
constructor(private service: DownloadService) {} constructor(private service: DownloadService) {}
@SharedLinkRoute()
@Post('info') @Post('info')
@Authenticated(Permission.ASSET_READ, { sharedLink: true })
getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> { getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
return this.service.getDownloadInfo(auth, dto); return this.service.getDownloadInfo(auth, dto);
} }
@SharedLinkRoute()
@Post('archive') @Post('archive')
@Authenticated(Permission.ASSET_DOWNLOAD, { sharedLink: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@FileResponse() @FileResponse()
downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> { downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
return this.service.downloadArchive(auth, dto).then(asStreamableFile); return this.service.downloadArchive(auth, dto).then(asStreamableFile);
} }
@SharedLinkRoute()
@Post('asset/:id') @Post('asset/:id')
@Authenticated(Permission.ASSET_DOWNLOAD, { sharedLink: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@FileResponse() @FileResponse()
async downloadFile( async downloadFile(
+3 -2
View File
@@ -1,6 +1,6 @@
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; 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 { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PersonService } from 'src/services/person.service'; import { PersonService } from 'src/services/person.service';
@@ -8,16 +8,17 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Face') @ApiTags('Face')
@Controller('face') @Controller('face')
@Authenticated()
export class FaceController { export class FaceController {
constructor(private service: PersonService) {} constructor(private service: PersonService) {}
@Get() @Get()
@Authenticated(Permission.FACE_READ)
getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> { getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
return this.service.getFacesById(auth, dto); return this.service.getFacesById(auth, dto);
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.FACE_UPDATE)
reassignFacesById( reassignFacesById(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -1,29 +1,29 @@
import { Body, Controller, Get, Post } from '@nestjs/common'; import { Body, Controller, Get, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto'; 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'; import { AuditService } from 'src/services/audit.service';
@ApiTags('File Report') @ApiTags('File Report')
@Controller('report') @Controller('report')
@Authenticated()
export class ReportController { export class ReportController {
constructor(private service: AuditService) {} constructor(private service: AuditService) {}
@AdminRoute()
@Get() @Get()
@Authenticated(Permission.REPORT_READ)
getAuditFiles(): Promise<FileReportDto> { getAuditFiles(): Promise<FileReportDto> {
return this.service.getFileReport(); return this.service.getFileReport();
} }
@AdminRoute()
@Post('/checksum') @Post('/checksum')
@Authenticated(Permission.REPORT_READ)
getFileChecksums(@Body() dto: FileChecksumDto): Promise<FileChecksumResponseDto[]> { getFileChecksums(@Body() dto: FileChecksumDto): Promise<FileChecksumResponseDto[]> {
return this.service.getChecksums(dto); return this.service.getChecksums(dto);
} }
@AdminRoute()
@Post('/fix') @Post('/fix')
@Authenticated(Permission.REPORT_UPDATE)
fixAuditFiles(@Body() dto: FileReportFixDto): Promise<void> { fixAuditFiles(@Body() dto: FileReportFixDto): Promise<void> {
return this.service.fixItems(dto.items); return this.service.fixItems(dto.items);
} }
+3 -1
View File
@@ -1,21 +1,23 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common'; import { Body, Controller, Get, Param, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { JobService } from 'src/services/job.service'; import { JobService } from 'src/services/job.service';
@ApiTags('Job') @ApiTags('Job')
@Controller('jobs') @Controller('jobs')
@Authenticated({ admin: true })
export class JobController { export class JobController {
constructor(private service: JobService) {} constructor(private service: JobService) {}
@Get() @Get()
@Authenticated(Permission.JOB_READ)
getAllJobsStatus(): Promise<AllJobStatusResponseDto> { getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
return this.service.getAllJobsStatus(); return this.service.getAllJobsStatus();
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.JOB_RUN)
sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> { sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
return this.service.handleCommand(id, dto); return this.service.handleCommand(id, dto);
} }
+11 -3
View File
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { import {
CreateLibraryDto, CreateLibraryDto,
LibraryResponseDto, LibraryResponseDto,
@@ -10,38 +11,41 @@ import {
ValidateLibraryDto, ValidateLibraryDto,
ValidateLibraryResponseDto, ValidateLibraryResponseDto,
} from 'src/dtos/library.dto'; } 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 { LibraryService } from 'src/services/library.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Library') @ApiTags('Library')
@Controller('library') @Controller('library')
@Authenticated()
@AdminRoute()
export class LibraryController { export class LibraryController {
constructor(private service: LibraryService) {} constructor(private service: LibraryService) {}
@Get() @Get()
@Authenticated(Permission.LIBRARY_READ)
getAllLibraries(@Query() dto: SearchLibraryDto): Promise<LibraryResponseDto[]> { getAllLibraries(@Query() dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
return this.service.getAll(dto); return this.service.getAll(dto);
} }
@Post() @Post()
@Authenticated(Permission.LIBRARY_CREATE)
createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> { createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> {
return this.service.create(dto); return this.service.create(dto);
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.LIBRARY_UPDATE)
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> { updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto); return this.service.update(id, dto);
} }
@Get(':id') @Get(':id')
@Authenticated(Permission.LIBRARY_READ)
getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> { getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@Post(':id/validate') @Post(':id/validate')
@Authenticated(Permission.LIBRARY_READ)
@HttpCode(200) @HttpCode(200)
// TODO: change endpoint to validate current settings instead // TODO: change endpoint to validate current settings instead
validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> { validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
@@ -49,23 +53,27 @@ export class LibraryController {
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.LIBRARY_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> { deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(id); return this.service.delete(id);
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated(Permission.LIBRARY_READ)
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> { getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(id); return this.service.getStatistics(id);
} }
@Post(':id/scan') @Post(':id/scan')
@Authenticated(Permission.LIBRARY_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) { scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
return this.service.queueScan(id, dto); return this.service.queueScan(id, dto);
} }
@Post(':id/removeOffline') @Post(':id/removeOffline')
@Authenticated(Permission.LIBRARY_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
removeOfflineFiles(@Param() { id }: UUIDParamDto) { removeOfflineFiles(@Param() { id }: UUIDParamDto) {
return this.service.queueRemoveOffline(id); return this.service.queueRemoveOffline(id);
+8 -2
View File
@@ -1,7 +1,7 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { MemoryService } from 'src/services/memory.service'; import { MemoryService } from 'src/services/memory.service';
@@ -9,26 +9,29 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Memory') @ApiTags('Memory')
@Controller('memories') @Controller('memories')
@Authenticated()
export class MemoryController { export class MemoryController {
constructor(private service: MemoryService) {} constructor(private service: MemoryService) {}
@Get() @Get()
@Authenticated(Permission.MEMORY_READ)
searchMemories(@Auth() auth: AuthDto): Promise<MemoryResponseDto[]> { searchMemories(@Auth() auth: AuthDto): Promise<MemoryResponseDto[]> {
return this.service.search(auth); return this.service.search(auth);
} }
@Post() @Post()
@Authenticated(Permission.MEMORY_CREATE)
createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> { createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated(Permission.MEMORY_READ)
getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> { getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.MEMORY_UPDATE)
updateMemory( updateMemory(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -38,12 +41,14 @@ export class MemoryController {
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.MEMORY_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
@Authenticated(Permission.MEMORY_ADD_ASSET)
addMemoryAssets( addMemoryAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -53,6 +58,7 @@ export class MemoryController {
} }
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated(Permission.MEMORY_REMOVE_ASSET)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
removeMemoryAssets( removeMemoryAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
+4 -5
View File
@@ -9,19 +9,18 @@ import {
OAuthAuthorizeResponseDto, OAuthAuthorizeResponseDto,
OAuthCallbackDto, OAuthCallbackDto,
OAuthConfigDto, OAuthConfigDto,
Permission,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserResponseDto } from 'src/dtos/user.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 { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie } from 'src/utils/response'; import { respondWithCookie } from 'src/utils/response';
@ApiTags('OAuth') @ApiTags('OAuth')
@Controller('oauth') @Controller('oauth')
@Authenticated()
export class OAuthController { export class OAuthController {
constructor(private service: AuthService) {} constructor(private service: AuthService) {}
@PublicRoute()
@Get('mobile-redirect') @Get('mobile-redirect')
@Redirect() @Redirect()
redirectOAuthToMobile(@Req() request: Request) { redirectOAuthToMobile(@Req() request: Request) {
@@ -31,13 +30,11 @@ export class OAuthController {
}; };
} }
@PublicRoute()
@Post('authorize') @Post('authorize')
startOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> { startOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto); return this.service.authorize(dto);
} }
@PublicRoute()
@Post('callback') @Post('callback')
async finishOAuth( async finishOAuth(
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@@ -56,11 +53,13 @@ export class OAuthController {
} }
@Post('link') @Post('link')
@Authenticated(Permission.AUTH_OAUTH)
linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> { linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(auth, dto); return this.service.link(auth, dto);
} }
@Post('unlink') @Post('unlink')
@Authenticated(Permission.AUTH_OAUTH)
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserResponseDto> { unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.unlink(auth); return this.service.unlink(auth);
} }
+5 -2
View File
@@ -1,6 +1,6 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger'; 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 { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { PartnerDirection } from 'src/interfaces/partner.interface'; import { PartnerDirection } from 'src/interfaces/partner.interface';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
@@ -9,11 +9,11 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Partner') @ApiTags('Partner')
@Controller('partner') @Controller('partner')
@Authenticated()
export class PartnerController { export class PartnerController {
constructor(private service: PartnerService) {} constructor(private service: PartnerService) {}
@Get() @Get()
@Authenticated(Permission.PARTNER_READ)
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
// TODO: remove 'direction' and convert to full query dto // TODO: remove 'direction' and convert to full query dto
getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> { getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> {
@@ -21,11 +21,13 @@ export class PartnerController {
} }
@Post(':id') @Post(':id')
@Authenticated(Permission.PARTNER_CREATE)
createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> { createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(auth, id); return this.service.create(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.PARTNER_UPDATE)
updatePartner( updatePartner(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -35,6 +37,7 @@ export class PartnerController {
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.PARTNER_DELETE)
removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
+11 -2
View File
@@ -3,7 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { import {
AssetFaceUpdateDto, AssetFaceUpdateDto,
MergePersonDto, MergePersonDto,
@@ -22,31 +22,35 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Person') @ApiTags('Person')
@Controller('person') @Controller('person')
@Authenticated()
export class PersonController { export class PersonController {
constructor(private service: PersonService) {} constructor(private service: PersonService) {}
@Get() @Get()
@Authenticated(Permission.PERSON_READ)
getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> { getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(auth, withHidden); return this.service.getAll(auth, withHidden);
} }
@Post() @Post()
@Authenticated(Permission.PERSON_CREATE)
createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> { createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Put() @Put()
@Authenticated(Permission.PERSON_UPDATE)
updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> { updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updateAll(auth, dto); return this.service.updateAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated(Permission.PERSON_READ)
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> { getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(auth, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated(Permission.PERSON_UPDATE)
updatePerson( updatePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -56,11 +60,13 @@ export class PersonController {
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated(Permission.PERSON_READ)
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> { getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
return this.service.getStatistics(auth, id); return this.service.getStatistics(auth, id);
} }
@Get(':id/thumbnail') @Get(':id/thumbnail')
@Authenticated(Permission.PERSON_READ)
@FileResponse() @FileResponse()
async getPersonThumbnail( async getPersonThumbnail(
@Res() res: Response, @Res() res: Response,
@@ -72,11 +78,13 @@ export class PersonController {
} }
@Get(':id/assets') @Get(':id/assets')
@Authenticated(Permission.ASSET_READ)
getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> { getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(auth, id); return this.service.getAssets(auth, id);
} }
@Put(':id/reassign') @Put(':id/reassign')
@Authenticated(Permission.PERSON_UPDATE)
reassignFaces( reassignFaces(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -86,6 +94,7 @@ export class PersonController {
} }
@Post(':id/merge') @Post(':id/merge')
@Authenticated(Permission.PERSON_UPDATE)
mergePerson( mergePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
+9 -2
View File
@@ -1,7 +1,7 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto'; import { PersonResponseDto } from 'src/dtos/person.dto';
import { import {
MetadataSearchDto, MetadataSearchDto,
@@ -19,49 +19,56 @@ import { SearchService } from 'src/services/search.service';
@ApiTags('Search') @ApiTags('Search')
@Controller('search') @Controller('search')
@Authenticated()
export class SearchController { export class SearchController {
constructor(private service: SearchService) {} constructor(private service: SearchService) {}
@Get() @Get()
@Authenticated(Permission.ASSET_READ)
@ApiOperation({ deprecated: true }) @ApiOperation({ deprecated: true })
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> { search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post('metadata') @Post('metadata')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> { searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
return this.service.searchMetadata(auth, dto); return this.service.searchMetadata(auth, dto);
} }
@Post('smart') @Post('smart')
@Authenticated(Permission.ASSET_READ)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> { searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
return this.service.searchSmart(auth, dto); return this.service.searchSmart(auth, dto);
} }
@Get('explore') @Get('explore')
@Authenticated(Permission.ASSET_READ)
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> { getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
return this.service.getExploreData(auth) as Promise<SearchExploreResponseDto[]>; return this.service.getExploreData(auth) as Promise<SearchExploreResponseDto[]>;
} }
@Get('person') @Get('person')
@Authenticated(Permission.PERSON_READ)
searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> { searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.service.searchPerson(auth, dto); return this.service.searchPerson(auth, dto);
} }
@Get('places') @Get('places')
@Authenticated(Permission.ASSET_READ)
searchPlaces(@Query() dto: SearchPlacesDto): Promise<PlacesResponseDto[]> { searchPlaces(@Query() dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
return this.service.searchPlaces(dto); return this.service.searchPlaces(dto);
} }
@Get('cities') @Get('cities')
@Authenticated(Permission.ASSET_READ)
getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> { getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> {
return this.service.getAssetsByCity(auth); return this.service.getAssetsByCity(auth);
} }
@Get('suggestions') @Get('suggestions')
@Authenticated(Permission.ASSET_READ)
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> { getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise<string[]> {
return this.service.getSearchSuggestions(auth, dto); return this.service.getSearchSuggestions(auth, dto);
} }
@@ -1,5 +1,6 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { import {
ServerConfigDto, ServerConfigDto,
ServerFeaturesDto, ServerFeaturesDto,
@@ -10,57 +11,51 @@ import {
ServerThemeDto, ServerThemeDto,
ServerVersionResponseDto, ServerVersionResponseDto,
} from 'src/dtos/server-info.dto'; } 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'; import { ServerInfoService } from 'src/services/server-info.service';
@ApiTags('Server Info') @ApiTags('Server Info')
@Controller('server-info') @Controller('server-info')
@Authenticated()
export class ServerInfoController { export class ServerInfoController {
constructor(private service: ServerInfoService) {} constructor(private service: ServerInfoService) {}
@Get() @Get()
@Authenticated(Permission.SERVER_READ)
getServerInfo(): Promise<ServerInfoResponseDto> { getServerInfo(): Promise<ServerInfoResponseDto> {
return this.service.getInfo(); return this.service.getInfo();
} }
@PublicRoute()
@Get('ping') @Get('ping')
pingServer(): ServerPingResponse { pingServer(): ServerPingResponse {
return this.service.ping(); return this.service.ping();
} }
@PublicRoute()
@Get('version') @Get('version')
getServerVersion(): ServerVersionResponseDto { getServerVersion(): ServerVersionResponseDto {
return this.service.getVersion(); return this.service.getVersion();
} }
@PublicRoute()
@Get('features') @Get('features')
getServerFeatures(): Promise<ServerFeaturesDto> { getServerFeatures(): Promise<ServerFeaturesDto> {
return this.service.getFeatures(); return this.service.getFeatures();
} }
@PublicRoute()
@Get('theme') @Get('theme')
getTheme(): Promise<ServerThemeDto> { getTheme(): Promise<ServerThemeDto> {
return this.service.getTheme(); return this.service.getTheme();
} }
@PublicRoute()
@Get('config') @Get('config')
getServerConfig(): Promise<ServerConfigDto> { getServerConfig(): Promise<ServerConfigDto> {
return this.service.getConfig(); return this.service.getConfig();
} }
@AdminRoute()
@Get('statistics') @Get('statistics')
@Authenticated(Permission.SERVER_READ)
getServerStatistics(): Promise<ServerStatsResponseDto> { getServerStatistics(): Promise<ServerStatsResponseDto> {
return this.service.getStatistics(); return this.service.getStatistics();
} }
@PublicRoute()
@Get('media-types') @Get('media-types')
getSupportedMediaTypes(): ServerMediaTypesResponseDto { getSupportedMediaTypes(): ServerMediaTypesResponseDto {
return this.service.getSupportedMediaTypes(); return this.service.getSupportedMediaTypes();
+4 -2
View File
@@ -1,6 +1,6 @@
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common'; import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; 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 { SessionResponseDto } from 'src/dtos/session.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SessionService } from 'src/services/session.service'; import { SessionService } from 'src/services/session.service';
@@ -8,22 +8,24 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Sessions') @ApiTags('Sessions')
@Controller('sessions') @Controller('sessions')
@Authenticated()
export class SessionController { export class SessionController {
constructor(private service: SessionService) {} constructor(private service: SessionService) {}
@Get() @Get()
@Authenticated(Permission.SESSION_READ)
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> { getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Delete() @Delete()
@Authenticated(Permission.SESSION_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteAllSessions(@Auth() auth: AuthDto): Promise<void> { deleteAllSessions(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteAll(auth); return this.service.deleteAll(auth);
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.SESSION_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
@@ -3,14 +3,14 @@ import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto'; import { AuthDto, ImmichCookie, Permission } from 'src/dtos/auth.dto';
import { import {
SharedLinkCreateDto, SharedLinkCreateDto,
SharedLinkEditDto, SharedLinkEditDto,
SharedLinkPasswordDto, SharedLinkPasswordDto,
SharedLinkResponseDto, SharedLinkResponseDto,
} from 'src/dtos/shared-link.dto'; } 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 { LoginDetails } from 'src/services/auth.service';
import { SharedLinkService } from 'src/services/shared-link.service'; import { SharedLinkService } from 'src/services/shared-link.service';
import { respondWithCookie } from 'src/utils/response'; import { respondWithCookie } from 'src/utils/response';
@@ -18,17 +18,17 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Shared Link') @ApiTags('Shared Link')
@Controller('shared-link') @Controller('shared-link')
@Authenticated()
export class SharedLinkController { export class SharedLinkController {
constructor(private service: SharedLinkService) {} constructor(private service: SharedLinkService) {}
@Get() @Get()
@Authenticated(Permission.SHARED_LINK_READ)
getAllSharedLinks(@Auth() auth: AuthDto): Promise<SharedLinkResponseDto[]> { getAllSharedLinks(@Auth() auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@SharedLinkRoute()
@Get('me') @Get('me')
@Authenticated(Permission.SHARED_LINK_READ, { sharedLink: true })
async getMySharedLink( async getMySharedLink(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Query() dto: SharedLinkPasswordDto, @Query() dto: SharedLinkPasswordDto,
@@ -48,16 +48,19 @@ export class SharedLinkController {
} }
@Get(':id') @Get(':id')
@Authenticated(Permission.SHARED_LINK_READ)
getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> { getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Post() @Post()
@Authenticated(Permission.SHARED_LINK_CREATE)
createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Patch(':id') @Patch(':id')
@Authenticated(Permission.SHARED_LINK_UPDATE)
updateSharedLink( updateSharedLink(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -67,12 +70,13 @@ export class SharedLinkController {
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.SHARED_LINK_DELETE)
removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
@SharedLinkRoute()
@Put(':id/assets') @Put(':id/assets')
@Authenticated(Permission.SHARED_LINK_UPDATE, { sharedLink: true })
addSharedLinkAssets( addSharedLinkAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -81,8 +85,8 @@ export class SharedLinkController {
return this.service.addAssets(auth, id, dto); return this.service.addAssets(auth, id, dto);
} }
@SharedLinkRoute()
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated(Permission.SHARED_LINK_DELETE, { sharedLink: true })
removeSharedLinkAssets( removeSharedLinkAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
+3 -2
View File
@@ -1,23 +1,24 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SyncService } from 'src/services/sync.service'; import { SyncService } from 'src/services/sync.service';
@ApiTags('Sync') @ApiTags('Sync')
@Controller('sync') @Controller('sync')
@Authenticated()
export class SyncController { export class SyncController {
constructor(private service: SyncService) {} constructor(private service: SyncService) {}
@Get('full-sync') @Get('full-sync')
@Authenticated(Permission.ASSET_READ)
getAllForUserFullSync(@Auth() auth: AuthDto, @Query() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> { getAllForUserFullSync(@Auth() auth: AuthDto, @Query() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
return this.service.getAllAssetsForUserFullSync(auth, dto); return this.service.getAllAssetsForUserFullSync(auth, dto);
} }
@Get('delta-sync') @Get('delta-sync')
@Authenticated(Permission.ASSET_READ)
getDeltaSync(@Auth() auth: AuthDto, @Query() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> { getDeltaSync(@Auth() auth: AuthDto, @Query() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
return this.service.getChangesForDeltaSync(auth, dto); return this.service.getChangesForDeltaSync(auth, dto);
} }
@@ -1,38 +1,41 @@
import { Body, Controller, Get, Put, Query } from '@nestjs/common'; import { Body, Controller, Get, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.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'; import { SystemConfigService } from 'src/services/system-config.service';
@ApiTags('System Config') @ApiTags('System Config')
@Controller('system-config') @Controller('system-config')
@Authenticated({ admin: true })
export class SystemConfigController { export class SystemConfigController {
constructor(private service: SystemConfigService) {} constructor(private service: SystemConfigService) {}
@Get() @Get()
@Authenticated(Permission.SYSTEM_CONFIG_READ)
getConfig(): Promise<SystemConfigDto> { getConfig(): Promise<SystemConfigDto> {
return this.service.getConfig(); return this.service.getConfig();
} }
@Get('defaults') @Get('defaults')
@Authenticated(Permission.SYSTEM_CONFIG_READ)
getConfigDefaults(): SystemConfigDto { getConfigDefaults(): SystemConfigDto {
return this.service.getDefaults(); return this.service.getDefaults();
} }
@Put() @Put()
@Authenticated(Permission.SYSTEM_CONFIG_UPDATE)
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> { updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
return this.service.updateConfig(dto); return this.service.updateConfig(dto);
} }
@Get('storage-template-options') @Get('storage-template-options')
@Authenticated(Permission.SYSTEM_CONFIG_READ)
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
return this.service.getStorageTemplateOptions(); return this.service.getStorageTemplateOptions();
} }
@AdminRoute(false)
@SharedLinkRoute()
@Get('map/style.json') @Get('map/style.json')
@Authenticated(Permission.MAP_READ, { sharedLink: true })
getMapStyle(@Query() dto: MapThemeDto) { getMapStyle(@Query() dto: MapThemeDto) {
return this.service.getMapStyle(dto.theme); return this.service.getMapStyle(dto.theme);
} }
@@ -1,27 +1,30 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Permission } from 'src/dtos/auth.dto';
import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { SystemMetadataService } from 'src/services/system-metadata.service'; import { SystemMetadataService } from 'src/services/system-metadata.service';
@ApiTags('System Metadata') @ApiTags('System Metadata')
@Controller('system-metadata') @Controller('system-metadata')
@Authenticated({ admin: true })
export class SystemMetadataController { export class SystemMetadataController {
constructor(private service: SystemMetadataService) {} constructor(private service: SystemMetadataService) {}
@Get('admin-onboarding') @Get('admin-onboarding')
@Authenticated(Permission.SYSTEM_METADATA_READ)
getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> { getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
return this.service.getAdminOnboarding(); return this.service.getAdminOnboarding();
} }
@Post('admin-onboarding') @Post('admin-onboarding')
@Authenticated(Permission.SYSTEM_METADATA_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> { updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
return this.service.updateAdminOnboarding(dto); return this.service.updateAdminOnboarding(dto);
} }
@Get('reverse-geocoding-state') @Get('reverse-geocoding-state')
@Authenticated(Permission.SYSTEM_METADATA_READ)
getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> { getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
return this.service.getReverseGeocodingState(); return this.service.getReverseGeocodingState();
} }
+9 -2
View File
@@ -3,7 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetIdsDto } from 'src/dtos/asset.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 { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TagService } from 'src/services/tag.service'; import { TagService } from 'src/services/tag.service';
@@ -11,41 +11,47 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('Tag') @ApiTags('Tag')
@Controller('tag') @Controller('tag')
@Authenticated()
export class TagController { export class TagController {
constructor(private service: TagService) {} constructor(private service: TagService) {}
@Post() @Post()
@Authenticated(Permission.TAG_CREATE)
createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> { createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated(Permission.TAG_READ)
getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> { getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Get(':id') @Get(':id')
@Authenticated(Permission.TAG_READ)
getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> { getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.getById(auth, id); return this.service.getById(auth, id);
} }
@Patch(':id') @Patch(':id')
@Authenticated(Permission.TAG_UPDATE)
updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise<TagResponseDto> { updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise<TagResponseDto> {
return this.service.update(auth, id, dto); return this.service.update(auth, id, dto);
} }
@Delete(':id') @Delete(':id')
@Authenticated(Permission.TAG_DELETE)
deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
@Get(':id/assets') @Get(':id/assets')
@Authenticated(Permission.TAG_READ)
getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> { getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(auth, id); return this.service.getAssets(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
@Authenticated(Permission.TAG_UPDATE)
tagAssets( tagAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -55,6 +61,7 @@ export class TagController {
} }
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated(Permission.TAG_UPDATE)
untagAssets( untagAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: AssetIdsDto, @Body() dto: AssetIdsDto,
@@ -1,24 +1,23 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TimelineService } from 'src/services/timeline.service'; import { TimelineService } from 'src/services/timeline.service';
@ApiTags('Timeline') @ApiTags('Timeline')
@Controller('timeline') @Controller('timeline')
@Authenticated()
export class TimelineController { export class TimelineController {
constructor(private service: TimelineService) {} constructor(private service: TimelineService) {}
@Authenticated({ isShared: true }) @Authenticated(Permission.ASSET_READ, { sharedLink: true })
@Get('buckets') @Get('buckets')
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> { getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
return this.service.getTimeBuckets(auth, dto); return this.service.getTimeBuckets(auth, dto);
} }
@Authenticated({ isShared: true }) @Authenticated(Permission.ASSET_READ, { sharedLink: true })
@Get('bucket') @Get('bucket')
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> { getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>; return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
+4 -2
View File
@@ -1,29 +1,31 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto, Permission } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TrashService } from 'src/services/trash.service'; import { TrashService } from 'src/services/trash.service';
@ApiTags('Trash') @ApiTags('Trash')
@Controller('trash') @Controller('trash')
@Authenticated()
export class TrashController { export class TrashController {
constructor(private service: TrashService) {} constructor(private service: TrashService) {}
@Post('empty') @Post('empty')
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
emptyTrash(@Auth() auth: AuthDto): Promise<void> { emptyTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.empty(auth); return this.service.empty(auth);
} }
@Post('restore') @Post('restore')
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
restoreTrash(@Auth() auth: AuthDto): Promise<void> { restoreTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.restore(auth); return this.service.restore(auth);
} }
@Post('restore/assets') @Post('restore/assets')
@Authenticated(Permission.ASSET_DELETE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.restoreAssets(auth, dto); return this.service.restoreAssets(auth, dto);
+12 -6
View File
@@ -16,10 +16,10 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; 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 { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.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 { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
import { UserService } from 'src/services/user.service'; import { UserService } from 'src/services/user.service';
import { sendFile } from 'src/utils/file'; import { sendFile } from 'src/utils/file';
@@ -27,39 +27,42 @@ import { UUIDParamDto } from 'src/validation';
@ApiTags('User') @ApiTags('User')
@Controller(Route.USER) @Controller(Route.USER)
@Authenticated()
export class UserController { export class UserController {
constructor(private service: UserService) {} constructor(private service: UserService) {}
@Get() @Get()
@Authenticated(Permission.USER_READ)
getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> { getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
return this.service.getAll(auth, isAll); return this.service.getAll(auth, isAll);
} }
@Get('info/:id') @Get('info/:id')
@Authenticated(Permission.USER_READ)
getUserById(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> { getUserById(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@Get('me') @Get('me')
@Authenticated(Permission.USER_READ)
getMyUserInfo(@Auth() auth: AuthDto): Promise<UserResponseDto> { getMyUserInfo(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.getMe(auth); return this.service.getMe(auth);
} }
@AdminRoute()
@Post() @Post()
@Authenticated(Permission.USER_CREATE)
createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> { createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
return this.service.create(createUserDto); return this.service.create(createUserDto);
} }
@Delete('profile-image') @Delete('profile-image')
@Authenticated(Permission.USER_UPDATE)
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteProfileImage(@Auth() auth: AuthDto): Promise<void> { deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteProfileImage(auth); return this.service.deleteProfileImage(auth);
} }
@AdminRoute()
@Delete(':id') @Delete(':id')
@Authenticated(Permission.USER_DELETE)
deleteUser( deleteUser(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@@ -68,14 +71,15 @@ export class UserController {
return this.service.delete(auth, id, dto); return this.service.delete(auth, id, dto);
} }
@AdminRoute()
@Post(':id/restore') @Post(':id/restore')
@Authenticated(Permission.USER_DELETE)
restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> { restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.restore(auth, id); return this.service.restore(auth, id);
} }
// TODO: replace with @Put(':id') // TODO: replace with @Put(':id')
@Put() @Put()
@Authenticated(Permission.USER_UPDATE)
updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateUserDto): Promise<UserResponseDto> { updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
return this.service.update(auth, updateUserDto); return this.service.update(auth, updateUserDto);
} }
@@ -84,6 +88,7 @@ export class UserController {
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto })
@Post('profile-image') @Post('profile-image')
@Authenticated(Permission.USER_UPDATE)
createProfileImage( createProfileImage(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@UploadedFile() fileInfo: Express.Multer.File, @UploadedFile() fileInfo: Express.Multer.File,
@@ -92,6 +97,7 @@ export class UserController {
} }
@Get('profile-image/:id') @Get('profile-image/:id')
@Authenticated(Permission.USER_READ)
@FileResponse() @FileResponse()
async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) { async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) {
await sendFile(res, next, () => this.service.getProfileImage(id)); await sendFile(res, next, () => this.service.getProfileImage(id));
+41 -41
View File
@@ -4,7 +4,7 @@ import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
export enum Permission { export enum AccessPermission {
ACTIVITY_CREATE = 'activity.create', ACTIVITY_CREATE = 'activity.create',
ACTIVITY_DELETE = 'activity.delete', ACTIVITY_DELETE = 'activity.delete',
@@ -74,7 +74,7 @@ export class AccessCore {
* Check if user has access to all ids, for the given permission. * 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. * 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]; ids = Array.isArray(ids) ? ids : [ids];
const allowedIds = await this.checkAccess(auth, permission, ids); const allowedIds = await this.checkAccess(auth, permission, ids);
if (!setIsEqual(new Set(ids), allowedIds)) { if (!setIsEqual(new Set(ids), allowedIds)) {
@@ -88,7 +88,7 @@ export class AccessCore {
* *
* @returns Set<string> * @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; const idSet = Array.isArray(ids) ? new Set(ids) : ids;
if (idSet.size === 0) { if (idSet.size === 0) {
return new Set(); return new Set();
@@ -103,40 +103,40 @@ export class AccessCore {
private async checkAccessSharedLink( private async checkAccessSharedLink(
sharedLink: SharedLinkEntity, sharedLink: SharedLinkEntity,
permission: Permission, permission: AccessPermission,
ids: Set<string>, ids: Set<string>,
): Promise<Set<string>> { ): Promise<Set<string>> {
const sharedLinkId = sharedLink.id; const sharedLinkId = sharedLink.id;
switch (permission) { switch (permission) {
case Permission.ASSET_READ: { case AccessPermission.ASSET_READ: {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
} }
case Permission.ASSET_VIEW: { case AccessPermission.ASSET_VIEW: {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
} }
case Permission.ASSET_DOWNLOAD: { case AccessPermission.ASSET_DOWNLOAD: {
return sharedLink.allowDownload return sharedLink.allowDownload
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
: new Set(); : new Set();
} }
case Permission.ASSET_UPLOAD: { case AccessPermission.ASSET_UPLOAD: {
return sharedLink.allowUpload ? ids : new Set(); 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 // TODO: fix this to not use sharedLink.userId for access control
return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids); 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); return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
} }
case Permission.ALBUM_DOWNLOAD: { case AccessPermission.ALBUM_DOWNLOAD: {
return sharedLink.allowDownload return sharedLink.allowDownload
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
: new Set(); : 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) { switch (permission) {
// uses album id // uses album id
case Permission.ACTIVITY_CREATE: { case AccessPermission.ACTIVITY_CREATE: {
return await this.repository.activity.checkCreateAccess(auth.user.id, ids); return await this.repository.activity.checkCreateAccess(auth.user.id, ids);
} }
// uses activity id // uses activity id
case Permission.ACTIVITY_DELETE: { case AccessPermission.ACTIVITY_DELETE: {
const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids); const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids);
const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess( const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess(
auth.user.id, auth.user.id,
@@ -165,7 +165,7 @@ export class AccessCore {
return setUnion(isOwner, isAlbumOwner); return setUnion(isOwner, isAlbumOwner);
} }
case Permission.ASSET_READ: { case AccessPermission.ASSET_READ: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); 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 isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess( const isPartner = await this.repository.asset.checkPartnerAccess(
@@ -175,13 +175,13 @@ export class AccessCore {
return setUnion(isOwner, isAlbum, isPartner); 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 isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner); return setUnion(isOwner, isPartner);
} }
case Permission.ASSET_VIEW: { case AccessPermission.ASSET_VIEW: {
const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); 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 isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess( const isPartner = await this.repository.asset.checkPartnerAccess(
@@ -191,7 +191,7 @@ export class AccessCore {
return setUnion(isOwner, isAlbum, isPartner); 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 isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess( const isPartner = await this.repository.asset.checkPartnerAccess(
@@ -201,101 +201,101 @@ export class AccessCore {
return setUnion(isOwner, isAlbum, isPartner); return setUnion(isOwner, isAlbum, isPartner);
} }
case Permission.ASSET_UPDATE: { case AccessPermission.ASSET_UPDATE: {
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); 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); 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); 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 isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared); return setUnion(isOwner, isShared);
} }
case Permission.ALBUM_UPDATE: { case AccessPermission.ALBUM_UPDATE: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids); 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); 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); 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 isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared); return setUnion(isOwner, isShared);
} }
case Permission.ALBUM_REMOVE_ASSET: { case AccessPermission.ALBUM_REMOVE_ASSET: {
return await this.repository.album.checkOwnerAccess(auth.user.id, ids); 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); 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(); 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); 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 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)); const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner); 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(); 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); 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); 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); 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); 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); 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); 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); 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); 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); return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
} }
+9
View File
@@ -48,6 +48,10 @@ export class UserCore {
throw new BadRequestException('The server already has an admin'); throw new BadRequestException('The server already has an admin');
} }
if (dto.permissions) {
// TODO validate granted permissions
}
if (dto.email) { if (dto.email) {
const duplicate = await this.userRepository.getByEmail(dto.email); const duplicate = await this.userRepository.getByEmail(dto.email);
if (duplicate && duplicate.id !== id) { if (duplicate && duplicate.id !== id) {
@@ -93,6 +97,11 @@ export class UserCore {
if (payload.storageLabel) { if (payload.storageLabel) {
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
} }
if (payload.permissions) {
// TODO validate permissions
}
const userEntity = await this.userRepository.create(payload); const userEntity = await this.userRepository.create(payload);
await this.libraryRepository.create({ await this.libraryRepository.create({
owner: { id: userEntity.id } as UserEntity, owner: { id: userEntity.id } as UserEntity,
+189
View File
@@ -25,6 +25,195 @@ export type CookieResponse = {
values: Array<{ key: ImmichCookie; value: string }>; 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 { export class AuthDto {
user!: UserEntity; user!: UserEntity;
+3
View File
@@ -1,5 +1,6 @@
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import { PermissionPreset } from 'src/dtos/auth.dto';
import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto'; import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
describe('update user DTO', () => { describe('update user DTO', () => {
@@ -22,6 +23,7 @@ describe('create user DTO', () => {
email: undefined, email: undefined,
password: 'password', password: 'password',
name: 'name', name: 'name',
permissionPreset: PermissionPreset.USER,
}; };
let dto: CreateUserDto = plainToInstance(CreateUserDto, params); let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
let errors = await validate(dto); let errors = await validate(dto);
@@ -45,6 +47,7 @@ describe('create user DTO', () => {
email: someEmail, email: someEmail,
password: 'some password', password: 'some password',
name: 'some name', name: 'some name',
permissionPreset: 'user',
}); });
const errors = await validate(dto); const errors = await validate(dto);
expect(errors).toHaveLength(0); expect(errors).toHaveLength(0);
+27 -2
View File
@@ -1,10 +1,14 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; 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 { getRandomAvatarColor } from 'src/dtos/user-profile.dto';
import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity'; import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
const isCustomPreset = ({ permissionPreset }: CreateUserDto) =>
permissionPreset && permissionPreset === PermissionPreset.CUSTOM;
export class CreateUserDto { export class CreateUserDto {
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@Transform(toEmail) @Transform(toEmail)
@@ -34,6 +38,15 @@ export class CreateUserDto {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
shouldChangePassword?: boolean; 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 { export class CreateAdminDto {
@@ -112,6 +125,16 @@ export class UpdateUserDto {
@IsPositive() @IsPositive()
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null; 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 { export class UserDto {
@@ -139,6 +162,7 @@ export class UserResponseDto extends UserDto {
quotaUsageInBytes!: number | null; quotaUsageInBytes!: number | null;
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) @ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
status!: string; status!: string;
permissions?: Permission[];
} }
export const mapSimpleUser = (entity: UserEntity): UserDto => { export const mapSimpleUser = (entity: UserEntity): UserDto => {
@@ -165,5 +189,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
quotaSizeInBytes: entity.quotaSizeInBytes, quotaSizeInBytes: entity.quotaSizeInBytes,
quotaUsageInBytes: entity.quotaUsageInBytes, quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status, status: entity.status,
permissions: entity.permissions,
}; };
} }
+4
View File
@@ -1,3 +1,4 @@
import { Permission } from 'src/dtos/auth.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { TagEntity } from 'src/entities/tag.entity'; import { TagEntity } from 'src/entities/tag.entity';
import { import {
@@ -87,4 +88,7 @@ export class UserEntity {
@Column({ type: 'bigint', default: 0 }) @Column({ type: 'bigint', default: 0 })
quotaUsageInBytes!: number; quotaUsageInBytes!: number;
@Column({ type: 'varchar', array: true })
permissions!: Permission[];
} }
+6 -4
View File
@@ -2,10 +2,12 @@ import { SessionEntity } from 'src/entities/session.entity';
export const ISessionRepository = 'ISessionRepository'; export const ISessionRepository = 'ISessionRepository';
type E = SessionEntity;
export interface ISessionRepository { export interface ISessionRepository {
create(dto: Partial<SessionEntity>): Promise<SessionEntity>; create<T extends Partial<E>>(dto: T): Promise<T>;
update(dto: Partial<SessionEntity>): Promise<SessionEntity>; update<T extends Partial<E>>(dto: T): Promise<T>;
delete(id: string): Promise<void>; delete(id: string): Promise<void>;
getByToken(token: string): Promise<SessionEntity | null>; getByToken(token: string): Promise<E | null>;
getByUserId(userId: string): Promise<SessionEntity[]>; getByUserId(userId: string): Promise<E[]>;
} }
+28 -34
View File
@@ -10,49 +10,40 @@ import {
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
import { Request } from 'express'; 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 { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthService, LoginDetails } from 'src/services/auth.service'; import { AuthService, LoginDetails } from 'src/services/auth.service';
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
export enum Metadata { export enum Metadata {
AUTH_ROUTE = 'auth_route',
ADMIN_ROUTE = 'admin_route',
SHARED_ROUTE = 'shared_route', SHARED_ROUTE = 'shared_route',
PUBLIC_SECURITY = 'public_security', PUBLIC_SECURITY = 'public_security',
API_KEY_SECURITY = 'api_key', API_KEY_SECURITY = 'api_key',
PERMISSION = 'auth_permission',
} }
export interface AuthenticatedOptions { type AuthenticatedOptions = {
admin?: true; sharedLink?: true;
isShared?: true; /** skip permission check when param id matches calling user */
} bypassParamId?: string;
};
export const Authenticated = (options: AuthenticatedOptions = {}) => { export const Authenticated = (permission: Permission, options?: AuthenticatedOptions) => {
const decorators: MethodDecorator[] = [ const { sharedLink } = { sharedLink: false, ...options };
const decorators = sharedLink
? [SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false })]
: [];
return applyDecorators(
ApiBearerAuth(), ApiBearerAuth(),
ApiCookieAuth(), ApiCookieAuth(),
ApiSecurity(Metadata.API_KEY_SECURITY), ApiSecurity(Metadata.API_KEY_SECURITY),
SetMetadata(Metadata.AUTH_ROUTE, true), SetMetadata(Metadata.PERMISSION, permission),
]; ...decorators,
);
if (options.admin) {
decorators.push(AdminRoute());
}
if (options.isShared) {
decorators.push(SharedLinkRoute());
}
return applyDecorators(...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 => { export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => {
return context.switchToHttp().getRequest<{ user: AuthDto }>().user; return context.switchToHttp().getRequest<{ user: AuthDto }>().user;
}); });
@@ -89,26 +80,29 @@ export class AuthGuard implements CanActivate {
} }
async canActivate(context: ExecutionContext): Promise<boolean> { 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 permission = this.reflector.get<Permission>(Metadata.PERMISSION, method);
const isAdminRoute = this.reflector.getAllAndOverride(Metadata.ADMIN_ROUTE, targets); const isSharedRoute = this.reflector.get<boolean>(Metadata.SHARED_ROUTE, method);
const isSharedRoute = this.reflector.getAllAndOverride(Metadata.SHARED_ROUTE, targets);
if (!isAuthRoute) { // public
if (!permission) {
return true; return true;
} }
const request = context.switchToHttp().getRequest<AuthRequest>(); const request = context.switchToHttp().getRequest<AuthRequest>();
const authDto = await this.authService.validate(request.headers, request.query as Record<string, string>); 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) { if (authDto.sharedLink && !isSharedRoute) {
this.logger.warn(`Denied access to non-shared route: ${request.path}`); this.logger.warn(`Denied access to non-shared route: ${request.path}`);
return false; return false;
} }
if (isAdminRoute && !authDto.user.isAdmin) { if ((isApiKey || isUserToken) && !authDto.user.permissions.includes(permission)) {
this.logger.warn(`Denied access to admin only route: ${request.path}`); this.logger.warn(`Denied access to route: no ${permission} permission: ${request.path}. `);
return false; return false;
} }
@@ -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"`);
}
}
@@ -31,12 +31,12 @@ export class SessionRepository implements ISessionRepository {
}); });
} }
create(session: Partial<SessionEntity>): Promise<SessionEntity> { create<T extends Partial<SessionEntity>>(dto: T): Promise<T & { id: string }> {
return this.repository.save(session); return this.repository.save(dto);
} }
update(session: Partial<SessionEntity>): Promise<SessionEntity> { update<T extends Partial<SessionEntity>>(dto: T): Promise<T> {
return this.repository.save(session); return this.repository.save(dto);
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
+5 -5
View File
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core'; import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { import {
ActivityCreateDto, ActivityCreateDto,
ActivityDto, ActivityDto,
@@ -28,7 +28,7 @@ export class ActivityService {
} }
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { 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({ const activities = await this.repository.search({
userId: dto.userId, userId: dto.userId,
albumId: dto.albumId, albumId: dto.albumId,
@@ -40,12 +40,12 @@ export class ActivityService {
} }
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { 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) }; return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
} }
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> { 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 = { const common = {
userId: auth.user.id, userId: auth.user.id,
@@ -79,7 +79,7 @@ export class ActivityService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { 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); await this.repository.delete(id);
} }
} }
+10 -10
View File
@@ -1,5 +1,5 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core'; import { AccessCore, AccessPermission } from 'src/cores/access.core';
import { import {
AddUsersDto, AddUsersDto,
AlbumCountResponseDto, AlbumCountResponseDto,
@@ -97,7 +97,7 @@ export class AlbumService {
} }
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> { 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(); await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets }); 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 assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity);
const album = await this.albumRepository.create({ const album = await this.albumRepository.create({
@@ -135,7 +135,7 @@ export class AlbumService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> { 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 }); const album = await this.findOrFail(id, { withAssets: true });
@@ -158,7 +158,7 @@ export class AlbumService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { 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 }); 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[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false }); 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( const results = await addAssets(
auth, auth,
@@ -190,12 +190,12 @@ export class AlbumService {
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false }); 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( const results = await removeAssets(
auth, auth,
{ accessRepository: this.accessRepository, repository: this.albumRepository }, { 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); 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> { 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 }); const album = await this.findOrFail(id, { withAssets: false });
@@ -259,7 +259,7 @@ export class AlbumService {
// non-admin can remove themselves // non-admin can remove themselves
if (auth.user.id !== userId) { 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({ await this.albumRepository.update({
+5 -5
View File
@@ -5,7 +5,7 @@ import {
InternalServerErrorException, InternalServerErrorException,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } 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 { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { import {
AssetBulkUploadCheckResponseDto, AssetBulkUploadCheckResponseDto,
@@ -78,7 +78,7 @@ export class AssetServiceV1 {
try { try {
const libraryId = await this.getLibraryId(auth, dto.libraryId); 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); this.requireQuota(auth, file.size);
if (livePhotoFile) { if (livePhotoFile) {
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId }; 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[]> { public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id; 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); const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
return assets.map((asset) => mapAsset(asset, { withStack: true, auth })); return assets.map((asset) => mapAsset(asset, { withStack: true, auth }));
} }
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> { 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); const asset = await this.assetRepositoryV1.get(assetId);
if (!asset) { if (!asset) {
@@ -135,7 +135,7 @@ export class AssetServiceV1 {
public async serveFile(auth: AuthDto, assetId: string, dto: ServeFileDto): Promise<ImmichFileResponse> { public async serveFile(auth: AuthDto, assetId: string, dto: ServeFileDto): Promise<ImmichFileResponse> {
// this is not quite right as sometimes this returns the original still // 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); const asset = await this.assetRepository.getById(assetId);
if (!asset) { if (!asset) {
+9 -9
View File
@@ -3,7 +3,7 @@ import _ from 'lodash';
import { DateTime, Duration } from 'luxon'; import { DateTime, Duration } from 'luxon';
import { extname } from 'node:path'; import { extname } from 'node:path';
import sanitize from 'sanitize-filename'; 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 { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { import {
@@ -210,7 +210,7 @@ export class AssetService {
} }
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> { 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, { const asset = await this.assetRepository.getById(id, {
exifInfo: true, exifInfo: true,
@@ -250,7 +250,7 @@ export class AssetService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> { 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; const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
@@ -273,7 +273,7 @@ export class AssetService {
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto; 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. // TODO: refactor this logic into separate API calls POST /stack, PUT /stack, etc.
const stackIdsToCheckForDelete: string[] = []; const stackIdsToCheckForDelete: string[] = [];
@@ -289,7 +289,7 @@ export class AssetService {
); );
} else if (options.stackParentId) { } else if (options.stackParentId) {
//Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack //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 } }); const primaryAsset = await this.assetRepository.getById(options.stackParentId, { stack: { assets: true } });
if (!primaryAsset) { if (!primaryAsset) {
throw new BadRequestException('Asset not found for given stackParentId'); throw new BadRequestException('Asset not found for given stackParentId');
@@ -418,7 +418,7 @@ export class AssetService {
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto; const { ids, force } = dto;
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids); await this.access.requirePermission(auth, AccessPermission.ASSET_DELETE, ids);
if (force) { if (force) {
await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.ASSET_DELETION, data: { id } }))); 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> { async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise<void> {
const { oldParentId, newParentId } = dto; const { oldParentId, newParentId } = dto;
await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId); await this.access.requirePermission(auth, AccessPermission.ASSET_READ, oldParentId);
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId); await this.access.requirePermission(auth, AccessPermission.ASSET_UPDATE, newParentId);
const childIds: string[] = []; const childIds: string[] = [];
const oldParent = await this.assetRepository.getById(oldParentId, { const oldParent = await this.assetRepository.getById(oldParentId, {
@@ -464,7 +464,7 @@ export class AssetService {
} }
async run(auth: AuthDto, dto: AssetJobsDto) { 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[] = []; const jobs: JobItem[] = [];
+2 -2
View File
@@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; 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 { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { import {
AuditDeletesDto, AuditDeletesDto,
@@ -51,7 +51,7 @@ export class AuditService {
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> { async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
const userId = dto.userId || auth.user.id; 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, { const audits = await this.repository.getAfter(dto.after, {
userIds: [userId], userIds: [userId],
+3
View File
@@ -55,6 +55,7 @@ const oauthUserWithDefaultQuota = {
oauthId: sub, oauthId: sub,
quotaSizeInBytes: 1_073_741_824, quotaSizeInBytes: 1_073_741_824,
storageLabel: null, storageLabel: null,
permissions: expect.any(Array),
}; };
describe('AuthService', () => { describe('AuthService', () => {
@@ -492,6 +493,7 @@ describe('AuthService', () => {
oauthId: sub, oauthId: sub,
quotaSizeInBytes: null, quotaSizeInBytes: null,
storageLabel: null, storageLabel: null,
permissions: expect.any(Array),
}); });
}); });
@@ -512,6 +514,7 @@ describe('AuthService', () => {
oauthId: sub, oauthId: sub,
quotaSizeInBytes: 5_368_709_120, quotaSizeInBytes: 5_368_709_120,
storageLabel: null, storageLabel: null,
permissions: expect.any(Array),
}); });
}); });
}); });
+4
View File
@@ -15,6 +15,7 @@ import { AccessCore } from 'src/cores/access.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core'; import { UserCore } from 'src/cores/user.core';
import { import {
ALL_PERMISSIONS,
AuthDto, AuthDto,
ChangePasswordDto, ChangePasswordDto,
ImmichCookie, ImmichCookie,
@@ -25,6 +26,7 @@ import {
OAuthCallbackDto, OAuthCallbackDto,
OAuthConfigDto, OAuthConfigDto,
SignUpDto, SignUpDto,
USER_PERMISSIONS,
mapLoginResponse, mapLoginResponse,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
@@ -143,6 +145,7 @@ export class AuthService {
name: dto.name, name: dto.name,
password: dto.password, password: dto.password,
storageLabel: 'admin', storageLabel: 'admin',
permissions: ALL_PERMISSIONS,
}); });
return mapUser(admin); return mapUser(admin);
@@ -238,6 +241,7 @@ export class AuthService {
oauthId: profile.sub, oauthId: profile.sub,
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null, quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
storageLabel: storageLabel || null, storageLabel: storageLabel || null,
permissions: USER_PERMISSIONS,
}); });
} }
+6 -6
View File
@@ -1,6 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { parse } from 'node:path'; 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 { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.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> { 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]); const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) { if (!asset) {
@@ -81,7 +81,7 @@ export class DownloadService {
} }
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> { 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 zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds); const assets = await this.assetRepository.getByIds(dto.assetIds);
@@ -117,20 +117,20 @@ export class DownloadService {
if (dto.assetIds) { if (dto.assetIds) {
const assetIds = 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 }); const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
} }
if (dto.albumId) { if (dto.albumId) {
const albumId = 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)); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
} }
if (dto.userId) { if (dto.userId) {
const userId = 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) => return usePagination(PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
); );
+8 -8
View File
@@ -1,5 +1,5 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; 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 { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.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> { 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); const memory = await this.findOrFail(id);
return mapMemory(memory); return mapMemory(memory);
} }
@@ -34,7 +34,7 @@ export class MemoryService {
// TODO validate type/data combination // TODO validate type/data combination
const assetIds = dto.assetIds || []; 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({ const memory = await this.repository.create({
ownerId: auth.user.id, ownerId: auth.user.id,
type: dto.type, type: dto.type,
@@ -49,7 +49,7 @@ export class MemoryService {
} }
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> { 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({ const memory = await this.repository.update({
id, id,
@@ -62,12 +62,12 @@ export class MemoryService {
} }
async remove(auth: AuthDto, id: string): Promise<void> { 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); await this.repository.delete(id);
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { 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 repos = { accessRepository: this.accessRepository, repository: this.repository };
const results = await addAssets(auth, repos, { id, assetIds: dto.ids }); 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[]> { 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 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 results = await removeAssets(auth, repos, { id, assetIds: dto.ids, permissions });
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
+2 -2
View File
@@ -1,5 +1,5 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; 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 { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.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> { 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 partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
+14 -14
View File
@@ -1,6 +1,6 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { FACE_THUMBNAIL_SIZE } from 'src/constants'; 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 { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; 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[]> { 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 person = await this.findOrFail(personId);
const result: PersonResponseDto[] = []; const result: PersonResponseDto[] = [];
const changeFeaturePhoto: string[] = []; const changeFeaturePhoto: string[] = [];
@@ -109,7 +109,7 @@ export class PersonService {
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
for (const face of faces) { 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) { if (person.faceAssetId === null) {
changeFeaturePhoto.push(person.id); changeFeaturePhoto.push(person.id);
} }
@@ -130,9 +130,9 @@ export class PersonService {
} }
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> { 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 face = await this.repository.getFaceById(dto.id);
const person = await this.findOrFail(personId); const person = await this.findOrFail(personId);
@@ -148,7 +148,7 @@ export class PersonService {
} }
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> { 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); const faces = await this.repository.getFaces(dto.id);
return faces.map((asset) => mapFaces(asset, auth)); return faces.map((asset) => mapFaces(asset, auth));
} }
@@ -175,17 +175,17 @@ export class PersonService {
} }
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> { 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); return this.findOrFail(id).then(mapPerson);
} }
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> { 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); return this.repository.getStatistics(id);
} }
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> { 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); const person = await this.repository.getById(id);
if (!person || !person.thumbnailPath) { if (!person || !person.thumbnailPath) {
throw new NotFoundException(); throw new NotFoundException();
@@ -199,7 +199,7 @@ export class PersonService {
} }
async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> { 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); const assets = await this.repository.getAssets(id);
return assets.map((asset) => mapAsset(asset)); return assets.map((asset) => mapAsset(asset));
} }
@@ -214,13 +214,13 @@ export class PersonService {
} }
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { 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; const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
// TODO: set by faceId directly // TODO: set by faceId directly
let faceId: string | undefined = undefined; let faceId: string | undefined = undefined;
if (assetId) { 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 }]); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
if (!face) { if (!face) {
throw new BadRequestException('Invalid assetId for feature 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[]> { async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
const mergeIds = dto.ids; 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); let primaryPerson = await this.findOrFail(id);
const primaryName = primaryPerson.name || primaryPerson.id; const primaryName = primaryPerson.name || primaryPerson.id;
const results: BulkIdResponseDto[] = []; 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) { for (const mergeId of mergeIds) {
const hasAccess = allowedIds.has(mergeId); const hasAccess = allowedIds.has(mergeId);
+2 -2
View File
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; 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 { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
@@ -25,7 +25,7 @@ export class SessionService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { 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); await this.sessionRepository.delete(id);
} }
+4 -4
View File
@@ -1,5 +1,5 @@
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; 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 { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@@ -59,7 +59,7 @@ export class SharedLinkService {
if (!dto.albumId) { if (!dto.albumId) {
throw new BadRequestException('Invalid 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; break;
} }
@@ -68,7 +68,7 @@ export class SharedLinkService {
throw new BadRequestException('Invalid assetIds'); 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; break;
} }
@@ -129,7 +129,7 @@ export class SharedLinkService {
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); 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[] = []; const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) { for (const assetId of dto.assetIds) {
+3 -3
View File
@@ -2,7 +2,7 @@ import { Inject } from '@nestjs/common';
import _ from 'lodash'; import _ from 'lodash';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; 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 { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.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[]> { async getAllAssetsForUserFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id; 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({ const assets = await this.assetRepository.getAllForUserFullSync({
ownerId: userId, ownerId: userId,
lastCreationDate: dto.lastCreationDate, lastCreationDate: dto.lastCreationDate,
@@ -39,7 +39,7 @@ export class SyncService {
} }
async getChangesForDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> { 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 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)]; const userIds = [auth.user.id, ...partner.filter((p) => p.sharedWithId == auth.user.id).map((p) => p.sharedById)];
userIds.sort(); userIds.sort();
+4 -4
View File
@@ -1,5 +1,5 @@
import { BadRequestException, Inject } from '@nestjs/common'; 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 { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.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) { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
if (dto.albumId) { if (dto.albumId) {
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); await this.accessCore.requirePermission(auth, AccessPermission.ALBUM_READ, [dto.albumId]);
} else { } else {
dto.userId = dto.userId || auth.user.id; dto.userId = dto.userId || auth.user.id;
} }
if (dto.userId) { 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) { if (dto.isArchived !== false) {
await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); await this.accessCore.requirePermission(auth, AccessPermission.ARCHIVE_READ, [dto.userId]);
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon'; 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 { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
@@ -23,7 +23,7 @@ export class TrashService {
async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> { async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
const { ids } = dto; 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); await this.restoreAndSend(auth, ids);
} }
+6 -4
View File
@@ -3,7 +3,7 @@ import { DateTime } from 'luxon';
import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.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 { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { UserEntity, UserStatus } from 'src/entities/user.entity'; import { UserEntity, UserStatus } from 'src/entities/user.entity';
@@ -60,8 +60,9 @@ export class UserService {
return this.findOrFail(auth.user.id, {}).then(mapUser); return this.findOrFail(auth.user.id, {}).then(mapUser);
} }
create(createUserDto: CreateUserDto): Promise<UserResponseDto> { create(dto: CreateUserDto): Promise<UserResponseDto> {
return this.userCore.createUser(createUserDto).then(mapUser); const permissions = presetToPermissions(dto);
return this.userCore.createUser({ ...dto, permissions }).then(mapUser);
} }
async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> { async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> {
@@ -71,7 +72,8 @@ export class UserService {
await this.userRepository.syncUsage(dto.id); 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> { async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {
+3 -3
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 { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { IAccessRepository } from 'src/interfaces/access.interface'; 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 existingAssetIds = await repository.getAssetIds(dto.id, dto.assetIds);
const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id)); 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[] = []; const results: BulkIdResponseDto[] = [];
for (const assetId of dto.assetIds) { for (const assetId of dto.assetIds) {
@@ -50,7 +50,7 @@ export const addAssets = async (
export const removeAssets = async ( export const removeAssets = async (
auth: AuthDto, auth: AuthDto,
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },
dto: { id: string; assetIds: string[]; permissions: Permission[] }, dto: { id: string; assetIds: string[]; permissions: AccessPermission[] },
) => { ) => {
const { accessRepository, repository } = repositories; const { accessRepository, repository } = repositories;
const access = AccessCore.create(accessRepository); const access = AccessCore.create(accessRepository);
-4
View File
@@ -103,10 +103,6 @@ const patchOpenAPI = (document: OpenAPIObject) => {
continue; continue;
} }
if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) {
delete operation.security;
}
if (operation.summary === '') { if (operation.summary === '') {
delete operation.summary; delete operation.summary;
} }
@@ -3,8 +3,8 @@ import { Mocked, vitest } from 'vitest';
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => { export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {
return { return {
create: vitest.fn(), create: vitest.fn() as any,
update: vitest.fn(), update: vitest.fn() as any,
delete: vitest.fn(), delete: vitest.fn(),
getByToken: vitest.fn(), getByToken: vitest.fn(),
getByUserId: vitest.fn(), getByUserId: vitest.fn(),
+18
View File
@@ -12,6 +12,8 @@
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/core": "^5.7.1",
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6", "@zoom-image/svelte": "^0.2.6",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2", "copy-image-clipboard": "^2.1.2",
@@ -1590,6 +1592,22 @@
"three": "^0.161.0" "three": "^0.161.0"
} }
}, },
"node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz",
"integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==",
"peerDependencies": {
"@photo-sphere-viewer/core": "5.7.2"
}
},
"node_modules/@photo-sphere-viewer/video-plugin": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz",
"integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==",
"peerDependencies": {
"@photo-sphere-viewer/core": "5.7.2"
}
},
"node_modules/@polka/url": { "node_modules/@polka/url": {
"version": "1.0.0-next.24", "version": "1.0.0-next.24",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",
+2
View File
@@ -61,6 +61,8 @@
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/core": "^5.7.1",
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6", "@zoom-image/svelte": "^0.2.6",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2", "copy-image-clipboard": "^2.1.2",
@@ -50,7 +50,7 @@
import PanoramaViewer from './panorama-viewer.svelte'; import PanoramaViewer from './panorama-viewer.svelte';
import PhotoViewer from './photo-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte';
import SlideshowBar from './slideshow-bar.svelte'; import SlideshowBar from './slideshow-bar.svelte';
import VideoViewer from './video-viewer.svelte'; import VideoViewer from './video-wrapper-viewer.svelte';
export let assetStore: AssetStore | null = null; export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
@@ -622,6 +622,7 @@
{:else} {:else}
<VideoViewer <VideoViewer
assetId={previewStackedAsset.id} assetId={previewStackedAsset.id}
projectionType={previewStackedAsset.exifInfo?.projectionType}
on:close={closeViewer} on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()} on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted} on:onVideoStarted={handleVideoStarted}
@@ -642,6 +643,7 @@
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId} {#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
<VideoViewer <VideoViewer
assetId={asset.livePhotoVideoId} assetId={asset.livePhotoVideoId}
projectionType={asset.exifInfo?.projectionType}
on:close={closeViewer} on:close={closeViewer}
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
/> />
@@ -655,6 +657,7 @@
{:else} {:else}
<VideoViewer <VideoViewer
assetId={asset.id} assetId={asset.id}
projectionType={asset.exifInfo?.projectionType}
on:close={closeViewer} on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()} on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted} on:onVideoStarted={handleVideoStarted}
@@ -1,22 +1,39 @@
<script lang="ts"> <script lang="ts">
import { serveFile, type AssetResponseDto } from '@immich/sdk'; import { serveFile, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { getKey } from '$lib/utils'; import { getKey } from '$lib/utils';
export let asset: AssetResponseDto; import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
export let asset: Pick<AssetResponseDto, 'id' | 'type'>;
const photoSphereConfigs =
asset.type === AssetTypeEnum.Video
? ([
import('@photo-sphere-viewer/equirectangular-video-adapter').then(
({ EquirectangularVideoAdapter }) => EquirectangularVideoAdapter,
),
import('@photo-sphere-viewer/video-plugin').then(({ VideoPlugin }) => [VideoPlugin]),
true,
import('@photo-sphere-viewer/video-plugin/index.css'),
] as [PromiseLike<AdapterConstructor>, Promise<PluginConstructor[]>, true, unknown])
: ([undefined, [], false] as [undefined, [], false]);
const loadAssetData = async () => { const loadAssetData = async () => {
const data = await serveFile({ id: asset.id, isWeb: false, isThumb: false, key: getKey() }); const data = await serveFile({ id: asset.id, isWeb: false, isThumb: false, key: getKey() });
return URL.createObjectURL(data); const url = URL.createObjectURL(data);
if (asset.type === AssetTypeEnum.Video) {
return { source: url };
}
return url;
}; };
</script> </script>
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center"> <div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
<!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data --> <!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data -->
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])} {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])}
<LoadingSpinner /> <LoadingSpinner />
{:then [data, module]} {:then [data, module, adapter, plugins, navbar]}
<svelte:component this={module.default} panorama={data} /> <svelte:component this={module.default} panorama={data} plugins={plugins ?? undefined} {navbar} {adapter} />
{:catch} {:catch}
Failed to load asset Failed to load asset
{/await} {/await}
@@ -1,17 +1,32 @@
<script lang="ts"> <script lang="ts">
import { Viewer } from '@photo-sphere-viewer/core'; import {
Viewer,
EquirectangularAdapter,
type PluginConstructor,
type AdapterConstructor,
} from '@photo-sphere-viewer/core';
import '@photo-sphere-viewer/core/index.css'; import '@photo-sphere-viewer/core/index.css';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
export let panorama: string; export let panorama: string | { source: string };
export let adapter: AdapterConstructor | [AdapterConstructor, unknown] = EquirectangularAdapter;
export let plugins: (PluginConstructor | [PluginConstructor, unknown])[] = [];
export let navbar = false;
let container: HTMLDivElement; let container: HTMLDivElement;
let viewer: Viewer; let viewer: Viewer;
onMount(() => { onMount(() => {
viewer = new Viewer({ viewer = new Viewer({
adapter,
plugins,
container, container,
panorama, panorama,
navbar: false, touchmoveTwoFingers: true,
mousewheelCtrlKey: false,
navbar,
maxFov: 180,
fisheye: true,
}); });
}); });
@@ -14,7 +14,7 @@
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { getAltText } from '$lib/utils/thumbnail-util'; import { getAltText } from '$lib/utils/thumbnail-util';
import { slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
const { slideshowState, slideshowLook } = slideshowStore; const { slideshowState, slideshowLook } = slideshowStore;
@@ -150,15 +150,24 @@
<div <div
bind:this={element} bind:this={element}
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
class="flex h-full select-none place-content-center place-items-center" class="relative h-full select-none"
> >
{#if !imageLoaded} {#if !imageLoaded}
<LoadingSpinner /> <div class="flex h-full items-center justify-center">
<LoadingSpinner />
</div>
{:else} {:else}
<div bind:this={imgElement} class="h-full w-full"> <div bind:this={imgElement} class="h-full w-full" transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}>
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
<img
src={assetData}
alt={getAltText(asset)}
class="absolute top-0 left-0 -z-10 object-cover h-full w-full blur-lg"
draggable="false"
/>
{/if}
<img <img
bind:this={$photoViewer} bind:this={$photoViewer}
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
src={assetData} src={assetData}
alt={getAltText(asset)} alt={getAltText(asset)}
class="h-full w-full {$slideshowState === SlideshowState.None class="h-full w-full {$slideshowState === SlideshowState.None
@@ -0,0 +1,15 @@
<script lang="ts">
import { AssetTypeEnum } from '@immich/sdk';
import { ProjectionType } from '$lib/constants';
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
import PanoramaViewer from '$lib/components/asset-viewer/panorama-viewer.svelte';
export let assetId: string;
export let projectionType: string | null | undefined;
</script>
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
{:else}
<VideoNativeViewer {assetId} on:onVideoEnded on:onVideoStarted />
{/if}
@@ -83,6 +83,7 @@
hoverLabel = new Date(attr).toLocaleString($locale, { hoverLabel = new Date(attr).toLocaleString($locale, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
timeZone: 'UTC',
}); });
}; };
@@ -4,7 +4,14 @@
SettingInputFieldType, SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte'; } from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { mdiArrowDownThin, mdiArrowUpThin, mdiFitToPageOutline, mdiFitToScreenOutline, mdiShuffle } from '@mdi/js'; import {
mdiArrowDownThin,
mdiArrowUpThin,
mdiFitToPageOutline,
mdiFitToScreenOutline,
mdiPanorama,
mdiShuffle,
} from '@mdi/js';
import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store'; import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
import Button from './elements/buttons/button.svelte'; import Button from './elements/buttons/button.svelte';
import type { RenderedOption } from './elements/dropdown.svelte'; import type { RenderedOption } from './elements/dropdown.svelte';
@@ -23,6 +30,7 @@
const lookOptions: Record<SlideshowLook, RenderedOption> = { const lookOptions: Record<SlideshowLook, RenderedOption> = {
[SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' }, [SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' },
[SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' }, [SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' },
[SlideshowLook.BlurredBackground]: { icon: mdiPanorama, title: 'Blurred background' },
}; };
const handleToggle = <Type extends SlideshowNavigation | SlideshowLook>( const handleToggle = <Type extends SlideshowNavigation | SlideshowLook>(
+2
View File
@@ -16,11 +16,13 @@ export enum SlideshowNavigation {
export enum SlideshowLook { export enum SlideshowLook {
Contain = 'contain', Contain = 'contain',
Cover = 'cover', Cover = 'cover',
BlurredBackground = 'blurred-background',
} }
export const slideshowLookCssMapping: Record<SlideshowLook, string> = { export const slideshowLookCssMapping: Record<SlideshowLook, string> = {
[SlideshowLook.Contain]: 'object-contain', [SlideshowLook.Contain]: 'object-contain',
[SlideshowLook.Cover]: 'object-cover', [SlideshowLook.Cover]: 'object-cover',
[SlideshowLook.BlurredBackground]: 'object-contain',
}; };
function createSlideshowStore() { function createSlideshowStore() {
+10 -2
View File
@@ -39,8 +39,12 @@
try { try {
await emptyTrash(); await emptyTrash();
const deletedAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = deletedAssetIds.length;
assetStore.removeAssets(deletedAssetIds);
notificationController.show({ notificationController.show({
message: `Empty trash initiated. Refresh the page to see the changes`, message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
@@ -52,8 +56,12 @@
try { try {
await restoreTrash(); await restoreTrash();
const restoredAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = restoredAssetIds.length;
assetStore.removeAssets(restoredAssetIds);
notificationController.show({ notificationController.show({
message: `Restore trash initiated. Refresh the page to see the changes`, message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {