mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 09:59:00 -07:00
feat(server): user and server license endpoints (#10682)
* feat: user license endpoints * feat: server license endpoints * chore: pr feedback * chore: add more test cases * chore: add prod license public keys * chore: open-api generation
This commit is contained in:
parent
4193b0dede
commit
3b37b70626
@ -22,6 +22,7 @@ services:
|
||||
- DB_DATABASE_NAME=immich
|
||||
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
||||
- IMMICH_METRICS=true
|
||||
- IMMICH_ENV=testing
|
||||
volumes:
|
||||
- upload:/usr/src/app/upload
|
||||
- ./test-assets:/test-assets
|
||||
|
@ -44,6 +44,7 @@ describe('/server-info', () => {
|
||||
imagemagick: expect.any(String),
|
||||
libvips: expect.any(String),
|
||||
exiftool: expect.any(String),
|
||||
licensed: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,12 @@ import { app, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const serverLicense = {
|
||||
licenseKey: 'IMSV-6ECZ-91TE-WZRM-Q7AQ-MBN4-UW48-2CPT-71X9',
|
||||
activationKey:
|
||||
'4kJUNUWMq13J14zqPFm1NodRcI6MV6DeOGvQNIgrM8Sc9nv669wyEVvFw1Nz4Kb1W7zLWblOtXEQzpRRqC4r4fKjewJxfbpeo9sEsqAVIfl4Ero-Vp1Dg21-sVdDGZEAy2oeTCXAyCT5d1JqrqR6N1qTAm4xOx9ujXQRFYhjRG8uwudw7_Q49pF18Tj5OEv9qCqElxztoNck4i6O_azsmsoOQrLIENIWPh3EynBN3ESpYERdCgXO8MlWeuG14_V1HbNjnJPZDuvYg__YfMzoOEtfm1sCqEaJ2Ww-BaX7yGfuCL4XsuZlCQQNHjfscy_WywVfIZPKCiW8QR74i0cSzQ',
|
||||
};
|
||||
|
||||
describe('/server', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
@ -44,6 +50,7 @@ describe('/server', () => {
|
||||
imagemagick: expect.any(String),
|
||||
libvips: expect.any(String),
|
||||
exiftool: expect.any(String),
|
||||
licensed: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -197,4 +204,104 @@ describe('/server', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /server/license', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/server/license');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should only work for admins', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/server/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should return the server license', async () => {
|
||||
await request(app).put('/server/license').set('Authorization', `Bearer ${admin.accessToken}`).send(serverLicense);
|
||||
const { status, body } = await request(app)
|
||||
.get('/server/license')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...serverLicense,
|
||||
activatedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /server/license', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete('/server/license');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should only work for admins', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete('/server/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should delete the server license', async () => {
|
||||
await request(app)
|
||||
.delete('/server/license')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send(serverLicense);
|
||||
const { status } = await request(app).get('/server/license').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /server/license', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put('/server/license');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should only work for admins', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put('/server/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should set the server license', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put('/server/license')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send(serverLicense);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ ...serverLicense, activatedAt: expect.any(String) });
|
||||
const { body: licenseBody } = await request(app)
|
||||
.get('/server/license')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(licenseBody).toEqual({ ...serverLicense, activatedAt: expect.any(String) });
|
||||
});
|
||||
|
||||
it('should reject license not starting with IMSV-', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put('/server/license')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ licenseKey: 'IMCL-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', activationKey: 'activationKey' });
|
||||
expect(status).toBe(400);
|
||||
expect(body.message).toBe('Invalid license key');
|
||||
});
|
||||
|
||||
it('should reject license with invalid activation key', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put('/server/license')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ licenseKey: serverLicense.licenseKey, activationKey: `invalid${serverLicense.activationKey}` });
|
||||
expect(status).toBe(400);
|
||||
expect(body.message).toBe('Invalid license key');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,12 @@ import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const userLicense = {
|
||||
licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4',
|
||||
activationKey:
|
||||
'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw',
|
||||
};
|
||||
|
||||
describe('/users', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let deletedUser: LoginResponseDto;
|
||||
@ -72,6 +78,24 @@ describe('/users', () => {
|
||||
quotaUsageInBytes: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get my user with license info', async () => {
|
||||
const { status: licenseStatus } = await request(app)
|
||||
.put(`/users/me/license`)
|
||||
.send(userLicense)
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(licenseStatus).toBe(200);
|
||||
const { status, body } = await request(app)
|
||||
.get(`/users/me`)
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
id: nonAdmin.userId,
|
||||
email: nonAdmin.userEmail,
|
||||
quotaUsageInBytes: 0,
|
||||
license: userLicense,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /users/me', () => {
|
||||
@ -236,4 +260,81 @@ describe('/users', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /server/license', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/users/me/license');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should return the user license', async () => {
|
||||
await request(app)
|
||||
.put('/users/me/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
|
||||
.send(userLicense);
|
||||
const { status, body } = await request(app)
|
||||
.get('/users/me/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...userLicense,
|
||||
activatedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /users/me/license', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status } = await request(app).put(`/users/me/license`);
|
||||
expect(status).toEqual(401);
|
||||
});
|
||||
|
||||
it('should set the user license', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/users/me/license`)
|
||||
.send(userLicense)
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ ...userLicense, activatedAt: expect.any(String) });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ ...userLicense, activatedAt: expect.any(String) });
|
||||
const { body: licenseBody } = await request(app)
|
||||
.get('/users/me/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(licenseBody).toEqual({ ...userLicense, activatedAt: expect.any(String) });
|
||||
});
|
||||
|
||||
it('should reject license not starting with IMCL-', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put('/users/me/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
|
||||
.send({ licenseKey: 'IMSV-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', activationKey: 'activationKey' });
|
||||
expect(status).toBe(400);
|
||||
expect(body.message).toBe('Invalid license key');
|
||||
});
|
||||
|
||||
it('should reject license with invalid activation key', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put('/users/me/license')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
|
||||
.send({ licenseKey: userLicense.licenseKey, activationKey: `invalid${userLicense.activationKey}` });
|
||||
expect(status).toBe(400);
|
||||
expect(body.message).toBe('Invalid license key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /users/me/license', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status } = await request(app).put(`/users/me/license`);
|
||||
expect(status).toEqual(401);
|
||||
});
|
||||
|
||||
it('should delete the user license', async () => {
|
||||
const { status } = await request(app)
|
||||
.delete(`/users/me/license`)
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -81,6 +81,7 @@ export const signupResponseDto = {
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: null,
|
||||
status: 'active',
|
||||
license: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
9
mobile/openapi/README.md
generated
9
mobile/openapi/README.md
generated
@ -180,6 +180,9 @@ Class | Method | HTTP request | Description
|
||||
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
||||
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
||||
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||
*ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license |
|
||||
*ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license |
|
||||
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
|
||||
*ServerInfoApi* | [**getAboutInfo**](doc//ServerInfoApi.md#getaboutinfo) | **GET** /server-info/about |
|
||||
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
|
||||
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
|
||||
@ -224,11 +227,14 @@ Class | Method | HTTP request | Description
|
||||
*TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore |
|
||||
*UsersApi* | [**createProfileImage**](doc//UsersApi.md#createprofileimage) | **POST** /users/profile-image |
|
||||
*UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /users/profile-image |
|
||||
*UsersApi* | [**deleteUserLicense**](doc//UsersApi.md#deleteuserlicense) | **DELETE** /users/me/license |
|
||||
*UsersApi* | [**getMyPreferences**](doc//UsersApi.md#getmypreferences) | **GET** /users/me/preferences |
|
||||
*UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me |
|
||||
*UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image |
|
||||
*UsersApi* | [**getUser**](doc//UsersApi.md#getuser) | **GET** /users/{id} |
|
||||
*UsersApi* | [**getUserLicense**](doc//UsersApi.md#getuserlicense) | **GET** /users/me/license |
|
||||
*UsersApi* | [**searchUsers**](doc//UsersApi.md#searchusers) | **GET** /users |
|
||||
*UsersApi* | [**setUserLicense**](doc//UsersApi.md#setuserlicense) | **PUT** /users/me/license |
|
||||
*UsersApi* | [**updateMyPreferences**](doc//UsersApi.md#updatemypreferences) | **PUT** /users/me/preferences |
|
||||
*UsersApi* | [**updateMyUser**](doc//UsersApi.md#updatemyuser) | **PUT** /users/me |
|
||||
*UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users |
|
||||
@ -326,6 +332,8 @@ Class | Method | HTTP request | Description
|
||||
- [JobStatusDto](doc//JobStatusDto.md)
|
||||
- [LibraryResponseDto](doc//LibraryResponseDto.md)
|
||||
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
|
||||
- [LicenseKeyDto](doc//LicenseKeyDto.md)
|
||||
- [LicenseResponseDto](doc//LicenseResponseDto.md)
|
||||
- [LogLevel](doc//LogLevel.md)
|
||||
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
||||
- [LoginResponseDto](doc//LoginResponseDto.md)
|
||||
@ -430,6 +438,7 @@ Class | Method | HTTP request | Description
|
||||
- [UserAdminResponseDto](doc//UserAdminResponseDto.md)
|
||||
- [UserAdminUpdateDto](doc//UserAdminUpdateDto.md)
|
||||
- [UserAvatarColor](doc//UserAvatarColor.md)
|
||||
- [UserLicense](doc//UserLicense.md)
|
||||
- [UserPreferencesResponseDto](doc//UserPreferencesResponseDto.md)
|
||||
- [UserPreferencesUpdateDto](doc//UserPreferencesUpdateDto.md)
|
||||
- [UserResponseDto](doc//UserResponseDto.md)
|
||||
|
4
mobile/openapi/lib/api.dart
generated
4
mobile/openapi/lib/api.dart
generated
@ -49,6 +49,7 @@ part 'api/o_auth_api.dart';
|
||||
part 'api/partners_api.dart';
|
||||
part 'api/people_api.dart';
|
||||
part 'api/search_api.dart';
|
||||
part 'api/server_api.dart';
|
||||
part 'api/server_info_api.dart';
|
||||
part 'api/sessions_api.dart';
|
||||
part 'api/shared_links_api.dart';
|
||||
@ -144,6 +145,8 @@ part 'model/job_settings_dto.dart';
|
||||
part 'model/job_status_dto.dart';
|
||||
part 'model/library_response_dto.dart';
|
||||
part 'model/library_stats_response_dto.dart';
|
||||
part 'model/license_key_dto.dart';
|
||||
part 'model/license_response_dto.dart';
|
||||
part 'model/log_level.dart';
|
||||
part 'model/login_credential_dto.dart';
|
||||
part 'model/login_response_dto.dart';
|
||||
@ -248,6 +251,7 @@ part 'model/user_admin_delete_dto.dart';
|
||||
part 'model/user_admin_response_dto.dart';
|
||||
part 'model/user_admin_update_dto.dart';
|
||||
part 'model/user_avatar_color.dart';
|
||||
part 'model/user_license.dart';
|
||||
part 'model/user_preferences_response_dto.dart';
|
||||
part 'model/user_preferences_update_dto.dart';
|
||||
part 'model/user_response_dto.dart';
|
||||
|
139
mobile/openapi/lib/api/server_api.dart
generated
Normal file
139
mobile/openapi/lib/api/server_api.dart
generated
Normal file
@ -0,0 +1,139 @@
|
||||
//
|
||||
// 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 ServerApi {
|
||||
ServerApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'DELETE /server/license' operation and returns the [Response].
|
||||
Future<Response> deleteServerLicenseWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/server/license';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteServerLicense() async {
|
||||
final response = await deleteServerLicenseWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /server/license' operation and returns the [Response].
|
||||
Future<Response> getServerLicenseWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/server/license';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Object?> getServerLicense() async {
|
||||
final response = await getServerLicenseWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /server/license' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [LicenseKeyDto] licenseKeyDto (required):
|
||||
Future<Response> setServerLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/server/license';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = licenseKeyDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [LicenseKeyDto] licenseKeyDto (required):
|
||||
Future<LicenseResponseDto?> setServerLicense(LicenseKeyDto licenseKeyDto,) async {
|
||||
final response = await setServerLicenseWithHttpInfo(licenseKeyDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
121
mobile/openapi/lib/api/users_api.dart
generated
121
mobile/openapi/lib/api/users_api.dart
generated
@ -106,6 +106,39 @@ class UsersApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /users/me/license' operation and returns the [Response].
|
||||
Future<Response> deleteUserLicenseWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/users/me/license';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteUserLicense() async {
|
||||
final response = await deleteUserLicenseWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /users/me/preferences' operation and returns the [Response].
|
||||
Future<Response> getMyPreferencesWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
@ -284,6 +317,47 @@ class UsersApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /users/me/license' operation and returns the [Response].
|
||||
Future<Response> getUserLicenseWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/users/me/license';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<LicenseResponseDto?> getUserLicense() async {
|
||||
final response = await getUserLicenseWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /users' operation and returns the [Response].
|
||||
Future<Response> searchUsersWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
@ -328,6 +402,53 @@ class UsersApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /users/me/license' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [LicenseKeyDto] licenseKeyDto (required):
|
||||
Future<Response> setUserLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/users/me/license';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = licenseKeyDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [LicenseKeyDto] licenseKeyDto (required):
|
||||
Future<LicenseResponseDto?> setUserLicense(LicenseKeyDto licenseKeyDto,) async {
|
||||
final response = await setUserLicenseWithHttpInfo(licenseKeyDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /users/me/preferences' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
@ -348,6 +348,10 @@ class ApiClient {
|
||||
return LibraryResponseDto.fromJson(value);
|
||||
case 'LibraryStatsResponseDto':
|
||||
return LibraryStatsResponseDto.fromJson(value);
|
||||
case 'LicenseKeyDto':
|
||||
return LicenseKeyDto.fromJson(value);
|
||||
case 'LicenseResponseDto':
|
||||
return LicenseResponseDto.fromJson(value);
|
||||
case 'LogLevel':
|
||||
return LogLevelTypeTransformer().decode(value);
|
||||
case 'LoginCredentialDto':
|
||||
@ -556,6 +560,8 @@ class ApiClient {
|
||||
return UserAdminUpdateDto.fromJson(value);
|
||||
case 'UserAvatarColor':
|
||||
return UserAvatarColorTypeTransformer().decode(value);
|
||||
case 'UserLicense':
|
||||
return UserLicense.fromJson(value);
|
||||
case 'UserPreferencesResponseDto':
|
||||
return UserPreferencesResponseDto.fromJson(value);
|
||||
case 'UserPreferencesUpdateDto':
|
||||
|
106
mobile/openapi/lib/model/license_key_dto.dart
generated
Normal file
106
mobile/openapi/lib/model/license_key_dto.dart
generated
Normal file
@ -0,0 +1,106 @@
|
||||
//
|
||||
// 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 LicenseKeyDto {
|
||||
/// Returns a new [LicenseKeyDto] instance.
|
||||
LicenseKeyDto({
|
||||
required this.activationKey,
|
||||
required this.licenseKey,
|
||||
});
|
||||
|
||||
String activationKey;
|
||||
|
||||
String licenseKey;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is LicenseKeyDto &&
|
||||
other.activationKey == activationKey &&
|
||||
other.licenseKey == licenseKey;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(activationKey.hashCode) +
|
||||
(licenseKey.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'LicenseKeyDto[activationKey=$activationKey, licenseKey=$licenseKey]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'activationKey'] = this.activationKey;
|
||||
json[r'licenseKey'] = this.licenseKey;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [LicenseKeyDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static LicenseKeyDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return LicenseKeyDto(
|
||||
activationKey: mapValueOfType<String>(json, r'activationKey')!,
|
||||
licenseKey: mapValueOfType<String>(json, r'licenseKey')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<LicenseKeyDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <LicenseKeyDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = LicenseKeyDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, LicenseKeyDto> mapFromJson(dynamic json) {
|
||||
final map = <String, LicenseKeyDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = LicenseKeyDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of LicenseKeyDto-objects as value to a dart map
|
||||
static Map<String, List<LicenseKeyDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<LicenseKeyDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = LicenseKeyDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'activationKey',
|
||||
'licenseKey',
|
||||
};
|
||||
}
|
||||
|
114
mobile/openapi/lib/model/license_response_dto.dart
generated
Normal file
114
mobile/openapi/lib/model/license_response_dto.dart
generated
Normal file
@ -0,0 +1,114 @@
|
||||
//
|
||||
// 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 LicenseResponseDto {
|
||||
/// Returns a new [LicenseResponseDto] instance.
|
||||
LicenseResponseDto({
|
||||
required this.activatedAt,
|
||||
required this.activationKey,
|
||||
required this.licenseKey,
|
||||
});
|
||||
|
||||
DateTime activatedAt;
|
||||
|
||||
String activationKey;
|
||||
|
||||
String licenseKey;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is LicenseResponseDto &&
|
||||
other.activatedAt == activatedAt &&
|
||||
other.activationKey == activationKey &&
|
||||
other.licenseKey == licenseKey;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(activatedAt.hashCode) +
|
||||
(activationKey.hashCode) +
|
||||
(licenseKey.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'LicenseResponseDto[activatedAt=$activatedAt, activationKey=$activationKey, licenseKey=$licenseKey]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String();
|
||||
json[r'activationKey'] = this.activationKey;
|
||||
json[r'licenseKey'] = this.licenseKey;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [LicenseResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static LicenseResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return LicenseResponseDto(
|
||||
activatedAt: mapDateTime(json, r'activatedAt', r'')!,
|
||||
activationKey: mapValueOfType<String>(json, r'activationKey')!,
|
||||
licenseKey: mapValueOfType<String>(json, r'licenseKey')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<LicenseResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <LicenseResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = LicenseResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, LicenseResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, LicenseResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = LicenseResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of LicenseResponseDto-objects as value to a dart map
|
||||
static Map<String, List<LicenseResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<LicenseResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = LicenseResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'activatedAt',
|
||||
'activationKey',
|
||||
'licenseKey',
|
||||
};
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ class ServerAboutResponseDto {
|
||||
this.ffmpeg,
|
||||
this.imagemagick,
|
||||
this.libvips,
|
||||
required this.licensed,
|
||||
this.nodejs,
|
||||
this.repository,
|
||||
this.repositoryUrl,
|
||||
@ -95,6 +96,8 @@ class ServerAboutResponseDto {
|
||||
///
|
||||
String? libvips;
|
||||
|
||||
bool licensed;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@ -157,6 +160,7 @@ class ServerAboutResponseDto {
|
||||
other.ffmpeg == ffmpeg &&
|
||||
other.imagemagick == imagemagick &&
|
||||
other.libvips == libvips &&
|
||||
other.licensed == licensed &&
|
||||
other.nodejs == nodejs &&
|
||||
other.repository == repository &&
|
||||
other.repositoryUrl == repositoryUrl &&
|
||||
@ -177,6 +181,7 @@ class ServerAboutResponseDto {
|
||||
(ffmpeg == null ? 0 : ffmpeg!.hashCode) +
|
||||
(imagemagick == null ? 0 : imagemagick!.hashCode) +
|
||||
(libvips == null ? 0 : libvips!.hashCode) +
|
||||
(licensed.hashCode) +
|
||||
(nodejs == null ? 0 : nodejs!.hashCode) +
|
||||
(repository == null ? 0 : repository!.hashCode) +
|
||||
(repositoryUrl == null ? 0 : repositoryUrl!.hashCode) +
|
||||
@ -187,7 +192,7 @@ class ServerAboutResponseDto {
|
||||
(versionUrl.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, version=$version, versionUrl=$versionUrl]';
|
||||
String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, licensed=$licensed, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, version=$version, versionUrl=$versionUrl]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -231,6 +236,7 @@ class ServerAboutResponseDto {
|
||||
} else {
|
||||
// json[r'libvips'] = null;
|
||||
}
|
||||
json[r'licensed'] = this.licensed;
|
||||
if (this.nodejs != null) {
|
||||
json[r'nodejs'] = this.nodejs;
|
||||
} else {
|
||||
@ -282,6 +288,7 @@ class ServerAboutResponseDto {
|
||||
ffmpeg: mapValueOfType<String>(json, r'ffmpeg'),
|
||||
imagemagick: mapValueOfType<String>(json, r'imagemagick'),
|
||||
libvips: mapValueOfType<String>(json, r'libvips'),
|
||||
licensed: mapValueOfType<bool>(json, r'licensed')!,
|
||||
nodejs: mapValueOfType<String>(json, r'nodejs'),
|
||||
repository: mapValueOfType<String>(json, r'repository'),
|
||||
repositoryUrl: mapValueOfType<String>(json, r'repositoryUrl'),
|
||||
@ -337,6 +344,7 @@ class ServerAboutResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'licensed',
|
||||
'version',
|
||||
'versionUrl',
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ class UserAdminResponseDto {
|
||||
required this.email,
|
||||
required this.id,
|
||||
required this.isAdmin,
|
||||
required this.license,
|
||||
required this.name,
|
||||
required this.oauthId,
|
||||
required this.profileImagePath,
|
||||
@ -42,6 +43,8 @@ class UserAdminResponseDto {
|
||||
|
||||
bool isAdmin;
|
||||
|
||||
UserLicense? license;
|
||||
|
||||
String name;
|
||||
|
||||
String oauthId;
|
||||
@ -68,6 +71,7 @@ class UserAdminResponseDto {
|
||||
other.email == email &&
|
||||
other.id == id &&
|
||||
other.isAdmin == isAdmin &&
|
||||
other.license == license &&
|
||||
other.name == name &&
|
||||
other.oauthId == oauthId &&
|
||||
other.profileImagePath == profileImagePath &&
|
||||
@ -87,6 +91,7 @@ class UserAdminResponseDto {
|
||||
(email.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isAdmin.hashCode) +
|
||||
(license == null ? 0 : license!.hashCode) +
|
||||
(name.hashCode) +
|
||||
(oauthId.hashCode) +
|
||||
(profileImagePath.hashCode) +
|
||||
@ -98,7 +103,7 @@ class UserAdminResponseDto {
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -112,6 +117,11 @@ class UserAdminResponseDto {
|
||||
json[r'email'] = this.email;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isAdmin'] = this.isAdmin;
|
||||
if (this.license != null) {
|
||||
json[r'license'] = this.license;
|
||||
} else {
|
||||
// json[r'license'] = null;
|
||||
}
|
||||
json[r'name'] = this.name;
|
||||
json[r'oauthId'] = this.oauthId;
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
@ -150,6 +160,7 @@ class UserAdminResponseDto {
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||
license: UserLicense.fromJson(json[r'license']),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
oauthId: mapValueOfType<String>(json, r'oauthId')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
@ -212,6 +223,7 @@ class UserAdminResponseDto {
|
||||
'email',
|
||||
'id',
|
||||
'isAdmin',
|
||||
'license',
|
||||
'name',
|
||||
'oauthId',
|
||||
'profileImagePath',
|
||||
|
114
mobile/openapi/lib/model/user_license.dart
generated
Normal file
114
mobile/openapi/lib/model/user_license.dart
generated
Normal file
@ -0,0 +1,114 @@
|
||||
//
|
||||
// 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 UserLicense {
|
||||
/// Returns a new [UserLicense] instance.
|
||||
UserLicense({
|
||||
required this.activatedAt,
|
||||
required this.activationKey,
|
||||
required this.licenseKey,
|
||||
});
|
||||
|
||||
DateTime activatedAt;
|
||||
|
||||
String activationKey;
|
||||
|
||||
String licenseKey;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UserLicense &&
|
||||
other.activatedAt == activatedAt &&
|
||||
other.activationKey == activationKey &&
|
||||
other.licenseKey == licenseKey;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(activatedAt.hashCode) +
|
||||
(activationKey.hashCode) +
|
||||
(licenseKey.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserLicense[activatedAt=$activatedAt, activationKey=$activationKey, licenseKey=$licenseKey]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String();
|
||||
json[r'activationKey'] = this.activationKey;
|
||||
json[r'licenseKey'] = this.licenseKey;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [UserLicense] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UserLicense? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UserLicense(
|
||||
activatedAt: mapDateTime(json, r'activatedAt', r'')!,
|
||||
activationKey: mapValueOfType<String>(json, r'activationKey')!,
|
||||
licenseKey: mapValueOfType<String>(json, r'licenseKey')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UserLicense> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UserLicense>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UserLicense.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UserLicense> mapFromJson(dynamic json) {
|
||||
final map = <String, UserLicense>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UserLicense.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UserLicense-objects as value to a dart map
|
||||
static Map<String, List<UserLicense>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UserLicense>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = UserLicense.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'activatedAt',
|
||||
'activationKey',
|
||||
'licenseKey',
|
||||
};
|
||||
}
|
||||
|
@ -4994,6 +4994,101 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/server/license": {
|
||||
"delete": {
|
||||
"operationId": "deleteServerLicense",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Server"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getServerLicense",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Server"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "setServerLicense",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LicenseKeyDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LicenseResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Server"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions": {
|
||||
"delete": {
|
||||
"operationId": "deleteAllSessions",
|
||||
@ -6594,6 +6689,101 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/users/me/license": {
|
||||
"delete": {
|
||||
"operationId": "deleteUserLicense",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getUserLicense",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LicenseResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "setUserLicense",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LicenseKeyDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LicenseResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/users/me/preferences": {
|
||||
"get": {
|
||||
"operationId": "getMyPreferences",
|
||||
@ -8765,6 +8955,43 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LicenseKeyDto": {
|
||||
"properties": {
|
||||
"activationKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"licenseKey": {
|
||||
"pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activationKey",
|
||||
"licenseKey"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LicenseResponseDto": {
|
||||
"properties": {
|
||||
"activatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"activationKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"licenseKey": {
|
||||
"pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activatedAt",
|
||||
"activationKey",
|
||||
"licenseKey"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LogLevel": {
|
||||
"enum": [
|
||||
"verbose",
|
||||
@ -9752,6 +9979,9 @@
|
||||
"libvips": {
|
||||
"type": "string"
|
||||
},
|
||||
"licensed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"nodejs": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -9778,6 +10008,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"licensed",
|
||||
"version",
|
||||
"versionUrl"
|
||||
],
|
||||
@ -11330,6 +11561,14 @@
|
||||
"isAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"license": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UserLicense"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -11371,6 +11610,7 @@
|
||||
"email",
|
||||
"id",
|
||||
"isAdmin",
|
||||
"license",
|
||||
"name",
|
||||
"oauthId",
|
||||
"profileImagePath",
|
||||
@ -11425,6 +11665,26 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"UserLicense": {
|
||||
"properties": {
|
||||
"activatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"activationKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"licenseKey": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activatedAt",
|
||||
"activationKey",
|
||||
"licenseKey"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserPreferencesResponseDto": {
|
||||
"properties": {
|
||||
"avatar": {
|
||||
|
@ -38,6 +38,11 @@ export type ActivityCreateDto = {
|
||||
export type ActivityStatisticsResponseDto = {
|
||||
comments: number;
|
||||
};
|
||||
export type UserLicense = {
|
||||
activatedAt: string;
|
||||
activationKey: string;
|
||||
licenseKey: string;
|
||||
};
|
||||
export type UserAdminResponseDto = {
|
||||
avatarColor: UserAvatarColor;
|
||||
createdAt: string;
|
||||
@ -45,6 +50,7 @@ export type UserAdminResponseDto = {
|
||||
email: string;
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
license: (UserLicense) | null;
|
||||
name: string;
|
||||
oauthId: string;
|
||||
profileImagePath: string;
|
||||
@ -800,6 +806,7 @@ export type ServerAboutResponseDto = {
|
||||
ffmpeg?: string;
|
||||
imagemagick?: string;
|
||||
libvips?: string;
|
||||
licensed: boolean;
|
||||
nodejs?: string;
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
@ -873,6 +880,15 @@ export type ServerVersionResponseDto = {
|
||||
minor: number;
|
||||
patch: number;
|
||||
};
|
||||
export type LicenseKeyDto = {
|
||||
activationKey: string;
|
||||
licenseKey: string;
|
||||
};
|
||||
export type LicenseResponseDto = {
|
||||
activatedAt: string;
|
||||
activationKey: string;
|
||||
licenseKey: string;
|
||||
};
|
||||
export type SessionResponseDto = {
|
||||
createdAt: string;
|
||||
current: boolean;
|
||||
@ -2484,6 +2500,32 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function deleteServerLicense(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/server/license", {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function getServerLicense(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: object;
|
||||
}>("/server/license", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function setServerLicense({ licenseKeyDto }: {
|
||||
licenseKeyDto: LicenseKeyDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: LicenseResponseDto;
|
||||
}>("/server/license", oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: licenseKeyDto
|
||||
})));
|
||||
}
|
||||
export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/sessions", {
|
||||
...opts,
|
||||
@ -2892,6 +2934,32 @@ export function updateMyUser({ userUpdateMeDto }: {
|
||||
body: userUpdateMeDto
|
||||
})));
|
||||
}
|
||||
export function deleteUserLicense(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/users/me/license", {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function getUserLicense(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: LicenseResponseDto;
|
||||
}>("/users/me/license", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function setUserLicense({ licenseKeyDto }: {
|
||||
licenseKeyDto: LicenseKeyDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: LicenseResponseDto;
|
||||
}>("/users/me/license", oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: licenseKeyDto
|
||||
})));
|
||||
}
|
||||
export function getMyPreferences(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
|
@ -361,7 +361,7 @@ export const immichAppConfig: ConfigModuleOptions = {
|
||||
envFilePath: '.env',
|
||||
isGlobal: true,
|
||||
validationSchema: Joi.object({
|
||||
IMMICH_ENV: Joi.string().optional().valid('development', 'production').default('production'),
|
||||
IMMICH_ENV: Joi.string().optional().valid('development', 'testing', 'production').default('production'),
|
||||
IMMICH_LOG_LEVEL: Joi.string()
|
||||
.optional()
|
||||
.valid(...Object.values(LogLevel)),
|
||||
@ -441,3 +441,29 @@ export const getBuildMetadata = () => ({
|
||||
sourceCommit: process.env.IMMICH_SOURCE_COMMIT,
|
||||
sourceUrl: process.env.IMMICH_SOURCE_URL,
|
||||
});
|
||||
|
||||
const clientLicensePublicKeyProd =
|
||||
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2LzdTMzJjUkE1KysxTm5WRHNDTQpzcFAvakpISU1xT0pYRm5oNE53QTJPcHorUk1mZGNvOTJQc09naCt3d1FlRXYxVTJjMnBqelRpUS8ybHJLcS9rCnpKUmxYd2M0Y1Vlc1FETUpPRitQMnFPTlBiQUprWHZDWFlCVUxpdENJa29Md2ZoU0dOanlJS2FSRGhkL3ROeU4KOCtoTlJabllUMWhTSWo5U0NrS3hVQ096YXRQVjRtQ0RlclMrYkUrZ0VVZVdwOTlWOWF6dkYwRkltblRXcFFTdwpjOHdFWmdPTWg0c3ZoNmFpY3dkemtQQ3dFTGFrMFZhQkgzMUJFVUNRTGI5K0FJdEhBVXRKQ0t4aGI1V2pzMXM5CmJyWGZpMHZycGdjWi82RGFuWTJxZlNQem5PbXZEMkZycmxTMXE0SkpOM1ZvN1d3LzBZeS95TWNtelRXWmhHdWgKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
|
||||
|
||||
const clientLicensePublicKeyStaging =
|
||||
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFuSUNyTm5jbGpPSC9JdTNtWVVaRQp0dGJLV1c3OGRuajl5M0U2ekk3dU1NUndEckdYWFhkTGhkUDFxSWtlZHh0clVVeUpCMWR4R04yQW91S082MlNGCldrbU9PTmNGQlRBWFZTdjhUNVY0S0VwWnFQYWEwaXpNaGxMaE5sRXEvY1ZKdllrWlh1Z2x6b1o3cG1nbzFSdHgKam1iRm5NNzhrYTFRUUJqOVdLaEw2eWpWRUl2MDdVS0lKWHBNTnNuS2g1V083MjZhYmMzSE9udTlETjY5VnFFRQo3dGZrUnRWNmx2U1NzMkFVMngzT255cHA4ek53b0lPTWRibGsyb09aWWROZzY0Y3l2SzJoU0FlU3NVMFRyOVc5Ckgra0Y5QlNCNlk0QXl0QlVkSmkrK2pMSW5HM2Q5cU9ieFVzTlYrN05mRkF5NjJkL0xNR0xSOC9OUFc0U0s3c0MKRlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
|
||||
|
||||
export const getClientLicensePublicKey = (): string => {
|
||||
if (process.env.IMMICH_ENV === 'production') {
|
||||
return clientLicensePublicKeyProd;
|
||||
}
|
||||
return clientLicensePublicKeyStaging;
|
||||
};
|
||||
|
||||
const serverLicensePublicKeyProd =
|
||||
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFvcG5ZRGEwYS9kVTVJZUc3NGlFRQpNd2RBS2pzTmN6TGRDcVJkMVo5eTVUMndqTzdlWUlPZUpUc2wzNTBzUjBwNEtmU1VEU1h2QzlOcERwYzF0T0tsCjVzaEMvQXhwdlFBTENva0Y0anQ4dnJyZDlmQ2FYYzFUcVJiT21uaGl1Z0Q2dmtyME8vRmIzVURpM1UwVHZoUFAKbFBkdlNhd3pMcldaUExmbUhWVnJiclNLbW45SWVTZ3kwN3VrV1RJeUxzY2lOcnZuQnl3c0phUmVEdW9OV1BCSApVL21vMm1YYThtNHdNV2hpWGVoaUlPUXFNdVNVZ1BlQ3NXajhVVngxQ0dsUnpQREEwYlZOUXZlS1hXVnhjRUk2ClVMRWdKeTJGNDlsSDArYVlDbUJmN05FcjZWUTJXQjk1ZXZUS1hLdm4wcUlNN25nRmxjVUF3NmZ1VjFjTkNUSlMKNndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
|
||||
|
||||
const serverLicensePublicKeyStaging =
|
||||
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE3Sy8yd3ZLUS9NdU8ydi9MUm5saAoyUy9zTHhDOGJiTEw1UUlKOGowQ3BVZW40YURlY2dYMUpKUmtGNlpUVUtpNTdTbEhtS3RSM2JOTzJmdTBUUVg5Ck5WMEJzVzllZVB0MmlTMWl4VVFmTzRObjdvTjZzbEtac01qd29RNGtGRGFmM3VHTlZJc0dMb3UxVWRLUVhpeDEKUlRHcXVTb3NZVjNWRlk3Q1hGYTVWaENBL3poVXNsNGFuVXp3eEF6M01jUFVlTXBaenYvbVZiQlRKVzBPSytWZgpWQUJvMXdYMkVBanpBekVHVzQ3Vko4czhnMnQrNHNPaHFBNStMQjBKVzlORUg5QUpweGZzWE4zSzVtM00yNUJVClZXcTlRYStIdHRENnJ0bnAvcUFweXVkWUdwZk9HYTRCUlZTR1MxMURZM0xrb2FlRzYwUEU5NHpoYjduOHpMWkgKelFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=';
|
||||
|
||||
export const getServerLicensePublicKey = (): string => {
|
||||
if (process.env.IMMICH_ENV === 'production') {
|
||||
return serverLicensePublicKeyProd;
|
||||
}
|
||||
return serverLicensePublicKeyStaging;
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Put } from '@nestjs/common';
|
||||
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
import {
|
||||
ServerAboutResponseDto,
|
||||
ServerConfigDto,
|
||||
@ -79,4 +80,22 @@ export class ServerController {
|
||||
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
|
||||
return this.service.getSupportedMediaTypes();
|
||||
}
|
||||
|
||||
@Put('license')
|
||||
@Authenticated({ admin: true })
|
||||
setServerLicense(@Body() license: LicenseKeyDto): Promise<LicenseResponseDto> {
|
||||
return this.service.setLicense(license);
|
||||
}
|
||||
|
||||
@Delete('license')
|
||||
@Authenticated({ admin: true })
|
||||
deleteServerLicense(): Promise<void> {
|
||||
return this.service.deleteLicense();
|
||||
}
|
||||
|
||||
@Get('license')
|
||||
@Authenticated({ admin: true })
|
||||
getServerLicense(): Promise<LicenseKeyDto | null> {
|
||||
return this.service.getLicense();
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
||||
@ -68,6 +69,24 @@ export class UserController {
|
||||
return this.service.updateMyPreferences(auth, dto);
|
||||
}
|
||||
|
||||
@Get('me/license')
|
||||
@Authenticated()
|
||||
getUserLicense(@Auth() auth: AuthDto): LicenseResponseDto {
|
||||
return this.service.getLicense(auth);
|
||||
}
|
||||
|
||||
@Put('me/license')
|
||||
@Authenticated()
|
||||
async setUserLicense(@Auth() auth: AuthDto, @Body() license: LicenseKeyDto): Promise<LicenseResponseDto> {
|
||||
return this.service.setLicense(auth, license);
|
||||
}
|
||||
|
||||
@Delete('me/license')
|
||||
@Authenticated()
|
||||
async deleteUserLicense(@Auth() auth: AuthDto): Promise<void> {
|
||||
await this.service.deleteLicense(auth);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||
|
16
server/src/dtos/license.dto.ts
Normal file
16
server/src/dtos/license.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { IsNotEmpty, IsString, Matches } from 'class-validator';
|
||||
|
||||
export class LicenseKeyDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Matches(/IM(SV|CL)(-[\dA-Za-z]{4}){8}/)
|
||||
licenseKey!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
activationKey!: string;
|
||||
}
|
||||
|
||||
export class LicenseResponseDto extends LicenseKeyDto {
|
||||
activatedAt!: Date;
|
||||
}
|
@ -28,6 +28,8 @@ export class ServerAboutResponseDto {
|
||||
imagemagick?: string;
|
||||
libvips?: string;
|
||||
exiftool?: string;
|
||||
|
||||
licensed!: boolean;
|
||||
}
|
||||
|
||||
export class ServerStorageResponseDto {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
|
||||
import { UserAvatarColor } from 'src/entities/user-metadata.entity';
|
||||
import { UserAvatarColor, UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity, UserStatus } from 'src/entities/user.entity';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||
import { Optional, toEmail, toSanitized, ValidateBoolean } from 'src/validation';
|
||||
|
||||
export class UserUpdateMeDto {
|
||||
@Optional()
|
||||
@ -33,6 +33,12 @@ export class UserResponseDto {
|
||||
avatarColor!: UserAvatarColor;
|
||||
}
|
||||
|
||||
export class UserLicense {
|
||||
licenseKey!: string;
|
||||
activationKey!: string;
|
||||
activatedAt!: Date;
|
||||
}
|
||||
|
||||
export const mapUser = (entity: UserEntity): UserResponseDto => {
|
||||
return {
|
||||
id: entity.id,
|
||||
@ -130,9 +136,13 @@ export class UserAdminResponseDto extends UserResponseDto {
|
||||
quotaUsageInBytes!: number | null;
|
||||
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
|
||||
status!: string;
|
||||
license!: UserLicense | null;
|
||||
}
|
||||
|
||||
export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto {
|
||||
const license = entity.metadata.find(
|
||||
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
||||
)?.value;
|
||||
return {
|
||||
...mapUser(entity),
|
||||
storageLabel: entity.storageLabel,
|
||||
@ -145,5 +155,6 @@ export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto {
|
||||
quotaSizeInBytes: entity.quotaSizeInBytes,
|
||||
quotaUsageInBytes: entity.quotaUsageInBytes,
|
||||
status: entity.status,
|
||||
license: license ?? null,
|
||||
};
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export enum SystemMetadataKey {
|
||||
ADMIN_ONBOARDING = 'admin-onboarding',
|
||||
SYSTEM_CONFIG = 'system-config',
|
||||
VERSION_CHECK_STATE = 'version-check-state',
|
||||
LICENSE = 'license',
|
||||
}
|
||||
|
||||
export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string };
|
||||
@ -24,4 +25,5 @@ export interface SystemMetadata extends Record<SystemMetadataKey, Record<string,
|
||||
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
|
||||
[SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>;
|
||||
[SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata;
|
||||
[SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date };
|
||||
}
|
||||
|
@ -73,8 +73,10 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
|
||||
|
||||
export enum UserMetadataKey {
|
||||
PREFERENCES = 'preferences',
|
||||
LICENSE = 'license',
|
||||
}
|
||||
|
||||
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {
|
||||
[UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>;
|
||||
[UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date };
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ export interface ICryptoRepository {
|
||||
randomUUID(): string;
|
||||
hashFile(filePath: string | Buffer): Promise<Buffer>;
|
||||
hashSha256(data: string): string;
|
||||
verifySha256(data: string, encrypted: string, publicKey: string): boolean;
|
||||
hashSha1(data: string | Buffer): Buffer;
|
||||
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
|
||||
compareBcrypt(data: string | Buffer, encrypted: string): boolean;
|
||||
|
@ -5,5 +5,6 @@ export const ISystemMetadataRepository = 'ISystemMetadataRepository';
|
||||
export interface ISystemMetadataRepository {
|
||||
get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
|
||||
set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>;
|
||||
delete<T extends keyof SystemMetadata>(key: T): Promise<void>;
|
||||
readFile(filename: string): Promise<string>;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ export interface IUserRepository {
|
||||
create(user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>;
|
||||
deleteMetadata<T extends keyof UserMetadata>(id: string, key: T): Promise<void>;
|
||||
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
||||
updateUsage(id: string, delta: number): Promise<void>;
|
||||
syncUsage(id?: string): Promise<void>;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { compareSync, hash } from 'bcrypt';
|
||||
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
||||
import { createHash, createPublicKey, createVerify, randomBytes, randomUUID } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
@ -28,6 +28,21 @@ export class CryptoRepository implements ICryptoRepository {
|
||||
return createHash('sha256').update(value).digest('base64');
|
||||
}
|
||||
|
||||
verifySha256(value: string, encryptedValue: string, publicKey: string) {
|
||||
const publicKeyBuffer = Buffer.from(publicKey, 'base64');
|
||||
const cryptoPublicKey = createPublicKey({
|
||||
key: publicKeyBuffer,
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
});
|
||||
|
||||
const verifier = createVerify('SHA256');
|
||||
verifier.update(value);
|
||||
verifier.end();
|
||||
const encryptedValueBuffer = Buffer.from(encryptedValue, 'base64');
|
||||
return verifier.verify(cryptoPublicKey, encryptedValueBuffer);
|
||||
}
|
||||
|
||||
hashSha1(value: string | Buffer): Buffer {
|
||||
return createHash('sha1').update(value).digest();
|
||||
}
|
||||
|
@ -26,6 +26,10 @@ export class SystemMetadataRepository implements ISystemMetadataRepository {
|
||||
await this.repository.upsert({ key, value }, { conflictPaths: { key: true } });
|
||||
}
|
||||
|
||||
async delete<T extends keyof SystemMetadata>(key: T): Promise<void> {
|
||||
await this.repository.delete({ key });
|
||||
}
|
||||
|
||||
readFile(filename: string): Promise<string> {
|
||||
return readFile(filename, { encoding: 'utf8' });
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserMetadata, UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserMetadata, UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
IUserRepository,
|
||||
@ -89,13 +89,14 @@ export class UserRepository implements IUserRepository {
|
||||
return this.save({ ...user, id });
|
||||
}
|
||||
|
||||
async upsertMetadata<T extends UserMetadataKey.PREFERENCES>(
|
||||
id: string,
|
||||
{ key, value }: { key: T; value: UserMetadata[T] },
|
||||
) {
|
||||
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
|
||||
await this.metadataRepository.upsert({ userId: id, key, value }, { conflictPaths: { userId: true, key: true } });
|
||||
}
|
||||
|
||||
async deleteMetadata<T extends keyof UserMetadata>(id: string, key: T) {
|
||||
await this.metadataRepository.delete({ userId: id, key });
|
||||
}
|
||||
|
||||
async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> {
|
||||
return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user);
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { ServerService } from 'src/services/server.service';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
@ -18,6 +21,7 @@ describe(ServerService.name, () => {
|
||||
let serverInfoMock: Mocked<IServerInfoRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
storageMock = newStorageRepositoryMock();
|
||||
@ -25,8 +29,9 @@ describe(ServerService.name, () => {
|
||||
serverInfoMock = newServerInfoRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
|
||||
sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock);
|
||||
sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@ -249,4 +254,33 @@ describe(ServerService.name, () => {
|
||||
expect(userMock.getUserStats).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLicense', () => {
|
||||
it('should save license if valid', async () => {
|
||||
systemMock.set.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'IMSV-license-key', activationKey: 'activation-key' };
|
||||
await sut.setLicense(license);
|
||||
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.LICENSE, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should not save license if invalid', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'license-key', activationKey: 'activation-key' };
|
||||
const call = sut.setLicense(license);
|
||||
await expect(call).rejects.toThrowError('Invalid license key');
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteLicense', () => {
|
||||
it('should delete license', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
|
||||
await sut.deleteLicense();
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { getBuildMetadata } from 'src/config';
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
import {
|
||||
ServerAboutResponseDto,
|
||||
ServerConfigDto,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
UsageByUserDto,
|
||||
} from 'src/dtos/server.dto';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { OnEvents } from 'src/interfaces/event.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
@ -34,6 +36,7 @@ export class ServerService implements OnEvents {
|
||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
) {
|
||||
this.logger.setContext(ServerService.name);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
@ -53,10 +56,12 @@ export class ServerService implements OnEvents {
|
||||
const version = `v${serverVersion.toString()}`;
|
||||
const buildMetadata = getBuildMetadata();
|
||||
const buildVersions = await this.serverInfoRepository.getBuildVersions();
|
||||
const licensed = await this.systemMetadataRepository.get(SystemMetadataKey.LICENSE);
|
||||
|
||||
return {
|
||||
version,
|
||||
versionUrl: `https://github.com/immich-app/immich/releases/tag/${version}`,
|
||||
licensed: !!licensed,
|
||||
...buildMetadata,
|
||||
...buildVersions,
|
||||
};
|
||||
@ -154,4 +159,36 @@ export class ServerService implements OnEvents {
|
||||
sidecar: Object.keys(mimeTypes.sidecar),
|
||||
};
|
||||
}
|
||||
|
||||
async deleteLicense(): Promise<void> {
|
||||
await this.systemMetadataRepository.delete(SystemMetadataKey.LICENSE);
|
||||
}
|
||||
|
||||
async getLicense(): Promise<LicenseKeyDto | null> {
|
||||
return this.systemMetadataRepository.get(SystemMetadataKey.LICENSE);
|
||||
}
|
||||
|
||||
async setLicense(dto: LicenseKeyDto): Promise<LicenseResponseDto> {
|
||||
if (!dto.licenseKey.startsWith('IMSV-')) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
const licenseValid = this.cryptoRepository.verifySha256(
|
||||
dto.licenseKey,
|
||||
dto.activationKey,
|
||||
getServerLicensePublicKey(),
|
||||
);
|
||||
|
||||
if (!licenseValid) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
|
||||
const licenseData = {
|
||||
...dto,
|
||||
activatedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.LICENSE, licenseData);
|
||||
|
||||
return licenseData;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
@ -285,6 +286,38 @@ describe(UserService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLicense', () => {
|
||||
it('should save license if valid', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'IMCL-license-key', activationKey: 'activation-key' };
|
||||
await sut.setLicense(authStub.user1, license);
|
||||
|
||||
expect(userMock.upsertMetadata).toHaveBeenCalledWith(authStub.user1.user.id, {
|
||||
key: UserMetadataKey.LICENSE,
|
||||
value: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not save license if invalid', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'license-key', activationKey: 'activation-key' };
|
||||
const call = sut.setLicense(authStub.admin, license);
|
||||
await expect(call).rejects.toThrowError('Invalid license key');
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteLicense', () => {
|
||||
it('should delete license', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
|
||||
await sut.deleteLicense(authStub.admin);
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUserSyncUsage', () => {
|
||||
it('should sync usage', async () => {
|
||||
await sut.handleUserSyncUsage();
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getClientLicensePublicKey } from 'src/config';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
import { mapPreferences, UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
|
||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { mapUser, mapUserAdmin, UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
||||
import { UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
@ -123,6 +125,47 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
getLicense({ user }: AuthDto): LicenseResponseDto {
|
||||
const license = user.metadata.find(
|
||||
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
||||
);
|
||||
if (!license) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return license.value;
|
||||
}
|
||||
|
||||
async deleteLicense({ user }: AuthDto): Promise<void> {
|
||||
await this.userRepository.deleteMetadata(user.id, UserMetadataKey.LICENSE);
|
||||
}
|
||||
|
||||
async setLicense(auth: AuthDto, license: LicenseKeyDto): Promise<LicenseResponseDto> {
|
||||
if (!license.licenseKey.startsWith('IMCL-')) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
const licenseValid = this.cryptoRepository.verifySha256(
|
||||
license.licenseKey,
|
||||
license.activationKey,
|
||||
getClientLicensePublicKey(),
|
||||
);
|
||||
|
||||
if (!licenseValid) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
|
||||
const licenseData = {
|
||||
...license,
|
||||
activatedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.userRepository.upsertMetadata(auth.user.id, {
|
||||
key: UserMetadataKey.LICENSE,
|
||||
value: licenseData,
|
||||
});
|
||||
|
||||
return licenseData;
|
||||
}
|
||||
|
||||
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||
await this.userRepository.syncUsage();
|
||||
return JobStatus.SUCCESS;
|
||||
|
9
server/test/fixtures/auth.stub.ts
vendored
9
server/test/fixtures/auth.stub.ts
vendored
@ -1,6 +1,7 @@
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export const authStub = {
|
||||
@ -9,6 +10,7 @@ export const authStub = {
|
||||
id: 'admin_id',
|
||||
email: 'admin@test.com',
|
||||
isAdmin: true,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
}),
|
||||
user1: Object.freeze<AuthDto>({
|
||||
@ -16,6 +18,7 @@ export const authStub = {
|
||||
id: 'user-id',
|
||||
email: 'immich@test.com',
|
||||
isAdmin: false,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
session: {
|
||||
id: 'token-id',
|
||||
@ -26,6 +29,7 @@ export const authStub = {
|
||||
id: 'user-2',
|
||||
email: 'user2@immich.app',
|
||||
isAdmin: false,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
session: {
|
||||
id: 'token-id',
|
||||
@ -36,6 +40,7 @@ export const authStub = {
|
||||
id: 'user-id',
|
||||
email: 'immich@test.com',
|
||||
isAdmin: false,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
session: {
|
||||
id: 'token-id',
|
||||
@ -46,6 +51,7 @@ export const authStub = {
|
||||
id: 'admin_id',
|
||||
email: 'admin@test.com',
|
||||
isAdmin: true,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
sharedLink: {
|
||||
id: '123',
|
||||
@ -60,6 +66,7 @@ export const authStub = {
|
||||
id: 'admin_id',
|
||||
email: 'admin@test.com',
|
||||
isAdmin: true,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
sharedLink: {
|
||||
id: '123',
|
||||
@ -74,6 +81,7 @@ export const authStub = {
|
||||
id: 'admin_id',
|
||||
email: 'admin@test.com',
|
||||
isAdmin: true,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
sharedLink: {
|
||||
id: '123',
|
||||
@ -87,6 +95,7 @@ export const authStub = {
|
||||
id: 'admin_id',
|
||||
email: 'admin@test.com',
|
||||
isAdmin: true,
|
||||
metadata: [] as UserMetadataEntity[],
|
||||
} as UserEntity,
|
||||
sharedLink: {
|
||||
id: '123',
|
||||
|
@ -8,6 +8,7 @@ export const newCryptoRepositoryMock = (): Mocked<ICryptoRepository> => {
|
||||
compareBcrypt: vitest.fn().mockReturnValue(true),
|
||||
hashBcrypt: vitest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)),
|
||||
hashSha256: vitest.fn().mockImplementation((input) => `${input} (hashed)`),
|
||||
verifySha256: vitest.fn().mockImplementation(() => true),
|
||||
hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)),
|
||||
hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`),
|
||||
newPassword: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')),
|
||||
|
@ -10,6 +10,7 @@ export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMet
|
||||
return {
|
||||
get: vitest.fn() as any,
|
||||
set: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
readFile: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
@ -23,5 +23,6 @@ export const newUserRepositoryMock = (reset = true): Mocked<IUserRepository> =>
|
||||
updateUsage: vitest.fn(),
|
||||
syncUsage: vitest.fn(),
|
||||
upsertMetadata: vitest.fn(),
|
||||
deleteMetadata: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
@ -26,4 +26,9 @@ export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({
|
||||
shouldChangePassword: false,
|
||||
status: UserStatus.Active,
|
||||
storageLabel: null,
|
||||
license: {
|
||||
licenseKey: 'IMCL-license-key',
|
||||
activationKey: 'activation-key',
|
||||
activatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user