mirror of
https://github.com/immich-app/immich.git
synced 2025-01-21 00:52:43 -05:00
feat(server): granular permissions for api keys (#11824)
feat(server): api auth permissions
This commit is contained in:
parent
a372b56d44
commit
f230b3aa42
43 changed files with 817 additions and 135 deletions
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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. "
|
||||
|
|
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…
Add table
Reference in a new issue