Merge branch 'main' into main

This commit is contained in:
Lena Tauchner 2024-08-17 14:51:07 +02:00 committed by GitHub
commit 5c927856b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 836 additions and 152 deletions

View File

@ -73,14 +73,14 @@ You can edit the link properties, options include;
- **Allow public user to download -** whether to allow whoever has the link to download all the images or a certain image (optional). - **Allow public user to download -** whether to allow whoever has the link to download all the images or a certain image (optional).
- **Allow public user to upload -** whether to allow whoever has the link to upload assets to the album (optional). - **Allow public user to upload -** whether to allow whoever has the link to upload assets to the album (optional).
:::info :::info
whoever has the link and have uploaded files cannot delete them. Whoever has the link and have uploaded files cannot delete them.
::: :::
- **Expire after -** adding an expiration date to the link (optional). - **Expire after -** adding an expiration date to the link (optional).
## Share Specific Assets ## Share Specific Assets
A user can share specific assets without linking them to a specific album. A user can share specific assets without linking them to a specific album.
in order to do so; In order to do this:
1. Go to the timeline 1. Go to the timeline
2. Select the assets (Shift can be used for multiple selection) 2. Select the assets (Shift can be used for multiple selection)

View File

@ -4,7 +4,7 @@ sidebar_position: 10
# Requirements # Requirements
Hardware and software requirements for Immich Hardware and software requirements for Immich:
## Software ## Software

View File

