mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:06:56 -04:00
Merge branch 'main' into main
This commit is contained in:
commit
5c927856b1
@ -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 upload -** whether to allow whoever has the link to upload assets to the album (optional).
|
||||
:::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).
|
||||
|
||||
## Share Specific Assets
|
||||
|
||||
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
|
||||
2. Select the assets (Shift can be used for multiple selection)
|
||||
|
@ -4,7 +4,7 @@ sidebar_position: 10
|
||||
|
||||
# Requirements
|
||||
|
||||
Hardware and software requirements for Immich
|
||||
Hardware and software requirements for Immich:
|
||||
|
||||
## Software
|
||||
|
||||
|
@ -45,7 +45,7 @@ width="70%"
|
||||
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.
|
||||
|
||||
<details >
|
||||
@ -130,7 +130,7 @@ For more information on how to use the application once installed, please refer
|
||||
|
||||
## 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
|
||||
src={require('./img/unraid09.png').default}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 1.5 MiB |
@ -1,12 +1,12 @@
|
||||
import { LoginResponseDto, createApiKey } from '@immich/sdk';
|
||||
import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const create = (accessToken: string) =>
|
||||
createApiKey({ apiKeyCreateDto: { name: 'api key' } }, { headers: asBearerAuth(accessToken) });
|
||||
const create = (accessToken: string, permissions: Permission[]) =>
|
||||
createApiKey({ apiKeyCreateDto: { name: 'api key', permissions } }, { headers: asBearerAuth(accessToken) });
|
||||
|
||||
describe('/api-keys', () => {
|
||||
let admin: LoginResponseDto;
|
||||
@ -30,15 +30,65 @@ describe('/api-keys', () => {
|
||||
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 () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/api-keys')
|
||||
.send({ name: 'API Key' })
|
||||
.send({ name: 'API Key', permissions: [Permission.All] })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual({
|
||||
apiKey: {
|
||||
id: expect.any(String),
|
||||
name: 'API Key',
|
||||
permissions: [Permission.All],
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
},
|
||||
@ -63,9 +113,9 @@ describe('/api-keys', () => {
|
||||
|
||||
it('should return a list of api keys', async () => {
|
||||
const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([
|
||||
create(admin.accessToken),
|
||||
create(admin.accessToken),
|
||||
create(admin.accessToken),
|
||||
create(admin.accessToken, [Permission.All]),
|
||||
create(admin.accessToken, [Permission.All]),
|
||||
create(admin.accessToken, [Permission.All]),
|
||||
]);
|
||||
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toHaveLength(3);
|
||||
@ -82,7 +132,7 @@ describe('/api-keys', () => {
|
||||
});
|
||||
|
||||
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)
|
||||
.get(`/api-keys/${apiKey.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
@ -99,7 +149,7 @@ describe('/api-keys', () => {
|
||||
});
|
||||
|
||||
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)
|
||||
.get(`/api-keys/${apiKey.id}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
@ -107,6 +157,7 @@ describe('/api-keys', () => {
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
name: 'api key',
|
||||
permissions: [Permission.All],
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
@ -121,7 +172,7 @@ describe('/api-keys', () => {
|
||||
});
|
||||
|
||||
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)
|
||||
.put(`/api-keys/${apiKey.id}`)
|
||||
.send({ name: 'new name' })
|
||||
@ -140,7 +191,7 @@ describe('/api-keys', () => {
|
||||
});
|
||||
|
||||
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)
|
||||
.put(`/api-keys/${apiKey.id}`)
|
||||
.send({ name: 'new name' })
|
||||
@ -149,6 +200,7 @@ describe('/api-keys', () => {
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
name: 'new name',
|
||||
permissions: [Permission.All],
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
@ -163,7 +215,7 @@ describe('/api-keys', () => {
|
||||
});
|
||||
|
||||
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)
|
||||
.delete(`/api-keys/${apiKey.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
@ -180,7 +232,7 @@ describe('/api-keys', () => {
|
||||
});
|
||||
|
||||
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)
|
||||
.delete(`/api-keys/${apiKey.id}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
@ -190,14 +242,14 @@ describe('/api-keys', () => {
|
||||
|
||||
describe('authentication', () => {
|
||||
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);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
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}`);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(status).toBe(200);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Permission } from '@immich/sdk';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { app, immichCli, utils } from 'src/utils';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
@ -29,7 +30,7 @@ describe(`immich login`, () => {
|
||||
|
||||
it('should login and save auth.yml with 600', async () => {
|
||||
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}`]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'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 () => {
|
||||
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}`]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'Logging in to http://127.0.0.1:2283',
|
||||
|
@ -13,6 +13,12 @@ export const errorDto = {
|
||||
message: expect.any(String),
|
||||
correlationId: expect.any(String),
|
||||
},
|
||||
missingPermission: (permission: string) => ({
|
||||
error: 'Forbidden',
|
||||
statusCode: 403,
|
||||
message: `Missing required permission: ${permission}`,
|
||||
correlationId: expect.any(String),
|
||||
}),
|
||||
wrongPassword: {
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
CreateAlbumDto,
|
||||
CreateLibraryDto,
|
||||
MetadataSearchDto,
|
||||
Permission,
|
||||
PersonCreateDto,
|
||||
SharedLinkCreateDto,
|
||||
UserAdminCreateDto,
|
||||
@ -279,8 +280,8 @@ export const utils = {
|
||||
});
|
||||
},
|
||||
|
||||
createApiKey: (accessToken: string) => {
|
||||
return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
|
||||
createApiKey: (accessToken: string, permissions: Permission[]) => {
|
||||
return createApiKey({ apiKeyCreateDto: { name: 'e2e', permissions } }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
|
||||
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
||||
@ -492,7 +493,7 @@ export const utils = {
|
||||
},
|
||||
|
||||
cliLogin: async (accessToken: string) => {
|
||||
const key = await utils.createApiKey(accessToken);
|
||||
const key = await utils.createApiKey(accessToken, [Permission.All]);
|
||||
await immichCli(['login', app, `${key.secret}`]);
|
||||
return key.secret;
|
||||
},
|
||||
|
@ -55,13 +55,13 @@
|
||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||
"asset_list_settings_title": "Photo Grid",
|
||||
"asset_restored_successfully": "Asset restored successfully",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"assets_deleted_permanently": "{} asset(s) deleted permanently",
|
||||
"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_restored_successfully": "{} asset(s) restored successfully",
|
||||
"assets_trashed": "{} asset(s) trashed",
|
||||
"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_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.",
|
||||
@ -173,6 +173,7 @@
|
||||
"control_bottom_app_bar_delete": "Delete",
|
||||
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||
"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_location": "Edit Location",
|
||||
"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_title": "Show background backup total progress",
|
||||
"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_title": "Looping",
|
||||
"setting_video_viewer_title": "Videos",
|
||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
||||
"share_add": "Add",
|
||||
"share_add_photos": "Add photos",
|
||||
"share_add_title": "Add a title",
|
||||
"share_assets_selected": "{} selected",
|
||||
"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_hint": "Say something",
|
||||
"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_owner_label": "Owner",
|
||||
"shared_album_section_people_title": "PEOPLE",
|
||||
"share_dialog_preparing": "Preparing...",
|
||||
"shared_link_app_bar_title": "Shared Links",
|
||||
"shared_link_clipboard_copied_massage": "Copied to clipboard",
|
||||
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||
@ -521,14 +524,12 @@
|
||||
"shared_link_info_chip_upload": "Upload",
|
||||
"shared_link_manage_links": "Manage Shared links",
|
||||
"shared_link_public_album": "Public album",
|
||||
"share_done": "Done",
|
||||
"share_invite": "Invite to album",
|
||||
"sharing_page_album": "Shared albums",
|
||||
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
||||
"sharing_page_empty_list": "EMPTY LIST",
|
||||
"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_shared_links": "Shared links",
|
||||
"tab_controller_nav_library": "Library",
|
||||
"tab_controller_nav_photos": "Photos",
|
||||
"tab_controller_nav_search": "Search",
|
||||
|
@ -51,8 +51,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void stopScreenDarkenTimer() {
|
||||
isScreenDarkened.value = false;
|
||||
darkenScreenTimer.value?.cancel();
|
||||
isScreenDarkened.value = false;
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: [
|
||||
@ -75,8 +75,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
.watch(websocketProvider.notifier)
|
||||
.stopListenToEvent('on_upload_success');
|
||||
|
||||
WakelockPlus.enable();
|
||||
|
||||
return () {
|
||||
WakelockPlus.disable();
|
||||
darkenScreenTimer.value?.cancel();
|
||||
@ -102,8 +100,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
() {
|
||||
if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
startScreenDarkenTimer();
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
stopScreenDarkenTimer();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -20,7 +20,7 @@ import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
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';
|
||||
|
||||
final backupServiceProvider = Provider(
|
||||
@ -213,7 +213,7 @@ class BackupService {
|
||||
_appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets);
|
||||
|
||||
if (Platform.isAndroid &&
|
||||
!(await Permission.accessMediaLocation.status).isGranted) {
|
||||
!(await pm.Permission.accessMediaLocation.status).isGranted) {
|
||||
// double check that permission is granted here, to guard against
|
||||
// uploading corrupt assets without EXIF information
|
||||
_log.warning("Media location permission is not granted. "
|
||||
|
@ -29,7 +29,8 @@ class LocalNotificationService {
|
||||
static const cancelUploadActionID = 'cancel_upload';
|
||||
|
||||
Future<void> setup() async {
|
||||
const androidSetting = AndroidInitializationSettings('notification_icon');
|
||||
const androidSetting =
|
||||
AndroidInitializationSettings('@drawable/notification_icon');
|
||||
const iosSetting = DarwinInitializationSettings();
|
||||
|
||||
const initSettings =
|
||||
|
@ -366,8 +366,8 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
{
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.download_outlined),
|
||||
label: 'download'.tr(),
|
||||
tooltip: 'download'.tr(),
|
||||
label: 'control_bottom_app_bar_download'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_download'.tr(),
|
||||
): (_) => handleDownload(),
|
||||
},
|
||||
if (isInAlbum)
|
||||
|
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -363,6 +363,7 @@ Class | Method | HTTP request | Description
|
||||
- [PeopleResponseDto](doc//PeopleResponseDto.md)
|
||||
- [PeopleUpdateDto](doc//PeopleUpdateDto.md)
|
||||
- [PeopleUpdateItem](doc//PeopleUpdateItem.md)
|
||||
- [Permission](doc//Permission.md)
|
||||
- [PersonCreateDto](doc//PersonCreateDto.md)
|
||||
- [PersonResponseDto](doc//PersonResponseDto.md)
|
||||
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
|
||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -175,6 +175,7 @@ part 'model/path_type.dart';
|
||||
part 'model/people_response_dto.dart';
|
||||
part 'model/people_update_dto.dart';
|
||||
part 'model/people_update_item.dart';
|
||||
part 'model/permission.dart';
|
||||
part 'model/person_create_dto.dart';
|
||||
part 'model/person_response_dto.dart';
|
||||
part 'model/person_statistics_response_dto.dart';
|
||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -407,6 +407,8 @@ class ApiClient {
|
||||
return PeopleUpdateDto.fromJson(value);
|
||||
case 'PeopleUpdateItem':
|
||||
return PeopleUpdateItem.fromJson(value);
|
||||
case 'Permission':
|
||||
return PermissionTypeTransformer().decode(value);
|
||||
case 'PersonCreateDto':
|
||||
return PersonCreateDto.fromJson(value);
|
||||
case 'PersonResponseDto':
|
||||
|
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@ -112,6 +112,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is PathType) {
|
||||
return PathTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is Permission) {
|
||||
return PermissionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ReactionLevel) {
|
||||
return ReactionLevelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
14
mobile/openapi/lib/model/api_key_create_dto.dart
generated
14
mobile/openapi/lib/model/api_key_create_dto.dart
generated
@ -14,6 +14,7 @@ class APIKeyCreateDto {
|
||||
/// Returns a new [APIKeyCreateDto] instance.
|
||||
APIKeyCreateDto({
|
||||
this.name,
|
||||
this.permissions = const [],
|
||||
});
|
||||
|
||||
///
|
||||
@ -24,17 +25,21 @@ class APIKeyCreateDto {
|
||||
///
|
||||
String? name;
|
||||
|
||||
List<Permission> permissions;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto &&
|
||||
other.name == name;
|
||||
other.name == name &&
|
||||
_deepEquality.equals(other.permissions, permissions);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(permissions.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'APIKeyCreateDto[name=$name]';
|
||||
String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -43,6 +48,7 @@ class APIKeyCreateDto {
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
json[r'permissions'] = this.permissions;
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -55,6 +61,7 @@ class APIKeyCreateDto {
|
||||
|
||||
return APIKeyCreateDto(
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
permissions: Permission.listFromJson(json[r'permissions']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@ -102,6 +109,7 @@ class APIKeyCreateDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'permissions',
|
||||
};
|
||||
}
|
||||
|
||||
|
10
mobile/openapi/lib/model/api_key_response_dto.dart
generated
10
mobile/openapi/lib/model/api_key_response_dto.dart
generated
@ -16,6 +16,7 @@ class APIKeyResponseDto {
|
||||
required this.createdAt,
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.permissions = const [],
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
@ -25,6 +26,8 @@ class APIKeyResponseDto {
|
||||
|
||||
String name;
|
||||
|
||||
List<Permission> permissions;
|
||||
|
||||
DateTime updatedAt;
|
||||
|
||||
@override
|
||||
@ -32,6 +35,7 @@ class APIKeyResponseDto {
|
||||
other.createdAt == createdAt &&
|
||||
other.id == id &&
|
||||
other.name == name &&
|
||||
_deepEquality.equals(other.permissions, permissions) &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@override
|
||||
@ -40,16 +44,18 @@ class APIKeyResponseDto {
|
||||
(createdAt.hashCode) +
|
||||
(id.hashCode) +
|
||||
(name.hashCode) +
|
||||
(permissions.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@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() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
json[r'id'] = this.id;
|
||||
json[r'name'] = this.name;
|
||||
json[r'permissions'] = this.permissions;
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
return json;
|
||||
}
|
||||
@ -65,6 +71,7 @@ class APIKeyResponseDto {
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
permissions: Permission.listFromJson(json[r'permissions']),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
);
|
||||
}
|
||||
@ -116,6 +123,7 @@ class APIKeyResponseDto {
|
||||
'createdAt',
|
||||
'id',
|
||||
'name',
|
||||
'permissions',
|
||||
'updatedAt',
|
||||
};
|
||||
}
|
||||
|
292
mobile/openapi/lib/model/permission.dart
generated
Normal file
292
mobile/openapi/lib/model/permission.dart
generated
Normal 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;
|
||||
}
|
||||
|
@ -7135,8 +7135,17 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Permission"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"APIKeyCreateResponseDto": {
|
||||
@ -7166,6 +7175,12 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Permission"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
@ -7175,6 +7190,7 @@
|
||||
"createdAt",
|
||||
"id",
|
||||
"name",
|
||||
"permissions",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
@ -9729,6 +9745,82 @@
|
||||
],
|
||||
"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": {
|
||||
"properties": {
|
||||
"birthDate": {
|
||||
|
@ -299,10 +299,12 @@ export type ApiKeyResponseDto = {
|
||||
createdAt: string;
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: Permission[];
|
||||
updatedAt: string;
|
||||
};
|
||||
export type ApiKeyCreateDto = {
|
||||
name?: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
export type ApiKeyCreateResponseDto = {
|
||||
apiKey: ApiKeyResponseDto;
|
||||
@ -3125,6 +3127,79 @@ export enum Error {
|
||||
NotFound = "not_found",
|
||||
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 {
|
||||
Created = "created",
|
||||
Replaced = "replaced",
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
ActivityStatisticsResponseDto,
|
||||
} from 'src/dtos/activity.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { ActivityService } from 'src/services/activity.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@ -19,19 +20,19 @@ export class ActivityController {
|
||||
constructor(private service: ActivityService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ACTIVITY_READ })
|
||||
getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
|
||||
return this.service.getAll(auth, dto);
|
||||
}
|
||||
|
||||
@Get('statistics')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ACTIVITY_STATISTICS })
|
||||
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
|
||||
return this.service.getStatistics(auth, dto);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ACTIVITY_CREATE })
|
||||
async createActivity(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: ActivityCreateDto,
|
||||
@ -46,7 +47,7 @@ export class ActivityController {
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ACTIVITY_DELETE })
|
||||
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from 'src/dtos/album.dto';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
|
||||
@ -22,24 +23,24 @@ export class AlbumController {
|
||||
constructor(private service: AlbumService) {}
|
||||
|
||||
@Get('count')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ALBUM_STATISTICS })
|
||||
getAlbumCount(@Auth() auth: AuthDto): Promise<AlbumCountResponseDto> {
|
||||
return this.service.getCount(auth);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ALBUM_READ })
|
||||
getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
||||
return this.service.getAll(auth, query);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ALBUM_CREATE })
|
||||
createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true })
|
||||
@Get(':id')
|
||||
getAlbumInfo(
|
||||
@Auth() auth: AuthDto,
|
||||
@ -50,7 +51,7 @@ export class AlbumController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ALBUM_UPDATE })
|
||||
updateAlbumInfo(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -60,7 +61,7 @@ export class AlbumController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ALBUM_DELETE })
|
||||
deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put }
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { APIKeyService } from 'src/services/api-key.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@ -12,25 +13,25 @@ export class APIKeyController {
|
||||
constructor(private service: APIKeyService) {}
|
||||
|
||||
@Post()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.API_KEY_CREATE })
|
||||
createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.API_KEY_READ })
|
||||
getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> {
|
||||
return this.service.getAll(auth);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.API_KEY_READ })
|
||||
getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
|
||||
return this.service.getById(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.API_KEY_UPDATE })
|
||||
updateApiKey(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -41,7 +42,7 @@ export class APIKeyController {
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.API_KEY_DELETE })
|
||||
deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@ -12,13 +13,13 @@ export class FaceController {
|
||||
constructor(private service: PersonService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.FACE_READ })
|
||||
getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
return this.service.getFacesById(auth, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.FACE_UPDATE })
|
||||
reassignFacesById(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
ValidateLibraryDto,
|
||||
ValidateLibraryResponseDto,
|
||||
} from 'src/dtos/library.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@ -19,25 +20,25 @@ export class LibraryController {
|
||||
constructor(private service: LibraryService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.LIBRARY_READ, admin: true })
|
||||
getAllLibraries(): Promise<LibraryResponseDto[]> {
|
||||
return this.service.getAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.LIBRARY_CREATE, admin: true })
|
||||
createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true })
|
||||
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
||||
return this.service.update(id, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.LIBRARY_READ, admin: true })
|
||||
getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> {
|
||||
return this.service.get(id);
|
||||
}
|
||||
@ -52,13 +53,13 @@ export class LibraryController {
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true })
|
||||
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(id);
|
||||
}
|
||||
|
||||
@Get(':id/statistics')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true })
|
||||
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
|
||||
return this.service.getStatistics(id);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@ -13,25 +14,25 @@ export class MemoryController {
|
||||
constructor(private service: MemoryService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.MEMORY_READ })
|
||||
searchMemories(@Auth() auth: AuthDto): Promise<MemoryResponseDto[]> {
|
||||
return this.service.search(auth);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.MEMORY_CREATE })
|
||||
createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.MEMORY_READ })
|
||||
getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.MEMORY_UPDATE })
|
||||
updateMemory(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -42,7 +43,7 @@ export class MemoryController {
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.MEMORY_DELETE })
|
||||
deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/
|
||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
@ -14,20 +15,20 @@ export class PartnerController {
|
||||
|
||||
@Get()
|
||||
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PARTNER_READ })
|
||||
// TODO: remove 'direction' and convert to full query dto
|
||||
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Post(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PARTNER_CREATE })
|
||||
createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
|
||||
return this.service.create(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PARTNER_UPDATE })
|
||||
updatePartner(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -37,7 +38,7 @@ export class PartnerController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PARTNER_DELETE })
|
||||
removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
PersonStatisticsResponseDto,
|
||||
PersonUpdateDto,
|
||||
} from 'src/dtos/person.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
@ -31,31 +32,31 @@ export class PersonController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_READ })
|
||||
getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
|
||||
return this.service.getAll(auth, withHidden);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_CREATE })
|
||||
createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Put()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_UPDATE })
|
||||
updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.updateAll(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_READ })
|
||||
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
|
||||
return this.service.getById(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_UPDATE })
|
||||
updatePerson(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -65,14 +66,14 @@ export class PersonController {
|
||||
}
|
||||
|
||||
@Get(':id/statistics')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_STATISTICS })
|
||||
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
|
||||
return this.service.getStatistics(auth, id);
|
||||
}
|
||||
|
||||
@Get(':id/thumbnail')
|
||||
@FileResponse()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_READ })
|
||||
async getPersonThumbnail(
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
@ -90,7 +91,7 @@ export class PersonController {
|
||||
}
|
||||
|
||||
@Put(':id/reassign')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_REASSIGN })
|
||||
reassignFaces(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -100,7 +101,7 @@ export class PersonController {
|
||||
}
|
||||
|
||||
@Post(':id/merge')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.PERSON_MERGE })
|
||||
mergePerson(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
SharedLinkPasswordDto,
|
||||
SharedLinkResponseDto,
|
||||
} from 'src/dtos/shared-link.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
@ -22,7 +23,7 @@ export class SharedLinkController {
|
||||
constructor(private service: SharedLinkService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.SHARED_LINK_READ })
|
||||
getAllSharedLinks(@Auth() auth: AuthDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.service.getAll(auth);
|
||||
}
|
||||
@ -48,19 +49,19 @@ export class SharedLinkController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.SHARED_LINK_READ })
|
||||
getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.SHARED_LINK_CREATE })
|
||||
createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.SHARED_LINK_UPDATE })
|
||||
updateSharedLink(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -70,7 +71,7 @@ export class SharedLinkController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.SHARED_LINK_DELETE })
|
||||
removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Get, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { SystemConfigService } from 'src/services/system-config.service';
|
||||
|
||||
@ -10,25 +11,25 @@ export class SystemConfigController {
|
||||
constructor(private service: SystemConfigService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
||||
getConfig(): Promise<SystemConfigDto> {
|
||||
return this.service.getConfig();
|
||||
}
|
||||
|
||||
@Get('defaults')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
||||
getConfigDefaults(): SystemConfigDto {
|
||||
return this.service.getDefaults();
|
||||
}
|
||||
|
||||
@Put()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true })
|
||||
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
|
||||
return this.service.updateConfig(dto);
|
||||
}
|
||||
|
||||
@Get('storage-template-options')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
||||
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
||||
return this.service.getStorageTemplateOptions();
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||
|
||||
@ -10,20 +11,20 @@ export class SystemMetadataController {
|
||||
constructor(private service: SystemMetadataService) {}
|
||||
|
||||
@Get('admin-onboarding')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true })
|
||||
getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
|
||||
return this.service.getAdminOnboarding();
|
||||
}
|
||||
|
||||
@Post('admin-onboarding')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.SYSTEM_METADATA_UPDATE, admin: true })
|
||||
updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
|
||||
return this.service.updateAdminOnboarding(dto);
|
||||
}
|
||||
|
||||
@Get('reverse-geocoding-state')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true })
|
||||
getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
|
||||
return this.service.getReverseGeocodingState();
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { TagService } from 'src/services/tag.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@ -15,31 +16,31 @@ export class TagController {
|
||||
constructor(private service: TagService) {}
|
||||
|
||||
@Post()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.TAG_CREATE })
|
||||
createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.TAG_READ })
|
||||
getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> {
|
||||
return this.service.getAll(auth);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.TAG_READ })
|
||||
getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
|
||||
return this.service.getById(auth, id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.TAG_UPDATE })
|
||||
updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise<TagResponseDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.TAG_DELETE })
|
||||
deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
UserAdminSearchDto,
|
||||
UserAdminUpdateDto,
|
||||
} from 'src/dtos/user.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { UserAdminService } from 'src/services/user-admin.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
@ -19,25 +20,25 @@ export class UserAdminController {
|
||||
constructor(private service: UserAdminService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
|
||||
searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_CREATE, admin: true })
|
||||
createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.create(createUserDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
|
||||
getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true })
|
||||
updateUserAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -47,7 +48,7 @@ export class UserAdminController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true })
|
||||
deleteUserAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -57,13 +58,13 @@ export class UserAdminController {
|
||||
}
|
||||
|
||||
@Get(':id/preferences')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
|
||||
getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> {
|
||||
return this.service.getPreferences(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/preferences')
|
||||
@Authenticated()
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true })
|
||||
updateUserPreferencesAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@ -73,7 +74,7 @@ export class UserAdminController {
|
||||
}
|
||||
|
||||
@Post(':id/restore')
|
||||
@Authenticated({ admin: true })
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true })
|
||||
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.restore(auth, id);
|
||||
}
|
||||
|
@ -256,7 +256,7 @@ export class AccessCore {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -272,7 +272,7 @@ export class AccessCore {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
export class APIKeyCreateDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
name?: string;
|
||||
|
||||
@IsEnum(Permission, { each: true })
|
||||
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
|
||||
@ArrayMinSize(1)
|
||||
permissions!: Permission[];
|
||||
}
|
||||
|
||||
export class APIKeyUpdateDto {
|
||||
@ -23,4 +30,6 @@ export class APIKeyResponseDto {
|
||||
name!: string;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
|
||||
permissions!: Permission[];
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('api_keys')
|
||||
@ -18,6 +19,9 @@ export class APIKeyEntity {
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@Column({ array: true, type: 'varchar' })
|
||||
permissions!: Permission[];
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
@ -32,8 +32,18 @@ export enum MemoryType {
|
||||
}
|
||||
|
||||
export enum Permission {
|
||||
ALL = 'all',
|
||||
|
||||
ACTIVITY_CREATE = 'activity.create',
|
||||
ACTIVITY_READ = 'activity.read',
|
||||
ACTIVITY_UPDATE = 'activity.update',
|
||||
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_READ = 'asset.read',
|
||||
@ -45,10 +55,12 @@ export enum Permission {
|
||||
ASSET_DOWNLOAD = 'asset.download',
|
||||
ASSET_UPLOAD = 'asset.upload',
|
||||
|
||||
// ALBUM_CREATE = 'album.create',
|
||||
ALBUM_CREATE = 'album.create',
|
||||
ALBUM_READ = 'album.read',
|
||||
ALBUM_UPDATE = 'album.update',
|
||||
ALBUM_DELETE = 'album.delete',
|
||||
ALBUM_STATISTICS = 'album.statistics',
|
||||
|
||||
ALBUM_ADD_ASSET = 'album.addAsset',
|
||||
ALBUM_REMOVE_ASSET = 'album.removeAsset',
|
||||
ALBUM_SHARE = 'album.share',
|
||||
@ -58,20 +70,58 @@ export enum Permission {
|
||||
|
||||
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_DOWNLOAD = 'timeline.download',
|
||||
|
||||
MEMORY_CREATE = 'memory.create',
|
||||
MEMORY_READ = 'memory.read',
|
||||
MEMORY_WRITE = 'memory.write',
|
||||
MEMORY_UPDATE = 'memory.update',
|
||||
MEMORY_DELETE = 'memory.delete',
|
||||
|
||||
PERSON_READ = 'person.read',
|
||||
PERSON_WRITE = 'person.write',
|
||||
PERSON_MERGE = 'person.merge',
|
||||
PARTNER_CREATE = 'partner.create',
|
||||
PARTNER_READ = 'partner.read',
|
||||
PARTNER_UPDATE = 'partner.update',
|
||||
PARTNER_DELETE = 'partner.delete',
|
||||
|
||||
PERSON_CREATE = 'person.create',
|
||||
PERSON_READ = 'person.read',
|
||||
PERSON_UPDATE = 'person.update',
|
||||
PERSON_DELETE = 'person.delete',
|
||||
PERSON_STATISTICS = 'person.statistics',
|
||||
PERSON_MERGE = 'person.merge',
|
||||
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 {
|
||||
|
@ -11,6 +11,7 @@ import { Reflector } from '@nestjs/core';
|
||||
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
@ -25,7 +26,7 @@ export enum Metadata {
|
||||
|
||||
type AdminRoute = { admin?: true };
|
||||
type SharedLinkRoute = { sharedLink?: true };
|
||||
type AuthenticatedOptions = AdminRoute | SharedLinkRoute;
|
||||
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
|
||||
|
||||
export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => {
|
||||
const decorators: MethodDecorator[] = [
|
||||
@ -89,13 +90,17 @@ export class AuthGuard implements CanActivate {
|
||||
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>();
|
||||
|
||||
request.user = await this.authService.authenticate({
|
||||
headers: request.headers,
|
||||
queryParams: request.query as Record<string, string>,
|
||||
metadata: { adminRoute, sharedLinkRoute, uri: request.path },
|
||||
metadata: { adminRoute, sharedLinkRoute, permission, uri: request.path },
|
||||
});
|
||||
|
||||
return true;
|
||||
|
14
server/src/migrations/1723719333525-AddApiKeyPermissions.ts
Normal file
14
server/src/migrations/1723719333525-AddApiKeyPermissions.ts
Normal 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"`);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ FROM
|
||||
"APIKeyEntity"."id" AS "APIKeyEntity_id",
|
||||
"APIKeyEntity"."key" AS "APIKeyEntity_key",
|
||||
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
|
||||
"APIKeyEntity"."permissions" AS "APIKeyEntity_permissions",
|
||||
"APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id",
|
||||
"APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name",
|
||||
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
|
||||
@ -46,6 +47,7 @@ SELECT
|
||||
"APIKeyEntity"."id" AS "APIKeyEntity_id",
|
||||
"APIKeyEntity"."name" AS "APIKeyEntity_name",
|
||||
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
|
||||
"APIKeyEntity"."permissions" AS "APIKeyEntity_permissions",
|
||||
"APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt",
|
||||
"APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt"
|
||||
FROM
|
||||
@ -63,6 +65,7 @@ SELECT
|
||||
"APIKeyEntity"."id" AS "APIKeyEntity_id",
|
||||
"APIKeyEntity"."name" AS "APIKeyEntity_name",
|
||||
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
|
||||
"APIKeyEntity"."permissions" AS "APIKeyEntity_permissions",
|
||||
"APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt",
|
||||
"APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt"
|
||||
FROM
|
||||
|
@ -31,6 +31,7 @@ export class ApiKeyRepository implements IKeyRepository {
|
||||
id: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
permissions: true,
|
||||
},
|
||||
where: { key: hashedToken },
|
||||
relations: {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Permission } from 'src/enum';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { APIKeyService } from 'src/services/api-key.service';
|
||||
@ -22,10 +23,11 @@ describe(APIKeyService.name, () => {
|
||||
describe('create', () => {
|
||||
it('should create a new key', async () => {
|
||||
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({
|
||||
key: 'cmFuZG9tLWJ5dGVz (hashed)',
|
||||
name: 'Test Key',
|
||||
permissions: [Permission.ALL],
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
expect(cryptoMock.newPassword).toHaveBeenCalled();
|
||||
@ -35,11 +37,12 @@ describe(APIKeyService.name, () => {
|
||||
it('should not require a name', async () => {
|
||||
keyMock.create.mockResolvedValue(keyStub.admin);
|
||||
|
||||
await sut.create(authStub.admin, {});
|
||||
await sut.create(authStub.admin, { permissions: [Permission.ALL] });
|
||||
|
||||
expect(keyMock.create).toHaveBeenCalledWith({
|
||||
key: 'cmFuZG9tLWJ5dGVz (hashed)',
|
||||
name: 'API Key',
|
||||
permissions: [Permission.ALL],
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
expect(cryptoMock.newPassword).toHaveBeenCalled();
|
||||
|
@ -1,9 +1,10 @@
|
||||
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 { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class APIKeyService {
|
||||
@ -14,16 +15,22 @@ export class APIKeyService {
|
||||
|
||||
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
|
||||
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({
|
||||
key: this.crypto.hashSha256(secret),
|
||||
name: dto.name || 'API Key',
|
||||
userId: auth.user.id,
|
||||
permissions: dto.permissions,
|
||||
});
|
||||
|
||||
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);
|
||||
if (!exists) {
|
||||
throw new BadRequestException('API Key not found');
|
||||
@ -62,6 +69,7 @@ export class APIKeyService {
|
||||
name: entity.name,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
permissions: entity.permissions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
} from 'src/dtos/auth.dto';
|
||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
|
||||
export interface LoginDetails {
|
||||
@ -61,6 +63,7 @@ export type ValidateRequest = {
|
||||
metadata: {
|
||||
sharedLinkRoute: boolean;
|
||||
adminRoute: boolean;
|
||||
permission?: Permission;
|
||||
uri: string;
|
||||
};
|
||||
};
|
||||
@ -157,7 +160,7 @@ export class AuthService {
|
||||
|
||||
async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise<AuthDto> {
|
||||
const authDto = await this.validate({ headers, queryParams });
|
||||
const { adminRoute, sharedLinkRoute, uri } = metadata;
|
||||
const { adminRoute, sharedLinkRoute, permission, uri } = metadata;
|
||||
|
||||
if (!authDto.user.isAdmin && adminRoute) {
|
||||
this.logger.warn(`Denied access to admin only route: ${uri}`);
|
||||
@ -169,6 +172,10 @@ export class AuthService {
|
||||
throw new ForbiddenException('Forbidden');
|
||||
}
|
||||
|
||||
if (authDto.apiKey && permission && !isGranted({ requested: [permission], current: authDto.apiKey.permissions })) {
|
||||
throw new ForbiddenException(`Missing required permission: ${permission}`);
|
||||
}
|
||||
|
||||
return authDto;
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ export class MemoryService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id);
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id);
|
||||
|
||||
const memory = await this.repository.update({
|
||||
id,
|
||||
@ -82,7 +82,7 @@ export class MemoryService {
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id);
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id);
|
||||
|
||||
const repos = { accessRepository: this.accessRepository, repository: this.repository };
|
||||
const results = await removeAssets(auth, repos, {
|
||||
|
@ -113,7 +113,7 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
|
||||
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId);
|
||||
const person = await this.findOrFail(personId);
|
||||
const result: PersonResponseDto[] = [];
|
||||
const changeFeaturePhoto: string[] = [];
|
||||
@ -142,7 +142,7 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
|
||||
await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId);
|
||||
|
||||
await this.access.requirePermission(auth, Permission.PERSON_CREATE, 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> {
|
||||
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;
|
||||
// TODO: set by faceId directly
|
||||
@ -581,7 +581,7 @@ export class PersonService {
|
||||
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);
|
||||
const primaryName = primaryPerson.name || primaryPerson.id;
|
||||
|
||||
|
15
server/src/utils/access.ts
Normal file
15
server/src/utils/access.ts
Normal 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));
|
||||
};
|
@ -1,25 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { ApiKeyResponseDto } from '@immich/sdk';
|
||||
import { mdiKeyVariant } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
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 cancelText = $t('cancel');
|
||||
export let submitText = $t('save');
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
cancel: void;
|
||||
submit: Partial<ApiKeyResponseDto>;
|
||||
}>();
|
||||
const handleCancel = () => dispatch('cancel');
|
||||
export let onSubmit: (apiKey: { name: string }) => void;
|
||||
export let onCancel: () => void;
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (apiKey.name) {
|
||||
dispatch('submit', apiKey);
|
||||
onSubmit({ name: apiKey.name });
|
||||
} else {
|
||||
notificationController.show({
|
||||
message: $t('api_key_empty'),
|
||||
@ -29,7 +25,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal {title} icon={mdiKeyVariant} onClose={handleCancel}>
|
||||
<FullScreenModal {title} icon={mdiKeyVariant} onClose={() => onCancel()}>
|
||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="api-key-form">
|
||||
<div class="mb-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||
@ -37,7 +33,7 @@
|
||||
</div>
|
||||
</form>
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
|
@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
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 { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
@ -14,7 +21,7 @@
|
||||
|
||||
export let keys: ApiKeyResponseDto[];
|
||||
|
||||
let newKey: Partial<ApiKeyResponseDto> | null = null;
|
||||
let newKey: { name: string } | null = null;
|
||||
let editKey: ApiKeyResponseDto | null = null;
|
||||
let secret = '';
|
||||
|
||||
@ -28,9 +35,14 @@
|
||||
keys = await getApiKeys();
|
||||
}
|
||||
|
||||
const handleCreate = async (detail: Partial<ApiKeyResponseDto>) => {
|
||||
const handleCreate = async ({ name }: { name: string }) => {
|
||||
try {
|
||||
const data = await createApiKey({ apiKeyCreateDto: detail });
|
||||
const data = await createApiKey({
|
||||
apiKeyCreateDto: {
|
||||
name,
|
||||
permissions: [Permission.All],
|
||||
},
|
||||
});
|
||||
secret = data.secret;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_create_api_key'));
|
||||
@ -84,8 +96,8 @@
|
||||
title={$t('new_api_key')}
|
||||
submitText={$t('create')}
|
||||
apiKey={newKey}
|
||||
on:submit={({ detail }) => handleCreate(detail)}
|
||||
on:cancel={() => (newKey = null)}
|
||||
onSubmit={(key) => handleCreate(key)}
|
||||
onCancel={() => (newKey = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@ -98,8 +110,8 @@
|
||||
title={$t('api_key')}
|
||||
submitText={$t('save')}
|
||||
apiKey={editKey}
|
||||
on:submit={({ detail }) => handleUpdate(detail)}
|
||||
on:cancel={() => (editKey = null)}
|
||||
onSubmit={(key) => handleUpdate(key)}
|
||||
onCancel={() => (editKey = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user