@ -45,7 +45,7 @@ width="70%"
alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich" alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/> />
3. Select the cog ⚙️ next to Immich then click "**Edit Stack**" 3. Select the cogwheel ⚙️ next to Immich and click "**Edit Stack**"
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed. 4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed.
<details > <details >
@ -130,7 +130,7 @@ For more information on how to use the application once installed, please refer
## Updating Steps ## Updating Steps
Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman ui, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager. Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman UI, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager.
<img <img
src={require('./img/unraid09.png').default} src={require('./img/unraid09.png').default}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -1,12 +1,12 @@
import { LoginResponseDto, createApiKey } from '@immich/sdk'; import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const create = (accessToken: string) => const create = (accessToken: string, permissions: Permission[]) =>
createApiKey({ apiKeyCreateDto: { name: 'api key' } }, { headers: asBearerAuth(accessToken) }); createApiKey({ apiKeyCreateDto: { name: 'api key', permissions } }, { headers: asBearerAuth(accessToken) });
describe('/api-keys', () => { describe('/api-keys', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
@ -30,15 +30,65 @@ describe('/api-keys', () => {
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should not work without permission', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]);
const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('apiKey.create'));
});
it('should work with apiKey.create', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate, Permission.ApiKeyRead]);
const { status, body } = await request(app)
.post('/api-keys')
.set('x-api-key', secret)
.send({
name: 'API Key',
permissions: [Permission.ApiKeyRead],
});
expect(body).toEqual({
secret: expect.any(String),
apiKey: {
id: expect.any(String),
name: 'API Key',
permissions: [Permission.ApiKeyRead],
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
});
expect(status).toBe(201);
});
it('should not create an api key with all permissions', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]);
const { status, body } = await request(app)
.post('/api-keys')
.set('x-api-key', secret)
.send({ name: 'API Key', permissions: [Permission.All] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have'));
});
it('should not create an api key with more permissions', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]);
const { status, body } = await request(app)
.post('/api-keys')
.set('x-api-key', secret)
.send({ name: 'API Key', permissions: [Permission.ApiKeyRead] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have'));
});
it('should create an api key', async () => { it('should create an api key', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/api-keys') .post('/api-keys')
.send({ name: 'API Key' }) .send({ name: 'API Key', permissions: [Permission.All] })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual({ expect(body).toEqual({
apiKey: { apiKey: {
id: expect.any(String), id: expect.any(String),
name: 'API Key', name: 'API Key',
permissions: [Permission.All],
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
}, },
@ -63,9 +113,9 @@ describe('/api-keys', () => {
it('should return a list of api keys', async () => { it('should return a list of api keys', async () => {
const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([ const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([
create(admin.accessToken), create(admin.accessToken, [Permission.All]),
create(admin.accessToken), create(admin.accessToken, [Permission.All]),
create(admin.accessToken), create(admin.accessToken, [Permission.All]),
]); ]);
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toHaveLength(3); expect(body).toHaveLength(3);
@ -82,7 +132,7 @@ describe('/api-keys', () => {
}); });
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/api-keys/${apiKey.id}`) .get(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@ -99,7 +149,7 @@ describe('/api-keys', () => {
}); });
it('should get api key details', async () => { it('should get api key details', async () => {
const { apiKey } = await create(user.accessToken); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/api-keys/${apiKey.id}`) .get(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${user.accessToken}`); .set('Authorization', `Bearer ${user.accessToken}`);
@ -107,6 +157,7 @@ describe('/api-keys', () => {
expect(body).toEqual({ expect(body).toEqual({
id: expect.any(String), id: expect.any(String),
name: 'api key', name: 'api key',
permissions: [Permission.All],
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
}); });
@ -121,7 +172,7 @@ describe('/api-keys', () => {
}); });
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/api-keys/${apiKey.id}`) .put(`/api-keys/${apiKey.id}`)
.send({ name: 'new name' }) .send({ name: 'new name' })
@ -140,7 +191,7 @@ describe('/api-keys', () => {
}); });
it('should update api key details', async () => { it('should update api key details', async () => {
const { apiKey } = await create(user.accessToken); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/api-keys/${apiKey.id}`) .put(`/api-keys/${apiKey.id}`)
.send({ name: 'new name' }) .send({ name: 'new name' })
@ -149,6 +200,7 @@ describe('/api-keys', () => {
expect(body).toEqual({ expect(body).toEqual({
id: expect.any(String), id: expect.any(String),
name: 'new name', name: 'new name',
permissions: [Permission.All],
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
}); });
@ -163,7 +215,7 @@ describe('/api-keys', () => {
}); });
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/api-keys/${apiKey.id}`) .delete(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@ -180,7 +232,7 @@ describe('/api-keys', () => {
}); });
it('should delete an api key', async () => { it('should delete an api key', async () => {
const { apiKey } = await create(user.accessToken); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status } = await request(app) const { status } = await request(app)
.delete(`/api-keys/${apiKey.id}`) .delete(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${user.accessToken}`); .set('Authorization', `Bearer ${user.accessToken}`);
@ -190,14 +242,14 @@ describe('/api-keys', () => {
describe('authentication', () => { describe('authentication', () => {
it('should work as a header', async () => { it('should work as a header', async () => {
const { secret } = await create(admin.accessToken); const { secret } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app).get('/api-keys').set('x-api-key', secret); const { status, body } = await request(app).get('/api-keys').set('x-api-key', secret);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
expect(status).toBe(200); expect(status).toBe(200);
}); });
it('should work as a query param', async () => { it('should work as a query param', async () => {
const { secret } = await create(admin.accessToken); const { secret } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app).get(`/api-keys?apiKey=${secret}`); const { status, body } = await request(app).get(`/api-keys?apiKey=${secret}`);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
expect(status).toBe(200); expect(status).toBe(200);

View File

@ -1,3 +1,4 @@
import { Permission } from '@immich/sdk';
import { stat } from 'node:fs/promises'; import { stat } from 'node:fs/promises';
import { app, immichCli, utils } from 'src/utils'; import { app, immichCli, utils } from 'src/utils';
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
@ -29,7 +30,7 @@ describe(`immich login`, () => {
it('should login and save auth.yml with 600', async () => { it('should login and save auth.yml with 600', async () => {
const admin = await utils.adminSetup(); const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken); const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]); const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
expect(stdout.split('\n')).toEqual([ expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2283/api', 'Logging in to http://127.0.0.1:2283/api',
@ -46,7 +47,7 @@ describe(`immich login`, () => {
it('should login without /api in the url', async () => { it('should login without /api in the url', async () => {
const admin = await utils.adminSetup(); const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken); const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]); const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
expect(stdout.split('\n')).toEqual([ expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2283', 'Logging in to http://127.0.0.1:2283',

View File

@ -13,6 +13,12 @@ export const errorDto = {
message: expect.any(String), message: expect.any(String),
correlationId: expect.any(String), correlationId: expect.any(String),
}, },
missingPermission: (permission: string) => ({
error: 'Forbidden',
statusCode: 403,
message: `Missing required permission: ${permission}`,
correlationId: expect.any(String),
}),
wrongPassword: { wrongPassword: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,

View File

@ -7,6 +7,7 @@ import {
CreateAlbumDto, CreateAlbumDto,
CreateLibraryDto, CreateLibraryDto,
MetadataSearchDto, MetadataSearchDto,
Permission,
PersonCreateDto, PersonCreateDto,
SharedLinkCreateDto, SharedLinkCreateDto,
UserAdminCreateDto, UserAdminCreateDto,
@ -279,8 +280,8 @@ export const utils = {
}); });
}, },
createApiKey: (accessToken: string) => { createApiKey: (accessToken: string, permissions: Permission[]) => {
return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) }); return createApiKey({ apiKeyCreateDto: { name: 'e2e', permissions } }, { headers: asBearerAuth(accessToken) });
}, },
createAlbum: (accessToken: string, dto: CreateAlbumDto) => createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
@ -492,7 +493,7 @@ export const utils = {
}, },
cliLogin: async (accessToken: string) => { cliLogin: async (accessToken: string) => {
const key = await utils.createApiKey(accessToken); const key = await utils.createApiKey(accessToken, [Permission.All]);
await immichCli(['login', app, `${key.secret}`]); await immichCli(['login', app, `${key.secret}`]);
return key.secret; return key.secret;
}, },

View File

@ -55,13 +55,13 @@
"asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid", "asset_list_settings_title": "Photo Grid",
"asset_restored_successfully": "Asset restored successfully", "asset_restored_successfully": "Asset restored successfully",
"asset_viewer_settings_title": "Asset Viewer",
"assets_deleted_permanently": "{} asset(s) deleted permanently", "assets_deleted_permanently": "{} asset(s) deleted permanently",
"assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
"assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
"assets_restored_successfully": "{} asset(s) restored successfully", "assets_restored_successfully": "{} asset(s) restored successfully",
"assets_trashed": "{} asset(s) trashed", "assets_trashed": "{} asset(s) trashed",
"assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
"asset_viewer_settings_title": "Asset Viewer",
"backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_device": "Albums on device ({})",
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
@ -173,6 +173,7 @@
"control_bottom_app_bar_delete": "Delete", "control_bottom_app_bar_delete": "Delete",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich", "control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device", "control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_download": "Download",
"control_bottom_app_bar_edit": "Edit", "control_bottom_app_bar_edit": "Edit",
"control_bottom_app_bar_edit_location": "Edit Location", "control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_edit_time": "Edit Date & Time", "control_bottom_app_bar_edit_time": "Edit Date & Time",
@ -455,15 +456,18 @@
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings", "setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping", "setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos", "setting_video_viewer_title": "Videos",
"settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_add_title": "Add a title", "share_add_title": "Add a title",
"share_assets_selected": "{} selected", "share_assets_selected": "{} selected",
"share_create_album": "Create album", "share_create_album": "Create album",
"share_dialog_preparing": "Preparing...",
"share_done": "Done",
"share_invite": "Invite to album",
"shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_disable": "Comment is disabled",
"shared_album_activities_input_hint": "Say something", "shared_album_activities_input_hint": "Say something",
"shared_album_activity_remove_content": "Do you want to delete this activity?", "shared_album_activity_remove_content": "Do you want to delete this activity?",
@ -475,7 +479,6 @@
"shared_album_section_people_action_remove_user": "Remove user from album", "shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_owner_label": "Owner", "shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE", "shared_album_section_people_title": "PEOPLE",
"share_dialog_preparing": "Preparing...",
"shared_link_app_bar_title": "Shared Links", "shared_link_app_bar_title": "Shared Links",
"shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPassword: {}", "shared_link_clipboard_text": "Link: {}\nPassword: {}",
@ -521,14 +524,12 @@
"shared_link_info_chip_upload": "Upload", "shared_link_info_chip_upload": "Upload",
"shared_link_manage_links": "Manage Shared links", "shared_link_manage_links": "Manage Shared links",
"shared_link_public_album": "Public album", "shared_link_public_album": "Public album",
"share_done": "Done",
"share_invite": "Invite to album",
"sharing_page_album": "Shared albums", "sharing_page_album": "Shared albums",
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
"sharing_page_empty_list": "EMPTY LIST", "sharing_page_empty_list": "EMPTY LIST",
"sharing_silver_appbar_create_shared_album": "New shared album", "sharing_silver_appbar_create_shared_album": "New shared album",
"sharing_silver_appbar_shared_links": "Shared links",
"sharing_silver_appbar_share_partner": "Share with partner", "sharing_silver_appbar_share_partner": "Share with partner",
"sharing_silver_appbar_shared_links": "Shared links",
"tab_controller_nav_library": "Library", "tab_controller_nav_library": "Library",
"tab_controller_nav_photos": "Photos", "tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search", "tab_controller_nav_search": "Search",

View File

@ -51,8 +51,8 @@ class BackupControllerPage extends HookConsumerWidget {
} }
void stopScreenDarkenTimer() { void stopScreenDarkenTimer() {
isScreenDarkened.value = false;
darkenScreenTimer.value?.cancel(); darkenScreenTimer.value?.cancel();
isScreenDarkened.value = false;
SystemChrome.setEnabledSystemUIMode( SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual, SystemUiMode.manual,
overlays: [ overlays: [
@ -75,8 +75,6 @@ class BackupControllerPage extends HookConsumerWidget {
.watch(websocketProvider.notifier) .watch(websocketProvider.notifier)
.stopListenToEvent('on_upload_success'); .stopListenToEvent('on_upload_success');
WakelockPlus.enable();
return () { return () {
WakelockPlus.disable(); WakelockPlus.disable();
darkenScreenTimer.value?.cancel(); darkenScreenTimer.value?.cancel();
@ -102,8 +100,10 @@ class BackupControllerPage extends HookConsumerWidget {
() { () {
if (backupState.backupProgress == BackUpProgressEnum.inProgress) { if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
startScreenDarkenTimer(); startScreenDarkenTimer();
WakelockPlus.enable();
} else { } else {
stopScreenDarkenTimer(); stopScreenDarkenTimer();
WakelockPlus.disable();
} }
return null; return null;

View File

@ -20,7 +20,7 @@ import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart' as pm;
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
final backupServiceProvider = Provider( final backupServiceProvider = Provider(
@ -213,7 +213,7 @@ class BackupService {
_appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets);
if (Platform.isAndroid && if (Platform.isAndroid &&
!(await Permission.accessMediaLocation.status).isGranted) { !(await pm.Permission.accessMediaLocation.status).isGranted) {
// double check that permission is granted here, to guard against // double check that permission is granted here, to guard against
// uploading corrupt assets without EXIF information // uploading corrupt assets without EXIF information
_log.warning("Media location permission is not granted. " _log.warning("Media location permission is not granted. "

View File

@ -29,7 +29,8 @@ class LocalNotificationService {
static const cancelUploadActionID = 'cancel_upload'; static const cancelUploadActionID = 'cancel_upload';
Future<void> setup() async { Future<void> setup() async {
const androidSetting = AndroidInitializationSettings('notification_icon'); const androidSetting =
AndroidInitializationSettings('@drawable/notification_icon');
const iosSetting = DarwinInitializationSettings(); const iosSetting = DarwinInitializationSettings();
const initSettings = const initSettings =

View File

@ -366,8 +366,8 @@ class BottomGalleryBar extends ConsumerWidget {
{ {
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.download_outlined), icon: const Icon(Icons.download_outlined),
label: 'download'.tr(), label: 'control_bottom_app_bar_download'.tr(),
tooltip: 'download'.tr(), tooltip: 'control_bottom_app_bar_download'.tr(),
): (_) => handleDownload(), ): (_) => handleDownload(),
}, },
if (isInAlbum) if (isInAlbum)

View File

@ -363,6 +363,7 @@ Class | Method | HTTP request | Description
- [PeopleResponseDto](doc//PeopleResponseDto.md) - [PeopleResponseDto](doc//PeopleResponseDto.md)
- [PeopleUpdateDto](doc//PeopleUpdateDto.md) - [PeopleUpdateDto](doc//PeopleUpdateDto.md)
- [PeopleUpdateItem](doc//PeopleUpdateItem.md) - [PeopleUpdateItem](doc//PeopleUpdateItem.md)
- [Permission](doc//Permission.md)
- [PersonCreateDto](doc//PersonCreateDto.md) - [PersonCreateDto](doc//PersonCreateDto.md)
- [PersonResponseDto](doc//PersonResponseDto.md) - [PersonResponseDto](doc//PersonResponseDto.md)
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)

View File

@ -175,6 +175,7 @@ part 'model/path_type.dart';
part 'model/people_response_dto.dart'; part 'model/people_response_dto.dart';
part 'model/people_update_dto.dart'; part 'model/people_update_dto.dart';
part 'model/people_update_item.dart'; part 'model/people_update_item.dart';
part 'model/permission.dart';
part 'model/person_create_dto.dart'; part 'model/person_create_dto.dart';
part 'model/person_response_dto.dart'; part 'model/person_response_dto.dart';
part 'model/person_statistics_response_dto.dart'; part 'model/person_statistics_response_dto.dart';

View File

@ -407,6 +407,8 @@ class ApiClient {
return PeopleUpdateDto.fromJson(value); return PeopleUpdateDto.fromJson(value);
case 'PeopleUpdateItem': case 'PeopleUpdateItem':
return PeopleUpdateItem.fromJson(value); return PeopleUpdateItem.fromJson(value);
case 'Permission':
return PermissionTypeTransformer().decode(value);
case 'PersonCreateDto': case 'PersonCreateDto':
return PersonCreateDto.fromJson(value); return PersonCreateDto.fromJson(value);
case 'PersonResponseDto': case 'PersonResponseDto':

View File

@ -112,6 +112,9 @@ String parameterToString(dynamic value) {
if (value is PathType) { if (value is PathType) {
return PathTypeTypeTransformer().encode(value).toString(); return PathTypeTypeTransformer().encode(value).toString();
} }
if (value is Permission) {
return PermissionTypeTransformer().encode(value).toString();
}
if (value is ReactionLevel) { if (value is ReactionLevel) {
return ReactionLevelTypeTransformer().encode(value).toString(); return ReactionLevelTypeTransformer().encode(value).toString();
} }

View File

@ -14,6 +14,7 @@ class APIKeyCreateDto {
/// Returns a new [APIKeyCreateDto] instance. /// Returns a new [APIKeyCreateDto] instance.
APIKeyCreateDto({ APIKeyCreateDto({
this.name, this.name,
this.permissions = const [],
}); });
/// ///
@ -24,17 +25,21 @@ class APIKeyCreateDto {
/// ///
String? name; String? name;
List<Permission> permissions;
@override @override
bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto &&
other.name == name; other.name == name &&
_deepEquality.equals(other.permissions, permissions);
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(name == null ? 0 : name!.hashCode); (name == null ? 0 : name!.hashCode) +
(permissions.hashCode);
@override @override
String toString() => 'APIKeyCreateDto[name=$name]'; String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -43,6 +48,7 @@ class APIKeyCreateDto {
} else { } else {
// json[r'name'] = null; // json[r'name'] = null;
} }
json[r'permissions'] = this.permissions;
return json; return json;
} }
@ -55,6 +61,7 @@ class APIKeyCreateDto {
return APIKeyCreateDto( return APIKeyCreateDto(
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
permissions: Permission.listFromJson(json[r'permissions']),
); );
} }
return null; return null;
@ -102,6 +109,7 @@ class APIKeyCreateDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'permissions',
}; };
} }

View File

@ -16,6 +16,7 @@ class APIKeyResponseDto {
required this.createdAt, required this.createdAt,
required this.id, required this.id,
required this.name, required this.name,
this.permissions = const [],
required this.updatedAt, required this.updatedAt,
}); });
@ -25,6 +26,8 @@ class APIKeyResponseDto {
String name; String name;
List<Permission> permissions;
DateTime updatedAt; DateTime updatedAt;
@override @override
@ -32,6 +35,7 @@ class APIKeyResponseDto {
other.createdAt == createdAt && other.createdAt == createdAt &&
other.id == id && other.id == id &&
other.name == name && other.name == name &&
_deepEquality.equals(other.permissions, permissions) &&
other.updatedAt == updatedAt; other.updatedAt == updatedAt;
@override @override
@ -40,16 +44,18 @@ class APIKeyResponseDto {
(createdAt.hashCode) + (createdAt.hashCode) +
(id.hashCode) + (id.hashCode) +
(name.hashCode) + (name.hashCode) +
(permissions.hashCode) +
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, updatedAt=$updatedAt]'; String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'permissions'] = this.permissions;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json; return json;
} }
@ -65,6 +71,7 @@ class APIKeyResponseDto {
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
permissions: Permission.listFromJson(json[r'permissions']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'')!,
); );
} }
@ -116,6 +123,7 @@ class APIKeyResponseDto {
'createdAt', 'createdAt',
'id', 'id',
'name', 'name',
'permissions',
'updatedAt', 'updatedAt',
}; };
} }

292
mobile/openapi/lib/model/permission.dart generated Normal file
View File

@ -0,0 +1,292 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class Permission {
/// Instantiate a new enum with the provided [value].
const Permission._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const all = Permission._(r'all');
static const activityPeriodCreate = Permission._(r'activity.create');
static const activityPeriodRead = Permission._(r'activity.read');
static const activityPeriodUpdate = Permission._(r'activity.update');
static const activityPeriodDelete = Permission._(r'activity.delete');
static const activityPeriodStatistics = Permission._(r'activity.statistics');
static const apiKeyPeriodCreate = Permission._(r'apiKey.create');
static const apiKeyPeriodRead = Permission._(r'apiKey.read');
static const apiKeyPeriodUpdate = Permission._(r'apiKey.update');
static const apiKeyPeriodDelete = Permission._(r'apiKey.delete');
static const assetPeriodRead = Permission._(r'asset.read');
static const assetPeriodUpdate = Permission._(r'asset.update');
static const assetPeriodDelete = Permission._(r'asset.delete');
static const assetPeriodRestore = Permission._(r'asset.restore');
static const assetPeriodShare = Permission._(r'asset.share');
static const assetPeriodView = Permission._(r'asset.view');
static const assetPeriodDownload = Permission._(r'asset.download');
static const assetPeriodUpload = Permission._(r'asset.upload');
static const albumPeriodCreate = Permission._(r'album.create');
static const albumPeriodRead = Permission._(r'album.read');
static const albumPeriodUpdate = Permission._(r'album.update');
static const albumPeriodDelete = Permission._(r'album.delete');
static const albumPeriodStatistics = Permission._(r'album.statistics');
static const albumPeriodAddAsset = Permission._(r'album.addAsset');
static const albumPeriodRemoveAsset = Permission._(r'album.removeAsset');
static const albumPeriodShare = Permission._(r'album.share');
static const albumPeriodDownload = Permission._(r'album.download');
static const authDevicePeriodDelete = Permission._(r'authDevice.delete');
static const archivePeriodRead = Permission._(r'archive.read');
static const facePeriodCreate = Permission._(r'face.create');
static const facePeriodRead = Permission._(r'face.read');
static const facePeriodUpdate = Permission._(r'face.update');
static const facePeriodDelete = Permission._(r'face.delete');
static const libraryPeriodCreate = Permission._(r'library.create');
static const libraryPeriodRead = Permission._(r'library.read');
static const libraryPeriodUpdate = Permission._(r'library.update');
static const libraryPeriodDelete = Permission._(r'library.delete');
static const libraryPeriodStatistics = Permission._(r'library.statistics');
static const timelinePeriodRead = Permission._(r'timeline.read');
static const timelinePeriodDownload = Permission._(r'timeline.download');
static const memoryPeriodCreate = Permission._(r'memory.create');
static const memoryPeriodRead = Permission._(r'memory.read');
static const memoryPeriodUpdate = Permission._(r'memory.update');
static const memoryPeriodDelete = Permission._(r'memory.delete');
static const partnerPeriodCreate = Permission._(r'partner.create');
static const partnerPeriodRead = Permission._(r'partner.read');
static const partnerPeriodUpdate = Permission._(r'partner.update');
static const partnerPeriodDelete = Permission._(r'partner.delete');
static const personPeriodCreate = Permission._(r'person.create');
static const personPeriodRead = Permission._(r'person.read');
static const personPeriodUpdate = Permission._(r'person.update');
static const personPeriodDelete = Permission._(r'person.delete');
static const personPeriodStatistics = Permission._(r'person.statistics');
static const personPeriodMerge = Permission._(r'person.merge');
static const personPeriodReassign = Permission._(r'person.reassign');
static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create');
static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
static const sharedLinkPeriodDelete = Permission._(r'sharedLink.delete');
static const systemConfigPeriodRead = Permission._(r'systemConfig.read');
static const systemConfigPeriodUpdate = Permission._(r'systemConfig.update');
static const systemMetadataPeriodRead = Permission._(r'systemMetadata.read');
static const systemMetadataPeriodUpdate = Permission._(r'systemMetadata.update');
static const tagPeriodCreate = Permission._(r'tag.create');
static const tagPeriodRead = Permission._(r'tag.read');
static const tagPeriodUpdate = Permission._(r'tag.update');
static const tagPeriodDelete = Permission._(r'tag.delete');
static const adminPeriodUserPeriodCreate = Permission._(r'admin.user.create');
static const adminPeriodUserPeriodRead = Permission._(r'admin.user.read');
static const adminPeriodUserPeriodUpdate = Permission._(r'admin.user.update');
static const adminPeriodUserPeriodDelete = Permission._(r'admin.user.delete');
/// List of all possible values in this [enum][Permission].
static const values = <Permission>[
all,
activityPeriodCreate,
activityPeriodRead,
activityPeriodUpdate,
activityPeriodDelete,
activityPeriodStatistics,
apiKeyPeriodCreate,
apiKeyPeriodRead,
apiKeyPeriodUpdate,
apiKeyPeriodDelete,
assetPeriodRead,
assetPeriodUpdate,
assetPeriodDelete,
assetPeriodRestore,
assetPeriodShare,
assetPeriodView,
assetPeriodDownload,
assetPeriodUpload,
albumPeriodCreate,
albumPeriodRead,
albumPeriodUpdate,
albumPeriodDelete,
albumPeriodStatistics,
albumPeriodAddAsset,
albumPeriodRemoveAsset,
albumPeriodShare,
albumPeriodDownload,
authDevicePeriodDelete,
archivePeriodRead,
facePeriodCreate,
facePeriodRead,
facePeriodUpdate,
facePeriodDelete,
libraryPeriodCreate,
libraryPeriodRead,
libraryPeriodUpdate,
libraryPeriodDelete,
libraryPeriodStatistics,
timelinePeriodRead,
timelinePeriodDownload,
memoryPeriodCreate,
memoryPeriodRead,
memoryPeriodUpdate,
memoryPeriodDelete,
partnerPeriodCreate,
partnerPeriodRead,
partnerPeriodUpdate,
partnerPeriodDelete,
personPeriodCreate,
personPeriodRead,
personPeriodUpdate,
personPeriodDelete,
personPeriodStatistics,
personPeriodMerge,
personPeriodReassign,
sharedLinkPeriodCreate,
sharedLinkPeriodRead,
sharedLinkPeriodUpdate,
sharedLinkPeriodDelete,
systemConfigPeriodRead,
systemConfigPeriodUpdate,
systemMetadataPeriodRead,
systemMetadataPeriodUpdate,
tagPeriodCreate,
tagPeriodRead,
tagPeriodUpdate,
tagPeriodDelete,
adminPeriodUserPeriodCreate,
adminPeriodUserPeriodRead,
adminPeriodUserPeriodUpdate,
adminPeriodUserPeriodDelete,
];
static Permission? fromJson(dynamic value) => PermissionTypeTransformer().decode(value);
static List<Permission> listFromJson(dynamic json, {bool growable = false,}) {
final result = <Permission>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = Permission.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [Permission] to String,
/// and [decode] dynamic data back to [Permission].
class PermissionTypeTransformer {
factory PermissionTypeTransformer() => _instance ??= const PermissionTypeTransformer._();
const PermissionTypeTransformer._();
String encode(Permission data) => data.value;
/// Decodes a [dynamic value][data] to a Permission.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
Permission? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'all': return Permission.all;
case r'activity.create': return Permission.activityPeriodCreate;
case r'activity.read': return Permission.activityPeriodRead;
case r'activity.update': return Permission.activityPeriodUpdate;
case r'activity.delete': return Permission.activityPeriodDelete;
case r'activity.statistics': return Permission.activityPeriodStatistics;
case r'apiKey.create': return Permission.apiKeyPeriodCreate;
case r'apiKey.read': return Permission.apiKeyPeriodRead;
case r'apiKey.update': return Permission.apiKeyPeriodUpdate;
case r'apiKey.delete': return Permission.apiKeyPeriodDelete;
case r'asset.read': return Permission.assetPeriodRead;
case r'asset.update': return Permission.assetPeriodUpdate;
case r'asset.delete': return Permission.assetPeriodDelete;
case r'asset.restore': return Permission.assetPeriodRestore;
case r'asset.share': return Permission.assetPeriodShare;
case r'asset.view': return Permission.assetPeriodView;
case r'asset.download': return Permission.assetPeriodDownload;
case r'asset.upload': return Permission.assetPeriodUpload;
case r'album.create': return Permission.albumPeriodCreate;
case r'album.read': return Permission.albumPeriodRead;
case r'album.update': return Permission.albumPeriodUpdate;
case r'album.delete': return Permission.albumPeriodDelete;
case r'album.statistics': return Permission.albumPeriodStatistics;
case r'album.addAsset': return Permission.albumPeriodAddAsset;
case r'album.removeAsset': return Permission.albumPeriodRemoveAsset;
case r'album.share': return Permission.albumPeriodShare;
case r'album.download': return Permission.albumPeriodDownload;
case r'authDevice.delete': return Permission.authDevicePeriodDelete;
case r'archive.read': return Permission.archivePeriodRead;
case r'face.create': return Permission.facePeriodCreate;
case r'face.read': return Permission.facePeriodRead;
case r'face.update': return Permission.facePeriodUpdate;
case r'face.delete': return Permission.facePeriodDelete;
case r'library.create': return Permission.libraryPeriodCreate;
case r'library.read': return Permission.libraryPeriodRead;
case r'library.update': return Permission.libraryPeriodUpdate;
case r'library.delete': return Permission.libraryPeriodDelete;
case r'library.statistics': return Permission.libraryPeriodStatistics;
case r'timeline.read': return Permission.timelinePeriodRead;
case r'timeline.download': return Permission.timelinePeriodDownload;
case r'memory.create': return Permission.memoryPeriodCreate;
case r'memory.read': return Permission.memoryPeriodRead;
case r'memory.update': return Permission.memoryPeriodUpdate;
case r'memory.delete': return Permission.memoryPeriodDelete;
case r'partner.create': return Permission.partnerPeriodCreate;
case r'partner.read': return Permission.partnerPeriodRead;
case r'partner.update': return Permission.partnerPeriodUpdate;
case r'partner.delete': return Permission.partnerPeriodDelete;
case r'person.create': return Permission.personPeriodCreate;
case r'person.read': return Permission.personPeriodRead;
case r'person.update': return Permission.personPeriodUpdate;
case r'person.delete': return Permission.personPeriodDelete;
case r'person.statistics': return Permission.personPeriodStatistics;
case r'person.merge': return Permission.personPeriodMerge;
case r'person.reassign': return Permission.personPeriodReassign;
case r'sharedLink.create': return Permission.sharedLinkPeriodCreate;
case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate;
case r'sharedLink.delete': return Permission.sharedLinkPeriodDelete;
case r'systemConfig.read': return Permission.systemConfigPeriodRead;
case r'systemConfig.update': return Permission.systemConfigPeriodUpdate;
case r'systemMetadata.read': return Permission.systemMetadataPeriodRead;
case r'systemMetadata.update': return Permission.systemMetadataPeriodUpdate;
case r'tag.create': return Permission.tagPeriodCreate;
case r'tag.read': return Permission.tagPeriodRead;
case r'tag.update': return Permission.tagPeriodUpdate;
case r'tag.delete': return Permission.tagPeriodDelete;
case r'admin.user.create': return Permission.adminPeriodUserPeriodCreate;
case r'admin.user.read': return Permission.adminPeriodUserPeriodRead;
case r'admin.user.update': return Permission.adminPeriodUserPeriodUpdate;
case r'admin.user.delete': return Permission.adminPeriodUserPeriodDelete;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [PermissionTypeTransformer] instance.
static PermissionTypeTransformer? _instance;
}

View File

@ -7135,8 +7135,17 @@
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string"
},
"permissions": {
"items": {
"$ref": "#/components/schemas/Permission"
},
"type": "array"
} }
}, },
"required": [
"permissions"
],
"type": "object" "type": "object"
}, },
"APIKeyCreateResponseDto": { "APIKeyCreateResponseDto": {
@ -7166,6 +7175,12 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"permissions": {
"items": {
"$ref": "#/components/schemas/Permission"
},
"type": "array"
},
"updatedAt": { "updatedAt": {
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
@ -7175,6 +7190,7 @@
"createdAt", "createdAt",
"id", "id",
"name", "name",
"permissions",
"updatedAt" "updatedAt"
], ],
"type": "object" "type": "object"
@ -9729,6 +9745,82 @@
], ],
"type": "object" "type": "object"
}, },
"Permission": {
"enum": [
"all",
"activity.create",
"activity.read",
"activity.update",
"activity.delete",
"activity.statistics",
"apiKey.create",
"apiKey.read",
"apiKey.update",
"apiKey.delete",
"asset.read",
"asset.update",
"asset.delete",
"asset.restore",
"asset.share",
"asset.view",
"asset.download",
"asset.upload",
"album.create",
"album.read",
"album.update",
"album.delete",
"album.statistics",
"album.addAsset",
"album.removeAsset",
"album.share",
"album.download",
"authDevice.delete",
"archive.read",
"face.create",
"face.read",
"face.update",
"face.delete",
"library.create",
"library.read",
"library.update",
"library.delete",
"library.statistics",
"timeline.read",
"timeline.download",
"memory.create",
"memory.read",
"memory.update",
"memory.delete",
"partner.create",
"partner.read",
"partner.update",
"partner.delete",
"person.create",
"person.read",
"person.update",
"person.delete",
"person.statistics",
"person.merge",
"person.reassign",
"sharedLink.create",
"sharedLink.read",
"sharedLink.update",
"sharedLink.delete",
"systemConfig.read",
"systemConfig.update",
"systemMetadata.read",
"systemMetadata.update",
"tag.create",
"tag.read",
"tag.update",
"tag.delete",
"admin.user.create",
"admin.user.read",
"admin.user.update",
"admin.user.delete"
],
"type": "string"
},
"PersonCreateDto": { "PersonCreateDto": {
"properties": { "properties": {
"birthDate": { "birthDate": {

View File

@ -299,10 +299,12 @@ export type ApiKeyResponseDto = {
createdAt: string; createdAt: string;
id: string; id: string;
name: string; name: string;
permissions: Permission[];
updatedAt: string; updatedAt: string;
}; };
export type ApiKeyCreateDto = { export type ApiKeyCreateDto = {
name?: string; name?: string;
permissions: Permission[];
}; };
export type ApiKeyCreateResponseDto = { export type ApiKeyCreateResponseDto = {
apiKey: ApiKeyResponseDto; apiKey: ApiKeyResponseDto;
@ -3125,6 +3127,79 @@ export enum Error {
NotFound = "not_found", NotFound = "not_found",
Unknown = "unknown" Unknown = "unknown"
} }
export enum Permission {
All = "all",
ActivityCreate = "activity.create",
ActivityRead = "activity.read",
ActivityUpdate = "activity.update",
ActivityDelete = "activity.delete",
ActivityStatistics = "activity.statistics",
ApiKeyCreate = "apiKey.create",
ApiKeyRead = "apiKey.read",
ApiKeyUpdate = "apiKey.update",
ApiKeyDelete = "apiKey.delete",
AssetRead = "asset.read",
AssetUpdate = "asset.update",
AssetDelete = "asset.delete",
AssetRestore = "asset.restore",
AssetShare = "asset.share",
AssetView = "asset.view",
AssetDownload = "asset.download",
AssetUpload = "asset.upload",
AlbumCreate = "album.create",
AlbumRead = "album.read",
AlbumUpdate = "album.update",
AlbumDelete = "album.delete",
AlbumStatistics = "album.statistics",
AlbumAddAsset = "album.addAsset",
AlbumRemoveAsset = "album.removeAsset",
AlbumShare = "album.share",
AlbumDownload = "album.download",
AuthDeviceDelete = "authDevice.delete",
ArchiveRead = "archive.read",
FaceCreate = "face.create",
FaceRead = "face.read",
FaceUpdate = "face.update",
FaceDelete = "face.delete",
LibraryCreate = "library.create",
LibraryRead = "library.read",
LibraryUpdate = "library.update",
LibraryDelete = "library.delete",
LibraryStatistics = "library.statistics",
TimelineRead = "timeline.read",
TimelineDownload = "timeline.download",
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",
PersonStatistics = "person.statistics",
PersonMerge = "person.merge",
PersonReassign = "person.reassign",
SharedLinkCreate = "sharedLink.create",
SharedLinkRead = "sharedLink.read",
SharedLinkUpdate = "sharedLink.update",
SharedLinkDelete = "sharedLink.delete",
SystemConfigRead = "systemConfig.read",
SystemConfigUpdate = "systemConfig.update",
SystemMetadataRead = "systemMetadata.read",
SystemMetadataUpdate = "systemMetadata.update",
TagCreate = "tag.create",
TagRead = "tag.read",
TagUpdate = "tag.update",
TagDelete = "tag.delete",
AdminUserCreate = "admin.user.create",
AdminUserRead = "admin.user.read",
AdminUserUpdate = "admin.user.update",
AdminUserDelete = "admin.user.delete"
}
export enum AssetMediaStatus { export enum AssetMediaStatus {
Created = "created", Created = "created",
Replaced = "replaced", Replaced = "replaced",

View File

@ -9,6 +9,7 @@ import {
ActivityStatisticsResponseDto, ActivityStatisticsResponseDto,
} from 'src/dtos/activity.dto'; } from 'src/dtos/activity.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { 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';
@ -19,19 +20,19 @@ export class ActivityController {
constructor(private service: ActivityService) {} constructor(private service: ActivityService) {}
@Get() @Get()
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: Permission.ACTIVITY_STATISTICS })
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() @Authenticated({ permission: Permission.ACTIVITY_CREATE })
async createActivity( async createActivity(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: ActivityCreateDto, @Body() dto: ActivityCreateDto,
@ -46,7 +47,7 @@ export class ActivityController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated() @Authenticated({ permission: Permission.ACTIVITY_DELETE })
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View File

@ -12,6 +12,7 @@ import {
} from 'src/dtos/album.dto'; } from 'src/dtos/album.dto';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AlbumService } from 'src/services/album.service'; import { AlbumService } from 'src/services/album.service';
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
@ -22,24 +23,24 @@ export class AlbumController {
constructor(private service: AlbumService) {} constructor(private service: AlbumService) {}
@Get('count') @Get('count')
@Authenticated() @Authenticated({ permission: Permission.ALBUM_STATISTICS })
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() @Authenticated({ permission: 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() @Authenticated({ permission: 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);
} }
@Authenticated({ sharedLink: true }) @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true })
@Get(':id') @Get(':id')
getAlbumInfo( getAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@ -50,7 +51,7 @@ export class AlbumController {
} }
@Patch(':id') @Patch(':id')
@Authenticated() @Authenticated({ permission: Permission.ALBUM_UPDATE })
updateAlbumInfo( updateAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -60,7 +61,7 @@ export class AlbumController {
} }
@Delete(':id') @Delete(':id')
@Authenticated() @Authenticated({ permission: 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);
} }

View File

@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put }
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 } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { APIKeyService } from 'src/services/api-key.service'; import { APIKeyService } from 'src/services/api-key.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -12,25 +13,25 @@ export class APIKeyController {
constructor(private service: APIKeyService) {} constructor(private service: APIKeyService) {}
@Post() @Post()
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: Permission.API_KEY_UPDATE })
updateApiKey( updateApiKey(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -41,7 +42,7 @@ export class APIKeyController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated() @Authenticated({ permission: 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);
} }

View File

@ -2,6 +2,7 @@ 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 } from 'src/dtos/auth.dto';
import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto'; import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PersonService } from 'src/services/person.service'; import { PersonService } from 'src/services/person.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -12,13 +13,13 @@ export class FaceController {
constructor(private service: PersonService) {} constructor(private service: PersonService) {}
@Get() @Get()
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: Permission.FACE_UPDATE })
reassignFacesById( reassignFacesById(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,

View File

@ -9,6 +9,7 @@ import {
ValidateLibraryDto, ValidateLibraryDto,
ValidateLibraryResponseDto, ValidateLibraryResponseDto,
} from 'src/dtos/library.dto'; } from 'src/dtos/library.dto';
import { Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { LibraryService } from 'src/services/library.service'; import { LibraryService } from 'src/services/library.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -19,25 +20,25 @@ export class LibraryController {
constructor(private service: LibraryService) {} constructor(private service: LibraryService) {}
@Get() @Get()
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.LIBRARY_READ, admin: true })
getAllLibraries(): Promise<LibraryResponseDto[]> { getAllLibraries(): Promise<LibraryResponseDto[]> {
return this.service.getAll(); return this.service.getAll();
} }
@Post() @Post()
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.LIBRARY_CREATE, admin: true })
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({ admin: true }) @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true })
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({ admin: true }) @Authenticated({ permission: Permission.LIBRARY_READ, admin: true })
getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> { getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@ -52,13 +53,13 @@ export class LibraryController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true })
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({ admin: true }) @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true })
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> { getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(id); return this.service.getStatistics(id);
} }

View File

@ -3,6 +3,7 @@ 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 } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { MemoryService } from 'src/services/memory.service'; import { MemoryService } from 'src/services/memory.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -13,25 +14,25 @@ export class MemoryController {
constructor(private service: MemoryService) {} constructor(private service: MemoryService) {}
@Get() @Get()
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: Permission.MEMORY_UPDATE })
updateMemory( updateMemory(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -42,7 +43,7 @@ export class MemoryController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated() @Authenticated({ permission: Permission.MEMORY_DELETE })
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);
} }

View File

@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/
import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { Permission } from 'src/enum';
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';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
@ -14,20 +15,20 @@ export class PartnerController {
@Get() @Get()
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
@Authenticated() @Authenticated({ permission: Permission.PARTNER_READ })
// TODO: remove 'direction' and convert to full query dto // TODO: remove 'direction' and convert to full query dto
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> { getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post(':id') @Post(':id')
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: Permission.PARTNER_UPDATE })
updatePartner( updatePartner(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -37,7 +38,7 @@ export class PartnerController {
} }
@Delete(':id') @Delete(':id')
@Authenticated() @Authenticated({ permission: 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);
} }

View File

@ -16,6 +16,7 @@ import {
PersonStatisticsResponseDto, PersonStatisticsResponseDto,
PersonUpdateDto, PersonUpdateDto,
} from 'src/dtos/person.dto'; } from 'src/dtos/person.dto';
import { Permission } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { PersonService } from 'src/services/person.service'; import { PersonService } from 'src/services/person.service';
@ -31,31 +32,31 @@ export class PersonController {
) {} ) {}
@Get() @Get()
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: Permission.PERSON_UPDATE })
updatePerson( updatePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -65,14 +66,14 @@ export class PersonController {
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated() @Authenticated({ permission: Permission.PERSON_STATISTICS })
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')
@FileResponse() @FileResponse()
@Authenticated() @Authenticated({ permission: Permission.PERSON_READ })
async getPersonThumbnail( async getPersonThumbnail(
@Res() res: Response, @Res() res: Response,
@Next() next: NextFunction, @Next() next: NextFunction,
@ -90,7 +91,7 @@ export class PersonController {
} }
@Put(':id/reassign') @Put(':id/reassign')
@Authenticated() @Authenticated({ permission: Permission.PERSON_REASSIGN })
reassignFaces( reassignFaces(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -100,7 +101,7 @@ export class PersonController {
} }
@Post(':id/merge') @Post(':id/merge')
@Authenticated() @Authenticated({ permission: Permission.PERSON_MERGE })
mergePerson( mergePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,

View File

@ -10,6 +10,7 @@ import {
SharedLinkPasswordDto, SharedLinkPasswordDto,
SharedLinkResponseDto, SharedLinkResponseDto,
} from 'src/dtos/shared-link.dto'; } from 'src/dtos/shared-link.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { LoginDetails } from 'src/services/auth.service'; import { LoginDetails } from 'src/services/auth.service';
import { SharedLinkService } from 'src/services/shared-link.service'; import { SharedLinkService } from 'src/services/shared-link.service';
@ -22,7 +23,7 @@ export class SharedLinkController {
constructor(private service: SharedLinkService) {} constructor(private service: SharedLinkService) {}
@Get() @Get()
@Authenticated() @Authenticated({ permission: 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);
} }
@ -48,19 +49,19 @@ export class SharedLinkController {
} }
@Get(':id') @Get(':id')
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: Permission.SHARED_LINK_UPDATE })
updateSharedLink( updateSharedLink(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -70,7 +71,7 @@ export class SharedLinkController {
} }
@Delete(':id') @Delete(':id')
@Authenticated() @Authenticated({ permission: 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);
} }

View File

@ -1,6 +1,7 @@
import { Body, Controller, Get, Put } from '@nestjs/common'; import { Body, Controller, Get, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
import { Permission } from 'src/enum';
import { Authenticated } 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';
@ -10,25 +11,25 @@ export class SystemConfigController {
constructor(private service: SystemConfigService) {} constructor(private service: SystemConfigService) {}
@Get() @Get()
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
getConfig(): Promise<SystemConfigDto> { getConfig(): Promise<SystemConfigDto> {
return this.service.getConfig(); return this.service.getConfig();
} }
@Get('defaults') @Get('defaults')
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
getConfigDefaults(): SystemConfigDto { getConfigDefaults(): SystemConfigDto {
return this.service.getDefaults(); return this.service.getDefaults();
} }
@Put() @Put()
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true })
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({ admin: true }) @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
return this.service.getStorageTemplateOptions(); return this.service.getStorageTemplateOptions();
} }

View File

@ -1,6 +1,7 @@
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 { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto';
import { Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Authenticated } from 'src/middleware/auth.guard';
import { SystemMetadataService } from 'src/services/system-metadata.service'; import { SystemMetadataService } from 'src/services/system-metadata.service';
@ -10,20 +11,20 @@ export class SystemMetadataController {
constructor(private service: SystemMetadataService) {} constructor(private service: SystemMetadataService) {}
@Get('admin-onboarding') @Get('admin-onboarding')
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true })
getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> { getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
return this.service.getAdminOnboarding(); return this.service.getAdminOnboarding();
} }
@Post('admin-onboarding') @Post('admin-onboarding')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.SYSTEM_METADATA_UPDATE, admin: true })
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({ admin: true }) @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true })
getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> { getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
return this.service.getReverseGeocodingState(); return this.service.getReverseGeocodingState();
} }

View File

@ -5,6 +5,7 @@ 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 } from 'src/dtos/auth.dto';
import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto'; import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TagService } from 'src/services/tag.service'; import { TagService } from 'src/services/tag.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -15,31 +16,31 @@ export class TagController {
constructor(private service: TagService) {} constructor(private service: TagService) {}
@Post() @Post()
@Authenticated() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: 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() @Authenticated({ permission: 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);
} }

View File

@ -9,6 +9,7 @@ import {
UserAdminSearchDto, UserAdminSearchDto,
UserAdminUpdateDto, UserAdminUpdateDto,
} from 'src/dtos/user.dto'; } from 'src/dtos/user.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { UserAdminService } from 'src/services/user-admin.service'; import { UserAdminService } from 'src/services/user-admin.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ -19,25 +20,25 @@ export class UserAdminController {
constructor(private service: UserAdminService) {} constructor(private service: UserAdminService) {}
@Get() @Get()
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> { searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post() @Post()
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.ADMIN_USER_CREATE, admin: true })
createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> { createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
return this.service.create(createUserDto); return this.service.create(createUserDto);
} }
@Get(':id') @Get(':id')
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> { getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true })
updateUserAdmin( updateUserAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -47,7 +48,7 @@ export class UserAdminController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true })
deleteUserAdmin( deleteUserAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -57,13 +58,13 @@ export class UserAdminController {
} }
@Get(':id/preferences') @Get(':id/preferences')
@Authenticated() @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> { getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> {
return this.service.getPreferences(auth, id); return this.service.getPreferences(auth, id);
} }
@Put(':id/preferences') @Put(':id/preferences')
@Authenticated() @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true })
updateUserPreferencesAdmin( updateUserPreferencesAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -73,7 +74,7 @@ export class UserAdminController {
} }
@Post(':id/restore') @Post(':id/restore')
@Authenticated({ admin: true }) @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true })
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> { restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.restore(auth, id); return this.service.restore(auth, id);
} }

View File

@ -256,7 +256,7 @@ export class AccessCore {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids); return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
} }
case Permission.MEMORY_WRITE: { case Permission.MEMORY_UPDATE: {
return this.repository.memory.checkOwnerAccess(auth.user.id, ids); return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
} }
@ -272,7 +272,7 @@ export class AccessCore {
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 Permission.PERSON_UPDATE: {
return await this.repository.person.checkOwnerAccess(auth.user.id, ids); return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
} }

View File

@ -1,10 +1,17 @@
import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { Permission } from 'src/enum';
import { Optional } from 'src/validation'; import { Optional } from 'src/validation';
export class APIKeyCreateDto { export class APIKeyCreateDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Optional() @Optional()
name?: string; name?: string;
@IsEnum(Permission, { each: true })
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
@ArrayMinSize(1)
permissions!: Permission[];
} }
export class APIKeyUpdateDto { export class APIKeyUpdateDto {
@ -23,4 +30,6 @@ export class APIKeyResponseDto {
name!: string; name!: string;
createdAt!: Date; createdAt!: Date;
updatedAt!: Date; updatedAt!: Date;
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
permissions!: Permission[];
} }

View File

@ -1,4 +1,5 @@
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { Permission } from 'src/enum';
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@Entity('api_keys') @Entity('api_keys')
@ -18,6 +19,9 @@ export class APIKeyEntity {
@Column() @Column()
userId!: string; userId!: string;
@Column({ array: true, type: 'varchar' })
permissions!: Permission[];
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;

View File

@ -32,8 +32,18 @@ export enum MemoryType {
} }
export enum Permission { export enum Permission {
ALL = 'all',
ACTIVITY_CREATE = 'activity.create', ACTIVITY_CREATE = 'activity.create',
ACTIVITY_READ = 'activity.read',
ACTIVITY_UPDATE = 'activity.update',
ACTIVITY_DELETE = 'activity.delete', ACTIVITY_DELETE = 'activity.delete',
ACTIVITY_STATISTICS = 'activity.statistics',
API_KEY_CREATE = 'apiKey.create',
API_KEY_READ = 'apiKey.read',
API_KEY_UPDATE = 'apiKey.update',
API_KEY_DELETE = 'apiKey.delete',
// ASSET_CREATE = 'asset.create', // ASSET_CREATE = 'asset.create',
ASSET_READ = 'asset.read', ASSET_READ = 'asset.read',
@ -45,10 +55,12 @@ export enum Permission {
ASSET_DOWNLOAD = 'asset.download', ASSET_DOWNLOAD = 'asset.download',
ASSET_UPLOAD = 'asset.upload', ASSET_UPLOAD = 'asset.upload',
// ALBUM_CREATE = 'album.create', ALBUM_CREATE = 'album.create',
ALBUM_READ = 'album.read', ALBUM_READ = 'album.read',
ALBUM_UPDATE = 'album.update', ALBUM_UPDATE = 'album.update',
ALBUM_DELETE = 'album.delete', ALBUM_DELETE = 'album.delete',
ALBUM_STATISTICS = 'album.statistics',
ALBUM_ADD_ASSET = 'album.addAsset', ALBUM_ADD_ASSET = 'album.addAsset',
ALBUM_REMOVE_ASSET = 'album.removeAsset', ALBUM_REMOVE_ASSET = 'album.removeAsset',
ALBUM_SHARE = 'album.share', ALBUM_SHARE = 'album.share',
@ -58,20 +70,58 @@ export enum Permission {
ARCHIVE_READ = 'archive.read', ARCHIVE_READ = 'archive.read',
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',
LIBRARY_STATISTICS = 'library.statistics',
TIMELINE_READ = 'timeline.read', TIMELINE_READ = 'timeline.read',
TIMELINE_DOWNLOAD = 'timeline.download', TIMELINE_DOWNLOAD = 'timeline.download',
MEMORY_CREATE = 'memory.create',
MEMORY_READ = 'memory.read', MEMORY_READ = 'memory.read',
MEMORY_WRITE = 'memory.write', MEMORY_UPDATE = 'memory.update',
MEMORY_DELETE = 'memory.delete', MEMORY_DELETE = 'memory.delete',
PERSON_READ = 'person.read', PARTNER_CREATE = 'partner.create',
PERSON_WRITE = 'person.write', PARTNER_READ = 'partner.read',
PERSON_MERGE = 'person.merge', PARTNER_UPDATE = 'partner.update',
PARTNER_DELETE = 'partner.delete',
PERSON_CREATE = 'person.create', PERSON_CREATE = 'person.create',
PERSON_READ = 'person.read',
PERSON_UPDATE = 'person.update',
PERSON_DELETE = 'person.delete',
PERSON_STATISTICS = 'person.statistics',
PERSON_MERGE = 'person.merge',
PERSON_REASSIGN = 'person.reassign', PERSON_REASSIGN = 'person.reassign',
PARTNER_UPDATE = 'partner.update', SHARED_LINK_CREATE = 'sharedLink.create',
SHARED_LINK_READ = 'sharedLink.read',
SHARED_LINK_UPDATE = 'sharedLink.update',
SHARED_LINK_DELETE = 'sharedLink.delete',
SYSTEM_CONFIG_READ = 'systemConfig.read',
SYSTEM_CONFIG_UPDATE = 'systemConfig.update',
SYSTEM_METADATA_READ = 'systemMetadata.read',
SYSTEM_METADATA_UPDATE = 'systemMetadata.update',
TAG_CREATE = 'tag.create',
TAG_READ = 'tag.read',
TAG_UPDATE = 'tag.update',
TAG_DELETE = 'tag.delete',
ADMIN_USER_CREATE = 'admin.user.create',
ADMIN_USER_READ = 'admin.user.read',
ADMIN_USER_UPDATE = 'admin.user.update',
ADMIN_USER_DELETE = 'admin.user.delete',
} }
export enum SharedLinkType { export enum SharedLinkType {

View File

@ -11,6 +11,7 @@ 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, ImmichQuery } from 'src/dtos/auth.dto'; import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
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';
@ -25,7 +26,7 @@ export enum Metadata {
type AdminRoute = { admin?: true }; type AdminRoute = { admin?: true };
type SharedLinkRoute = { sharedLink?: true }; type SharedLinkRoute = { sharedLink?: true };
type AuthenticatedOptions = AdminRoute | SharedLinkRoute; type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => { export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => {
const decorators: MethodDecorator[] = [ const decorators: MethodDecorator[] = [
@ -89,13 +90,17 @@ export class AuthGuard implements CanActivate {
return true; return true;
} }
const { admin: adminRoute, sharedLink: sharedLinkRoute } = { sharedLink: false, admin: false, ...options }; const {
admin: adminRoute,
sharedLink: sharedLinkRoute,
permission,
} = { sharedLink: false, admin: false, ...options };
const request = context.switchToHttp().getRequest<AuthRequest>(); const request = context.switchToHttp().getRequest<AuthRequest>();
request.user = await this.authService.authenticate({ request.user = await this.authService.authenticate({
headers: request.headers, headers: request.headers,
queryParams: request.query as Record<string, string>, queryParams: request.query as Record<string, string>,
metadata: { adminRoute, sharedLinkRoute, uri: request.path }, metadata: { adminRoute, sharedLinkRoute, permission, uri: request.path },
}); });
return true; return true;

View File

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

View File

@ -9,6 +9,7 @@ FROM
"APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."id" AS "APIKeyEntity_id",
"APIKeyEntity"."key" AS "APIKeyEntity_key", "APIKeyEntity"."key" AS "APIKeyEntity_key",
"APIKeyEntity"."userId" AS "APIKeyEntity_userId", "APIKeyEntity"."userId" AS "APIKeyEntity_userId",
"APIKeyEntity"."permissions" AS "APIKeyEntity_permissions",
"APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id", "APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id",
"APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name", "APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name",
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin", "APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
@ -46,6 +47,7 @@ SELECT
"APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."id" AS "APIKeyEntity_id",
"APIKeyEntity"."name" AS "APIKeyEntity_name", "APIKeyEntity"."name" AS "APIKeyEntity_name",
"APIKeyEntity"."userId" AS "APIKeyEntity_userId", "APIKeyEntity"."userId" AS "APIKeyEntity_userId",
"APIKeyEntity"."permissions" AS "APIKeyEntity_permissions",
"APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt",
"APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt"
FROM FROM
@ -63,6 +65,7 @@ SELECT
"APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."id" AS "APIKeyEntity_id",
"APIKeyEntity"."name" AS "APIKeyEntity_name", "APIKeyEntity"."name" AS "APIKeyEntity_name",
"APIKeyEntity"."userId" AS "APIKeyEntity_userId", "APIKeyEntity"."userId" AS "APIKeyEntity_userId",
"APIKeyEntity"."permissions" AS "APIKeyEntity_permissions",
"APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt",
"APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt"
FROM FROM

View File

@ -31,6 +31,7 @@ export class ApiKeyRepository implements IKeyRepository {
id: true, id: true,
key: true, key: true,
userId: true, userId: true,
permissions: true,
}, },
where: { key: hashedToken }, where: { key: hashedToken },
relations: { relations: {

View File

@ -1,4 +1,5 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { Permission } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { APIKeyService } from 'src/services/api-key.service'; import { APIKeyService } from 'src/services/api-key.service';
@ -22,10 +23,11 @@ describe(APIKeyService.name, () => {
describe('create', () => { describe('create', () => {
it('should create a new key', async () => { it('should create a new key', async () => {
keyMock.create.mockResolvedValue(keyStub.admin); keyMock.create.mockResolvedValue(keyStub.admin);
await sut.create(authStub.admin, { name: 'Test Key' }); await sut.create(authStub.admin, { name: 'Test Key', permissions: [Permission.ALL] });
expect(keyMock.create).toHaveBeenCalledWith({ expect(keyMock.create).toHaveBeenCalledWith({
key: 'cmFuZG9tLWJ5dGVz (hashed)', key: 'cmFuZG9tLWJ5dGVz (hashed)',
name: 'Test Key', name: 'Test Key',
permissions: [Permission.ALL],
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
}); });
expect(cryptoMock.newPassword).toHaveBeenCalled(); expect(cryptoMock.newPassword).toHaveBeenCalled();
@ -35,11 +37,12 @@ describe(APIKeyService.name, () => {
it('should not require a name', async () => { it('should not require a name', async () => {
keyMock.create.mockResolvedValue(keyStub.admin); keyMock.create.mockResolvedValue(keyStub.admin);
await sut.create(authStub.admin, {}); await sut.create(authStub.admin, { permissions: [Permission.ALL] });
expect(keyMock.create).toHaveBeenCalledWith({ expect(keyMock.create).toHaveBeenCalledWith({
key: 'cmFuZG9tLWJ5dGVz (hashed)', key: 'cmFuZG9tLWJ5dGVz (hashed)',
name: 'API Key', name: 'API Key',
permissions: [Permission.ALL],
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
}); });
expect(cryptoMock.newPassword).toHaveBeenCalled(); expect(cryptoMock.newPassword).toHaveBeenCalled();

View File

@ -1,9 +1,10 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/dtos/api-key.dto'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { APIKeyEntity } from 'src/entities/api-key.entity'; import { APIKeyEntity } from 'src/entities/api-key.entity';
import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { isGranted } from 'src/utils/access';
@Injectable() @Injectable()
export class APIKeyService { export class APIKeyService {
@ -14,16 +15,22 @@ export class APIKeyService {
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
const secret = this.crypto.newPassword(32); const secret = this.crypto.newPassword(32);
if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) {
throw new BadRequestException('Cannot grant permissions you do not have');
}
const entity = await this.repository.create({ const entity = await this.repository.create({
key: this.crypto.hashSha256(secret), key: this.crypto.hashSha256(secret),
name: dto.name || 'API Key', name: dto.name || 'API Key',
userId: auth.user.id, userId: auth.user.id,
permissions: dto.permissions,
}); });
return { secret, apiKey: this.map(entity) }; return { secret, apiKey: this.map(entity) };
} }
async update(auth: AuthDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> { async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise<APIKeyResponseDto> {
const exists = await this.repository.getById(auth.user.id, id); const exists = await this.repository.getById(auth.user.id, id);
if (!exists) { if (!exists) {
throw new BadRequestException('API Key not found'); throw new BadRequestException('API Key not found');
@ -62,6 +69,7 @@ export class APIKeyService {
name: entity.name, name: entity.name,
createdAt: entity.createdAt, createdAt: entity.createdAt,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
permissions: entity.permissions,
}; };
} }
} }

View File

@ -31,6 +31,7 @@ import {
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { Permission } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -38,6 +39,7 @@ import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { isGranted } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
export interface LoginDetails { export interface LoginDetails {
@ -61,6 +63,7 @@ export type ValidateRequest = {
metadata: { metadata: {
sharedLinkRoute: boolean; sharedLinkRoute: boolean;
adminRoute: boolean; adminRoute: boolean;
permission?: Permission;
uri: string; uri: string;
}; };
}; };
@ -157,7 +160,7 @@ export class AuthService {
async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise<AuthDto> { async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise<AuthDto> {
const authDto = await this.validate({ headers, queryParams }); const authDto = await this.validate({ headers, queryParams });
const { adminRoute, sharedLinkRoute, uri } = metadata; const { adminRoute, sharedLinkRoute, permission, uri } = metadata;
if (!authDto.user.isAdmin && adminRoute) { if (!authDto.user.isAdmin && adminRoute) {
this.logger.warn(`Denied access to admin only route: ${uri}`); this.logger.warn(`Denied access to admin only route: ${uri}`);
@ -169,6 +172,10 @@ export class AuthService {
throw new ForbiddenException('Forbidden'); throw new ForbiddenException('Forbidden');
} }
if (authDto.apiKey && permission && !isGranted({ requested: [permission], current: authDto.apiKey.permissions })) {
throw new ForbiddenException(`Missing required permission: ${permission}`);
}
return authDto; return authDto;
} }

View File

@ -50,7 +50,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, Permission.MEMORY_UPDATE, id);
const memory = await this.repository.update({ const memory = await this.repository.update({
id, id,
@ -82,7 +82,7 @@ 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, Permission.MEMORY_UPDATE, id);
const repos = { accessRepository: this.accessRepository, repository: this.repository }; const repos = { accessRepository: this.accessRepository, repository: this.repository };
const results = await removeAssets(auth, repos, { const results = await removeAssets(auth, repos, {

View File

@ -113,7 +113,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, Permission.PERSON_UPDATE, 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[] = [];
@ -142,7 +142,7 @@ 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, Permission.PERSON_UPDATE, personId);
await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id); await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id);
const face = await this.repository.getFaceById(dto.id); const face = await this.repository.getFaceById(dto.id);
@ -226,7 +226,7 @@ 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, Permission.PERSON_UPDATE, 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
@ -581,7 +581,7 @@ export class PersonService {
throw new BadRequestException('Cannot merge a person into themselves'); throw new BadRequestException('Cannot merge a person into themselves');
} }
await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); await this.access.requirePermission(auth, Permission.PERSON_UPDATE, 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;

View File

@ -0,0 +1,15 @@
import { Permission } from 'src/enum';
import { setIsSuperset } from 'src/utils/set';
export type GrantedRequest = {
requested: Permission[];
current: Permission[];
};
export const isGranted = ({ requested, current }: GrantedRequest) => {
if (current.includes(Permission.ALL)) {
return true;
}
return setIsSuperset(new Set(current), new Set(requested));
};

View File

@ -1,25 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { ApiKeyResponseDto } from '@immich/sdk';
import { mdiKeyVariant } from '@mdi/js'; import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher } from 'svelte'; import { t } from 'svelte-i18n';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { t } from 'svelte-i18n';
export let apiKey: Partial<ApiKeyResponseDto>; export let apiKey: { name: string };
export let title: string; export let title: string;
export let cancelText = $t('cancel'); export let cancelText = $t('cancel');
export let submitText = $t('save'); export let submitText = $t('save');
const dispatch = createEventDispatcher<{ export let onSubmit: (apiKey: { name: string }) => void;
cancel: void; export let onCancel: () => void;
submit: Partial<ApiKeyResponseDto>;
}>();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => { const handleSubmit = () => {
if (apiKey.name) { if (apiKey.name) {
dispatch('submit', apiKey); onSubmit({ name: apiKey.name });
} else { } else {
notificationController.show({ notificationController.show({
message: $t('api_key_empty'), message: $t('api_key_empty'),
@ -29,7 +25,7 @@
}; };
</script> </script>
<FullScreenModal {title} icon={mdiKeyVariant} onClose={handleCancel}> <FullScreenModal {title} icon={mdiKeyVariant} onClose={() => onCancel()}>
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="api-key-form"> <form on:submit|preventDefault={handleSubmit} autocomplete="off" id="api-key-form">
<div class="mb-4 flex flex-col gap-2"> <div class="mb-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label> <label class="immich-form-label" for="name">{$t('name')}</label>
@ -37,7 +33,7 @@
</div> </div>
</form> </form>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button> <Button color="gray" fullwidth on:click={() => onCancel()}>{cancelText}</Button>
<Button type="submit" fullwidth form="api-key-form">{submitText}</Button> <Button type="submit" fullwidth form="api-key-form">{submitText}</Button>
</svelte:fragment> </svelte:fragment>
</FullScreenModal> </FullScreenModal>

View File

@ -1,6 +1,13 @@
<script lang="ts"> <script lang="ts">
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { createApiKey, deleteApiKey, getApiKeys, updateApiKey, type ApiKeyResponseDto } from '@immich/sdk'; import {
createApiKey,
deleteApiKey,
getApiKeys,
Permission,
updateApiKey,
type ApiKeyResponseDto,
} from '@immich/sdk';
import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
@ -14,7 +21,7 @@
export let keys: ApiKeyResponseDto[]; export let keys: ApiKeyResponseDto[];
let newKey: Partial<ApiKeyResponseDto> | null = null; let newKey: { name: string } | null = null;
let editKey: ApiKeyResponseDto | null = null; let editKey: ApiKeyResponseDto | null = null;
let secret = ''; let secret = '';
@ -28,9 +35,14 @@
keys = await getApiKeys(); keys = await getApiKeys();
} }
const handleCreate = async (detail: Partial<ApiKeyResponseDto>) => { const handleCreate = async ({ name }: { name: string }) => {
try { try {
const data = await createApiKey({ apiKeyCreateDto: detail }); const data = await createApiKey({
apiKeyCreateDto: {
name,
permissions: [Permission.All],
},
});
secret = data.secret; secret = data.secret;
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_create_api_key')); handleError(error, $t('errors.unable_to_create_api_key'));
@ -84,8 +96,8 @@
title={$t('new_api_key')} title={$t('new_api_key')}
submitText={$t('create')} submitText={$t('create')}
apiKey={newKey} apiKey={newKey}
on:submit={({ detail }) => handleCreate(detail)} onSubmit={(key) => handleCreate(key)}
on:cancel={() => (newKey = null)} onCancel={() => (newKey = null)}
/> />
{/if} {/if}
@ -98,8 +110,8 @@
title={$t('api_key')} title={$t('api_key')}
submitText={$t('save')} submitText={$t('save')}
apiKey={editKey} apiKey={editKey}
on:submit={({ detail }) => handleUpdate(detail)} onSubmit={(key) => handleUpdate(key)}
on:cancel={() => (editKey = null)} onCancel={() => (editKey = null)}
/> />
{/if} {/if}