mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-01 02:17:43 +09: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:
		| @@ -22,6 +22,7 @@ services: | |||||||
|       - DB_DATABASE_NAME=immich |       - DB_DATABASE_NAME=immich | ||||||
|       - IMMICH_MACHINE_LEARNING_ENABLED=false |       - IMMICH_MACHINE_LEARNING_ENABLED=false | ||||||
|       - IMMICH_METRICS=true |       - IMMICH_METRICS=true | ||||||
|  |       - IMMICH_ENV=testing | ||||||
|     volumes: |     volumes: | ||||||
|       - upload:/usr/src/app/upload |       - upload:/usr/src/app/upload | ||||||
|       - ./test-assets:/test-assets |       - ./test-assets:/test-assets | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ describe('/server-info', () => { | |||||||
|         imagemagick: expect.any(String), |         imagemagick: expect.any(String), | ||||||
|         libvips: expect.any(String), |         libvips: expect.any(String), | ||||||
|         exiftool: expect.any(String), |         exiftool: expect.any(String), | ||||||
|  |         licensed: false, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -5,6 +5,12 @@ import { app, utils } from 'src/utils'; | |||||||
| import request from 'supertest'; | import request from 'supertest'; | ||||||
| import { beforeAll, describe, expect, it } from 'vitest'; | 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', () => { | describe('/server', () => { | ||||||
|   let admin: LoginResponseDto; |   let admin: LoginResponseDto; | ||||||
|   let nonAdmin: LoginResponseDto; |   let nonAdmin: LoginResponseDto; | ||||||
| @@ -44,6 +50,7 @@ describe('/server', () => { | |||||||
|         imagemagick: expect.any(String), |         imagemagick: expect.any(String), | ||||||
|         libvips: expect.any(String), |         libvips: expect.any(String), | ||||||
|         exiftool: 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 request from 'supertest'; | ||||||
| import { beforeAll, describe, expect, it } from 'vitest'; | 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', () => { | describe('/users', () => { | ||||||
|   let admin: LoginResponseDto; |   let admin: LoginResponseDto; | ||||||
|   let deletedUser: LoginResponseDto; |   let deletedUser: LoginResponseDto; | ||||||
| @@ -72,6 +78,24 @@ describe('/users', () => { | |||||||
|         quotaUsageInBytes: 0, |         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', () => { |   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, |     quotaUsageInBytes: 0, | ||||||
|     quotaSizeInBytes: null, |     quotaSizeInBytes: null, | ||||||
|     status: 'active', |     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* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |  | ||||||
| *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |  | *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |  | ||||||
| *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |  | *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* | [**getAboutInfo**](doc//ServerInfoApi.md#getaboutinfo) | **GET** /server-info/about |  | ||||||
| *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |  | *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |  | ||||||
| *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |  | *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 |  | *TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore |  | ||||||
| *UsersApi* | [**createProfileImage**](doc//UsersApi.md#createprofileimage) | **POST** /users/profile-image |  | *UsersApi* | [**createProfileImage**](doc//UsersApi.md#createprofileimage) | **POST** /users/profile-image |  | ||||||
| *UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /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* | [**getMyPreferences**](doc//UsersApi.md#getmypreferences) | **GET** /users/me/preferences |  | ||||||
| *UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me |  | *UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me |  | ||||||
| *UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image |  | *UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image |  | ||||||
| *UsersApi* | [**getUser**](doc//UsersApi.md#getuser) | **GET** /users/{id} |  | *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* | [**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* | [**updateMyPreferences**](doc//UsersApi.md#updatemypreferences) | **PUT** /users/me/preferences |  | ||||||
| *UsersApi* | [**updateMyUser**](doc//UsersApi.md#updatemyuser) | **PUT** /users/me |  | *UsersApi* | [**updateMyUser**](doc//UsersApi.md#updatemyuser) | **PUT** /users/me |  | ||||||
| *UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users |  | *UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users |  | ||||||
| @@ -326,6 +332,8 @@ Class | Method | HTTP request | Description | |||||||
|  - [JobStatusDto](doc//JobStatusDto.md) |  - [JobStatusDto](doc//JobStatusDto.md) | ||||||
|  - [LibraryResponseDto](doc//LibraryResponseDto.md) |  - [LibraryResponseDto](doc//LibraryResponseDto.md) | ||||||
|  - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) |  - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) | ||||||
|  |  - [LicenseKeyDto](doc//LicenseKeyDto.md) | ||||||
|  |  - [LicenseResponseDto](doc//LicenseResponseDto.md) | ||||||
|  - [LogLevel](doc//LogLevel.md) |  - [LogLevel](doc//LogLevel.md) | ||||||
|  - [LoginCredentialDto](doc//LoginCredentialDto.md) |  - [LoginCredentialDto](doc//LoginCredentialDto.md) | ||||||
|  - [LoginResponseDto](doc//LoginResponseDto.md) |  - [LoginResponseDto](doc//LoginResponseDto.md) | ||||||
| @@ -430,6 +438,7 @@ Class | Method | HTTP request | Description | |||||||
|  - [UserAdminResponseDto](doc//UserAdminResponseDto.md) |  - [UserAdminResponseDto](doc//UserAdminResponseDto.md) | ||||||
|  - [UserAdminUpdateDto](doc//UserAdminUpdateDto.md) |  - [UserAdminUpdateDto](doc//UserAdminUpdateDto.md) | ||||||
|  - [UserAvatarColor](doc//UserAvatarColor.md) |  - [UserAvatarColor](doc//UserAvatarColor.md) | ||||||
|  |  - [UserLicense](doc//UserLicense.md) | ||||||
|  - [UserPreferencesResponseDto](doc//UserPreferencesResponseDto.md) |  - [UserPreferencesResponseDto](doc//UserPreferencesResponseDto.md) | ||||||
|  - [UserPreferencesUpdateDto](doc//UserPreferencesUpdateDto.md) |  - [UserPreferencesUpdateDto](doc//UserPreferencesUpdateDto.md) | ||||||
|  - [UserResponseDto](doc//UserResponseDto.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/partners_api.dart'; | ||||||
| part 'api/people_api.dart'; | part 'api/people_api.dart'; | ||||||
| part 'api/search_api.dart'; | part 'api/search_api.dart'; | ||||||
|  | part 'api/server_api.dart'; | ||||||
| part 'api/server_info_api.dart'; | part 'api/server_info_api.dart'; | ||||||
| part 'api/sessions_api.dart'; | part 'api/sessions_api.dart'; | ||||||
| part 'api/shared_links_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/job_status_dto.dart'; | ||||||
| part 'model/library_response_dto.dart'; | part 'model/library_response_dto.dart'; | ||||||
| part 'model/library_stats_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/log_level.dart'; | ||||||
| part 'model/login_credential_dto.dart'; | part 'model/login_credential_dto.dart'; | ||||||
| part 'model/login_response_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_response_dto.dart'; | ||||||
| part 'model/user_admin_update_dto.dart'; | part 'model/user_admin_update_dto.dart'; | ||||||
| part 'model/user_avatar_color.dart'; | part 'model/user_avatar_color.dart'; | ||||||
|  | part 'model/user_license.dart'; | ||||||
| part 'model/user_preferences_response_dto.dart'; | part 'model/user_preferences_response_dto.dart'; | ||||||
| part 'model/user_preferences_update_dto.dart'; | part 'model/user_preferences_update_dto.dart'; | ||||||
| part 'model/user_response_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]. |   /// Performs an HTTP 'GET /users/me/preferences' operation and returns the [Response]. | ||||||
|   Future<Response> getMyPreferencesWithHttpInfo() async { |   Future<Response> getMyPreferencesWithHttpInfo() async { | ||||||
|     // ignore: prefer_const_declarations |     // ignore: prefer_const_declarations | ||||||
| @@ -284,6 +317,47 @@ class UsersApi { | |||||||
|     return null; |     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]. |   /// Performs an HTTP 'GET /users' operation and returns the [Response]. | ||||||
|   Future<Response> searchUsersWithHttpInfo() async { |   Future<Response> searchUsersWithHttpInfo() async { | ||||||
|     // ignore: prefer_const_declarations |     // ignore: prefer_const_declarations | ||||||
| @@ -328,6 +402,53 @@ class UsersApi { | |||||||
|     return null; |     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]. |   /// Performs an HTTP 'PUT /users/me/preferences' operation and returns the [Response]. | ||||||
|   /// Parameters: |   /// 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); |           return LibraryResponseDto.fromJson(value); | ||||||
|         case 'LibraryStatsResponseDto': |         case 'LibraryStatsResponseDto': | ||||||
|           return LibraryStatsResponseDto.fromJson(value); |           return LibraryStatsResponseDto.fromJson(value); | ||||||
|  |         case 'LicenseKeyDto': | ||||||
|  |           return LicenseKeyDto.fromJson(value); | ||||||
|  |         case 'LicenseResponseDto': | ||||||
|  |           return LicenseResponseDto.fromJson(value); | ||||||
|         case 'LogLevel': |         case 'LogLevel': | ||||||
|           return LogLevelTypeTransformer().decode(value); |           return LogLevelTypeTransformer().decode(value); | ||||||
|         case 'LoginCredentialDto': |         case 'LoginCredentialDto': | ||||||
| @@ -556,6 +560,8 @@ class ApiClient { | |||||||
|           return UserAdminUpdateDto.fromJson(value); |           return UserAdminUpdateDto.fromJson(value); | ||||||
|         case 'UserAvatarColor': |         case 'UserAvatarColor': | ||||||
|           return UserAvatarColorTypeTransformer().decode(value); |           return UserAvatarColorTypeTransformer().decode(value); | ||||||
|  |         case 'UserLicense': | ||||||
|  |           return UserLicense.fromJson(value); | ||||||
|         case 'UserPreferencesResponseDto': |         case 'UserPreferencesResponseDto': | ||||||
|           return UserPreferencesResponseDto.fromJson(value); |           return UserPreferencesResponseDto.fromJson(value); | ||||||
|         case 'UserPreferencesUpdateDto': |         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.ffmpeg, | ||||||
|     this.imagemagick, |     this.imagemagick, | ||||||
|     this.libvips, |     this.libvips, | ||||||
|  |     required this.licensed, | ||||||
|     this.nodejs, |     this.nodejs, | ||||||
|     this.repository, |     this.repository, | ||||||
|     this.repositoryUrl, |     this.repositoryUrl, | ||||||
| @@ -95,6 +96,8 @@ class ServerAboutResponseDto { | |||||||
|   /// |   /// | ||||||
|   String? libvips; |   String? libvips; | ||||||
| 
 | 
 | ||||||
|  |   bool licensed; | ||||||
|  | 
 | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// 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 |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @@ -157,6 +160,7 @@ class ServerAboutResponseDto { | |||||||
|     other.ffmpeg == ffmpeg && |     other.ffmpeg == ffmpeg && | ||||||
|     other.imagemagick == imagemagick && |     other.imagemagick == imagemagick && | ||||||
|     other.libvips == libvips && |     other.libvips == libvips && | ||||||
|  |     other.licensed == licensed && | ||||||
|     other.nodejs == nodejs && |     other.nodejs == nodejs && | ||||||
|     other.repository == repository && |     other.repository == repository && | ||||||
|     other.repositoryUrl == repositoryUrl && |     other.repositoryUrl == repositoryUrl && | ||||||
| @@ -177,6 +181,7 @@ class ServerAboutResponseDto { | |||||||
|     (ffmpeg == null ? 0 : ffmpeg!.hashCode) + |     (ffmpeg == null ? 0 : ffmpeg!.hashCode) + | ||||||
|     (imagemagick == null ? 0 : imagemagick!.hashCode) + |     (imagemagick == null ? 0 : imagemagick!.hashCode) + | ||||||
|     (libvips == null ? 0 : libvips!.hashCode) + |     (libvips == null ? 0 : libvips!.hashCode) + | ||||||
|  |     (licensed.hashCode) + | ||||||
|     (nodejs == null ? 0 : nodejs!.hashCode) + |     (nodejs == null ? 0 : nodejs!.hashCode) + | ||||||
|     (repository == null ? 0 : repository!.hashCode) + |     (repository == null ? 0 : repository!.hashCode) + | ||||||
|     (repositoryUrl == null ? 0 : repositoryUrl!.hashCode) + |     (repositoryUrl == null ? 0 : repositoryUrl!.hashCode) + | ||||||
| @@ -187,7 +192,7 @@ class ServerAboutResponseDto { | |||||||
|     (versionUrl.hashCode); |     (versionUrl.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @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() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -231,6 +236,7 @@ class ServerAboutResponseDto { | |||||||
|     } else { |     } else { | ||||||
|     //  json[r'libvips'] = null; |     //  json[r'libvips'] = null; | ||||||
|     } |     } | ||||||
|  |       json[r'licensed'] = this.licensed; | ||||||
|     if (this.nodejs != null) { |     if (this.nodejs != null) { | ||||||
|       json[r'nodejs'] = this.nodejs; |       json[r'nodejs'] = this.nodejs; | ||||||
|     } else { |     } else { | ||||||
| @@ -282,6 +288,7 @@ class ServerAboutResponseDto { | |||||||
|         ffmpeg: mapValueOfType<String>(json, r'ffmpeg'), |         ffmpeg: mapValueOfType<String>(json, r'ffmpeg'), | ||||||
|         imagemagick: mapValueOfType<String>(json, r'imagemagick'), |         imagemagick: mapValueOfType<String>(json, r'imagemagick'), | ||||||
|         libvips: mapValueOfType<String>(json, r'libvips'), |         libvips: mapValueOfType<String>(json, r'libvips'), | ||||||
|  |         licensed: mapValueOfType<bool>(json, r'licensed')!, | ||||||
|         nodejs: mapValueOfType<String>(json, r'nodejs'), |         nodejs: mapValueOfType<String>(json, r'nodejs'), | ||||||
|         repository: mapValueOfType<String>(json, r'repository'), |         repository: mapValueOfType<String>(json, r'repository'), | ||||||
|         repositoryUrl: mapValueOfType<String>(json, r'repositoryUrl'), |         repositoryUrl: mapValueOfType<String>(json, r'repositoryUrl'), | ||||||
| @@ -337,6 +344,7 @@ class ServerAboutResponseDto { | |||||||
| 
 | 
 | ||||||
|   /// The list of required keys that must be present in a JSON. |   /// The list of required keys that must be present in a JSON. | ||||||
|   static const requiredKeys = <String>{ |   static const requiredKeys = <String>{ | ||||||
|  |     'licensed', | ||||||
|     'version', |     'version', | ||||||
|     'versionUrl', |     'versionUrl', | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ class UserAdminResponseDto { | |||||||
|     required this.email, |     required this.email, | ||||||
|     required this.id, |     required this.id, | ||||||
|     required this.isAdmin, |     required this.isAdmin, | ||||||
|  |     required this.license, | ||||||
|     required this.name, |     required this.name, | ||||||
|     required this.oauthId, |     required this.oauthId, | ||||||
|     required this.profileImagePath, |     required this.profileImagePath, | ||||||
| @@ -42,6 +43,8 @@ class UserAdminResponseDto { | |||||||
| 
 | 
 | ||||||
|   bool isAdmin; |   bool isAdmin; | ||||||
| 
 | 
 | ||||||
|  |   UserLicense? license; | ||||||
|  | 
 | ||||||
|   String name; |   String name; | ||||||
| 
 | 
 | ||||||
|   String oauthId; |   String oauthId; | ||||||
| @@ -68,6 +71,7 @@ class UserAdminResponseDto { | |||||||
|     other.email == email && |     other.email == email && | ||||||
|     other.id == id && |     other.id == id && | ||||||
|     other.isAdmin == isAdmin && |     other.isAdmin == isAdmin && | ||||||
|  |     other.license == license && | ||||||
|     other.name == name && |     other.name == name && | ||||||
|     other.oauthId == oauthId && |     other.oauthId == oauthId && | ||||||
|     other.profileImagePath == profileImagePath && |     other.profileImagePath == profileImagePath && | ||||||
| @@ -87,6 +91,7 @@ class UserAdminResponseDto { | |||||||
|     (email.hashCode) + |     (email.hashCode) + | ||||||
|     (id.hashCode) + |     (id.hashCode) + | ||||||
|     (isAdmin.hashCode) + |     (isAdmin.hashCode) + | ||||||
|  |     (license == null ? 0 : license!.hashCode) + | ||||||
|     (name.hashCode) + |     (name.hashCode) + | ||||||
|     (oauthId.hashCode) + |     (oauthId.hashCode) + | ||||||
|     (profileImagePath.hashCode) + |     (profileImagePath.hashCode) + | ||||||
| @@ -98,7 +103,7 @@ class UserAdminResponseDto { | |||||||
|     (updatedAt.hashCode); |     (updatedAt.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @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() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -112,6 +117,11 @@ class UserAdminResponseDto { | |||||||
|       json[r'email'] = this.email; |       json[r'email'] = this.email; | ||||||
|       json[r'id'] = this.id; |       json[r'id'] = this.id; | ||||||
|       json[r'isAdmin'] = this.isAdmin; |       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'name'] = this.name; | ||||||
|       json[r'oauthId'] = this.oauthId; |       json[r'oauthId'] = this.oauthId; | ||||||
|       json[r'profileImagePath'] = this.profileImagePath; |       json[r'profileImagePath'] = this.profileImagePath; | ||||||
| @@ -150,6 +160,7 @@ class UserAdminResponseDto { | |||||||
|         email: mapValueOfType<String>(json, r'email')!, |         email: mapValueOfType<String>(json, r'email')!, | ||||||
|         id: mapValueOfType<String>(json, r'id')!, |         id: mapValueOfType<String>(json, r'id')!, | ||||||
|         isAdmin: mapValueOfType<bool>(json, r'isAdmin')!, |         isAdmin: mapValueOfType<bool>(json, r'isAdmin')!, | ||||||
|  |         license: UserLicense.fromJson(json[r'license']), | ||||||
|         name: mapValueOfType<String>(json, r'name')!, |         name: mapValueOfType<String>(json, r'name')!, | ||||||
|         oauthId: mapValueOfType<String>(json, r'oauthId')!, |         oauthId: mapValueOfType<String>(json, r'oauthId')!, | ||||||
|         profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, |         profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, | ||||||
| @@ -212,6 +223,7 @@ class UserAdminResponseDto { | |||||||
|     'email', |     'email', | ||||||
|     'id', |     'id', | ||||||
|     'isAdmin', |     'isAdmin', | ||||||
|  |     'license', | ||||||
|     'name', |     'name', | ||||||
|     'oauthId', |     'oauthId', | ||||||
|     'profileImagePath', |     '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": { |     "/sessions": { | ||||||
|       "delete": { |       "delete": { | ||||||
|         "operationId": "deleteAllSessions", |         "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": { |     "/users/me/preferences": { | ||||||
|       "get": { |       "get": { | ||||||
|         "operationId": "getMyPreferences", |         "operationId": "getMyPreferences", | ||||||
| @@ -8765,6 +8955,43 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "object" |         "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": { |       "LogLevel": { | ||||||
|         "enum": [ |         "enum": [ | ||||||
|           "verbose", |           "verbose", | ||||||
| @@ -9752,6 +9979,9 @@ | |||||||
|           "libvips": { |           "libvips": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "licensed": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|           "nodejs": { |           "nodejs": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
| @@ -9778,6 +10008,7 @@ | |||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "required": [ |         "required": [ | ||||||
|  |           "licensed", | ||||||
|           "version", |           "version", | ||||||
|           "versionUrl" |           "versionUrl" | ||||||
|         ], |         ], | ||||||
| @@ -11330,6 +11561,14 @@ | |||||||
|           "isAdmin": { |           "isAdmin": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|           }, |           }, | ||||||
|  |           "license": { | ||||||
|  |             "allOf": [ | ||||||
|  |               { | ||||||
|  |                 "$ref": "#/components/schemas/UserLicense" | ||||||
|  |               } | ||||||
|  |             ], | ||||||
|  |             "nullable": true | ||||||
|  |           }, | ||||||
|           "name": { |           "name": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
| @@ -11371,6 +11610,7 @@ | |||||||
|           "email", |           "email", | ||||||
|           "id", |           "id", | ||||||
|           "isAdmin", |           "isAdmin", | ||||||
|  |           "license", | ||||||
|           "name", |           "name", | ||||||
|           "oauthId", |           "oauthId", | ||||||
|           "profileImagePath", |           "profileImagePath", | ||||||
| @@ -11425,6 +11665,26 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "string" |         "type": "string" | ||||||
|       }, |       }, | ||||||
|  |       "UserLicense": { | ||||||
|  |         "properties": { | ||||||
|  |           "activatedAt": { | ||||||
|  |             "format": "date-time", | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "activationKey": { | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "licenseKey": { | ||||||
|  |             "type": "string" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "required": [ | ||||||
|  |           "activatedAt", | ||||||
|  |           "activationKey", | ||||||
|  |           "licenseKey" | ||||||
|  |         ], | ||||||
|  |         "type": "object" | ||||||
|  |       }, | ||||||
|       "UserPreferencesResponseDto": { |       "UserPreferencesResponseDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "avatar": { |           "avatar": { | ||||||
|   | |||||||
| @@ -38,6 +38,11 @@ export type ActivityCreateDto = { | |||||||
| export type ActivityStatisticsResponseDto = { | export type ActivityStatisticsResponseDto = { | ||||||
|     comments: number; |     comments: number; | ||||||
| }; | }; | ||||||
|  | export type UserLicense = { | ||||||
|  |     activatedAt: string; | ||||||
|  |     activationKey: string; | ||||||
|  |     licenseKey: string; | ||||||
|  | }; | ||||||
| export type UserAdminResponseDto = { | export type UserAdminResponseDto = { | ||||||
|     avatarColor: UserAvatarColor; |     avatarColor: UserAvatarColor; | ||||||
|     createdAt: string; |     createdAt: string; | ||||||
| @@ -45,6 +50,7 @@ export type UserAdminResponseDto = { | |||||||
|     email: string; |     email: string; | ||||||
|     id: string; |     id: string; | ||||||
|     isAdmin: boolean; |     isAdmin: boolean; | ||||||
|  |     license: (UserLicense) | null; | ||||||
|     name: string; |     name: string; | ||||||
|     oauthId: string; |     oauthId: string; | ||||||
|     profileImagePath: string; |     profileImagePath: string; | ||||||
| @@ -800,6 +806,7 @@ export type ServerAboutResponseDto = { | |||||||
|     ffmpeg?: string; |     ffmpeg?: string; | ||||||
|     imagemagick?: string; |     imagemagick?: string; | ||||||
|     libvips?: string; |     libvips?: string; | ||||||
|  |     licensed: boolean; | ||||||
|     nodejs?: string; |     nodejs?: string; | ||||||
|     repository?: string; |     repository?: string; | ||||||
|     repositoryUrl?: string; |     repositoryUrl?: string; | ||||||
| @@ -873,6 +880,15 @@ export type ServerVersionResponseDto = { | |||||||
|     minor: number; |     minor: number; | ||||||
|     patch: number; |     patch: number; | ||||||
| }; | }; | ||||||
|  | export type LicenseKeyDto = { | ||||||
|  |     activationKey: string; | ||||||
|  |     licenseKey: string; | ||||||
|  | }; | ||||||
|  | export type LicenseResponseDto = { | ||||||
|  |     activatedAt: string; | ||||||
|  |     activationKey: string; | ||||||
|  |     licenseKey: string; | ||||||
|  | }; | ||||||
| export type SessionResponseDto = { | export type SessionResponseDto = { | ||||||
|     createdAt: string; |     createdAt: string; | ||||||
|     current: boolean; |     current: boolean; | ||||||
| @@ -2484,6 +2500,32 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) { | |||||||
|         ...opts |         ...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) { | export function deleteAllSessions(opts?: Oazapfts.RequestOpts) { | ||||||
|     return oazapfts.ok(oazapfts.fetchText("/sessions", { |     return oazapfts.ok(oazapfts.fetchText("/sessions", { | ||||||
|         ...opts, |         ...opts, | ||||||
| @@ -2892,6 +2934,32 @@ export function updateMyUser({ userUpdateMeDto }: { | |||||||
|         body: 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) { | export function getMyPreferences(opts?: Oazapfts.RequestOpts) { | ||||||
|     return oazapfts.ok(oazapfts.fetchJson<{ |     return oazapfts.ok(oazapfts.fetchJson<{ | ||||||
|         status: 200; |         status: 200; | ||||||
|   | |||||||
| @@ -361,7 +361,7 @@ export const immichAppConfig: ConfigModuleOptions = { | |||||||
|   envFilePath: '.env', |   envFilePath: '.env', | ||||||
|   isGlobal: true, |   isGlobal: true, | ||||||
|   validationSchema: Joi.object({ |   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() |     IMMICH_LOG_LEVEL: Joi.string() | ||||||
|       .optional() |       .optional() | ||||||
|       .valid(...Object.values(LogLevel)), |       .valid(...Object.values(LogLevel)), | ||||||
| @@ -441,3 +441,29 @@ export const getBuildMetadata = () => ({ | |||||||
|   sourceCommit: process.env.IMMICH_SOURCE_COMMIT, |   sourceCommit: process.env.IMMICH_SOURCE_COMMIT, | ||||||
|   sourceUrl: process.env.IMMICH_SOURCE_URL, |   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 { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; | ||||||
|  | import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; | ||||||
| import { | import { | ||||||
|   ServerAboutResponseDto, |   ServerAboutResponseDto, | ||||||
|   ServerConfigDto, |   ServerConfigDto, | ||||||
| @@ -79,4 +80,22 @@ export class ServerController { | |||||||
|   getSupportedMediaTypes(): ServerMediaTypesResponseDto { |   getSupportedMediaTypes(): ServerMediaTypesResponseDto { | ||||||
|     return this.service.getSupportedMediaTypes(); |     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 { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; | ||||||
| import { NextFunction, Response } from 'express'; | import { NextFunction, Response } from 'express'; | ||||||
| import { AuthDto } from 'src/dtos/auth.dto'; | import { AuthDto } from 'src/dtos/auth.dto'; | ||||||
|  | import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; | ||||||
| import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; | import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; | ||||||
| import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; | import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; | ||||||
| import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; | import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; | ||||||
| @@ -68,6 +69,24 @@ export class UserController { | |||||||
|     return this.service.updateMyPreferences(auth, dto); |     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') |   @Get(':id') | ||||||
|   @Authenticated() |   @Authenticated() | ||||||
|   getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> { |   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; |   imagemagick?: string; | ||||||
|   libvips?: string; |   libvips?: string; | ||||||
|   exiftool?: string; |   exiftool?: string; | ||||||
|  |  | ||||||
|  |   licensed!: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class ServerStorageResponseDto { | export class ServerStorageResponseDto { | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import { ApiProperty } from '@nestjs/swagger'; | import { ApiProperty } from '@nestjs/swagger'; | ||||||
| import { Transform } from 'class-transformer'; | import { Transform } from 'class-transformer'; | ||||||
| import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; | 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 { UserEntity, UserStatus } from 'src/entities/user.entity'; | ||||||
| import { getPreferences } from 'src/utils/preferences'; | 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 { | export class UserUpdateMeDto { | ||||||
|   @Optional() |   @Optional() | ||||||
| @@ -33,6 +33,12 @@ export class UserResponseDto { | |||||||
|   avatarColor!: UserAvatarColor; |   avatarColor!: UserAvatarColor; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export class UserLicense { | ||||||
|  |   licenseKey!: string; | ||||||
|  |   activationKey!: string; | ||||||
|  |   activatedAt!: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
| export const mapUser = (entity: UserEntity): UserResponseDto => { | export const mapUser = (entity: UserEntity): UserResponseDto => { | ||||||
|   return { |   return { | ||||||
|     id: entity.id, |     id: entity.id, | ||||||
| @@ -130,9 +136,13 @@ export class UserAdminResponseDto extends UserResponseDto { | |||||||
|   quotaUsageInBytes!: number | null; |   quotaUsageInBytes!: number | null; | ||||||
|   @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) |   @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) | ||||||
|   status!: string; |   status!: string; | ||||||
|  |   license!: UserLicense | null; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto { | export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto { | ||||||
|  |   const license = entity.metadata.find( | ||||||
|  |     (item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE, | ||||||
|  |   )?.value; | ||||||
|   return { |   return { | ||||||
|     ...mapUser(entity), |     ...mapUser(entity), | ||||||
|     storageLabel: entity.storageLabel, |     storageLabel: entity.storageLabel, | ||||||
| @@ -145,5 +155,6 @@ export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto { | |||||||
|     quotaSizeInBytes: entity.quotaSizeInBytes, |     quotaSizeInBytes: entity.quotaSizeInBytes, | ||||||
|     quotaUsageInBytes: entity.quotaUsageInBytes, |     quotaUsageInBytes: entity.quotaUsageInBytes, | ||||||
|     status: entity.status, |     status: entity.status, | ||||||
|  |     license: license ?? null, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ export enum SystemMetadataKey { | |||||||
|   ADMIN_ONBOARDING = 'admin-onboarding', |   ADMIN_ONBOARDING = 'admin-onboarding', | ||||||
|   SYSTEM_CONFIG = 'system-config', |   SYSTEM_CONFIG = 'system-config', | ||||||
|   VERSION_CHECK_STATE = 'version-check-state', |   VERSION_CHECK_STATE = 'version-check-state', | ||||||
|  |   LICENSE = 'license', | ||||||
| } | } | ||||||
|  |  | ||||||
| export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string }; | 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.ADMIN_ONBOARDING]: { isOnboarded: boolean }; | ||||||
|   [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>; |   [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>; | ||||||
|   [SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata; |   [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 { | export enum UserMetadataKey { | ||||||
|   PREFERENCES = 'preferences', |   PREFERENCES = 'preferences', | ||||||
|  |   LICENSE = 'license', | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> { | export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> { | ||||||
|   [UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>; |   [UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>; | ||||||
|  |   [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ export interface ICryptoRepository { | |||||||
|   randomUUID(): string; |   randomUUID(): string; | ||||||
|   hashFile(filePath: string | Buffer): Promise<Buffer>; |   hashFile(filePath: string | Buffer): Promise<Buffer>; | ||||||
|   hashSha256(data: string): string; |   hashSha256(data: string): string; | ||||||
|  |   verifySha256(data: string, encrypted: string, publicKey: string): boolean; | ||||||
|   hashSha1(data: string | Buffer): Buffer; |   hashSha1(data: string | Buffer): Buffer; | ||||||
|   hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>; |   hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>; | ||||||
|   compareBcrypt(data: string | Buffer, encrypted: string): boolean; |   compareBcrypt(data: string | Buffer, encrypted: string): boolean; | ||||||
|   | |||||||
| @@ -5,5 +5,6 @@ export const ISystemMetadataRepository = 'ISystemMetadataRepository'; | |||||||
| export interface ISystemMetadataRepository { | export interface ISystemMetadataRepository { | ||||||
|   get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>; |   get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>; | ||||||
|   set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>; |   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>; |   readFile(filename: string): Promise<string>; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ export interface IUserRepository { | |||||||
|   create(user: Partial<UserEntity>): Promise<UserEntity>; |   create(user: Partial<UserEntity>): Promise<UserEntity>; | ||||||
|   update(id: string, 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>; |   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>; |   delete(user: UserEntity, hard?: boolean): Promise<UserEntity>; | ||||||
|   updateUsage(id: string, delta: number): Promise<void>; |   updateUsage(id: string, delta: number): Promise<void>; | ||||||
|   syncUsage(id?: string): Promise<void>; |   syncUsage(id?: string): Promise<void>; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import { compareSync, hash } from 'bcrypt'; | 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 { createReadStream } from 'node:fs'; | ||||||
| import { ICryptoRepository } from 'src/interfaces/crypto.interface'; | import { ICryptoRepository } from 'src/interfaces/crypto.interface'; | ||||||
| import { Instrumentation } from 'src/utils/instrumentation'; | import { Instrumentation } from 'src/utils/instrumentation'; | ||||||
| @@ -28,6 +28,21 @@ export class CryptoRepository implements ICryptoRepository { | |||||||
|     return createHash('sha256').update(value).digest('base64'); |     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 { |   hashSha1(value: string | Buffer): Buffer { | ||||||
|     return createHash('sha1').update(value).digest(); |     return createHash('sha1').update(value).digest(); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ export class SystemMetadataRepository implements ISystemMetadataRepository { | |||||||
|     await this.repository.upsert({ key, value }, { conflictPaths: { key: true } }); |     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> { |   readFile(filename: string): Promise<string> { | ||||||
|     return readFile(filename, { encoding: 'utf8' }); |     return readFile(filename, { encoding: 'utf8' }); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; | |||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | import { DummyValue, GenerateSql } from 'src/decorators'; | ||||||
| import { AssetEntity } from 'src/entities/asset.entity'; | 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 { UserEntity } from 'src/entities/user.entity'; | ||||||
| import { | import { | ||||||
|   IUserRepository, |   IUserRepository, | ||||||
| @@ -89,13 +89,14 @@ export class UserRepository implements IUserRepository { | |||||||
|     return this.save({ ...user, id }); |     return this.save({ ...user, id }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async upsertMetadata<T extends UserMetadataKey.PREFERENCES>( |   async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { | ||||||
|     id: string, |  | ||||||
|     { key, value }: { key: T; value: UserMetadata[T] }, |  | ||||||
|   ) { |  | ||||||
|     await this.metadataRepository.upsert({ userId: id, key, value }, { conflictPaths: { userId: true, key: true } }); |     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> { |   async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> { | ||||||
|     return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user); |     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 { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||||
| import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | ||||||
| import { IStorageRepository } from 'src/interfaces/storage.interface'; | import { IStorageRepository } from 'src/interfaces/storage.interface'; | ||||||
| import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | ||||||
| import { IUserRepository } from 'src/interfaces/user.interface'; | import { IUserRepository } from 'src/interfaces/user.interface'; | ||||||
| import { ServerService } from 'src/services/server.service'; | import { ServerService } from 'src/services/server.service'; | ||||||
|  | import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; | ||||||
| import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; | import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; | ||||||
| import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; | import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; | ||||||
| import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; | import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; | ||||||
| @@ -18,6 +21,7 @@ describe(ServerService.name, () => { | |||||||
|   let serverInfoMock: Mocked<IServerInfoRepository>; |   let serverInfoMock: Mocked<IServerInfoRepository>; | ||||||
|   let systemMock: Mocked<ISystemMetadataRepository>; |   let systemMock: Mocked<ISystemMetadataRepository>; | ||||||
|   let loggerMock: Mocked<ILoggerRepository>; |   let loggerMock: Mocked<ILoggerRepository>; | ||||||
|  |   let cryptoMock: Mocked<ICryptoRepository>; | ||||||
|  |  | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|     storageMock = newStorageRepositoryMock(); |     storageMock = newStorageRepositoryMock(); | ||||||
| @@ -25,8 +29,9 @@ describe(ServerService.name, () => { | |||||||
|     serverInfoMock = newServerInfoRepositoryMock(); |     serverInfoMock = newServerInfoRepositoryMock(); | ||||||
|     systemMock = newSystemMetadataRepositoryMock(); |     systemMock = newSystemMetadataRepositoryMock(); | ||||||
|     loggerMock = newLoggerRepositoryMock(); |     loggerMock = newLoggerRepositoryMock(); | ||||||
|  |     cryptoMock = newCryptoRepositoryMock(); | ||||||
|  |  | ||||||
|     sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock); |     sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   it('should work', () => { |   it('should work', () => { | ||||||
| @@ -249,4 +254,33 @@ describe(ServerService.name, () => { | |||||||
|       expect(userMock.getUserStats).toHaveBeenCalled(); |       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 { BadRequestException, Inject, Injectable } from '@nestjs/common'; | ||||||
| import { getBuildMetadata } from 'src/config'; | import { getBuildMetadata, getServerLicensePublicKey } from 'src/config'; | ||||||
| import { serverVersion } from 'src/constants'; | import { serverVersion } from 'src/constants'; | ||||||
| import { StorageCore, StorageFolder } from 'src/cores/storage.core'; | import { StorageCore, StorageFolder } from 'src/cores/storage.core'; | ||||||
| import { SystemConfigCore } from 'src/cores/system-config.core'; | import { SystemConfigCore } from 'src/cores/system-config.core'; | ||||||
|  | import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; | ||||||
| import { | import { | ||||||
|   ServerAboutResponseDto, |   ServerAboutResponseDto, | ||||||
|   ServerConfigDto, |   ServerConfigDto, | ||||||
| @@ -14,6 +15,7 @@ import { | |||||||
|   UsageByUserDto, |   UsageByUserDto, | ||||||
| } from 'src/dtos/server.dto'; | } from 'src/dtos/server.dto'; | ||||||
| import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; | import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; | ||||||
|  | import { ICryptoRepository } from 'src/interfaces/crypto.interface'; | ||||||
| import { OnEvents } from 'src/interfaces/event.interface'; | import { OnEvents } from 'src/interfaces/event.interface'; | ||||||
| import { ILoggerRepository } from 'src/interfaces/logger.interface'; | import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||||
| import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | ||||||
| @@ -34,6 +36,7 @@ export class ServerService implements OnEvents { | |||||||
|     @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository, |     @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository, | ||||||
|     @Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository, |     @Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository, | ||||||
|     @Inject(ILoggerRepository) private logger: ILoggerRepository, |     @Inject(ILoggerRepository) private logger: ILoggerRepository, | ||||||
|  |     @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, | ||||||
|   ) { |   ) { | ||||||
|     this.logger.setContext(ServerService.name); |     this.logger.setContext(ServerService.name); | ||||||
|     this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); |     this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); | ||||||
| @@ -53,10 +56,12 @@ export class ServerService implements OnEvents { | |||||||
|     const version = `v${serverVersion.toString()}`; |     const version = `v${serverVersion.toString()}`; | ||||||
|     const buildMetadata = getBuildMetadata(); |     const buildMetadata = getBuildMetadata(); | ||||||
|     const buildVersions = await this.serverInfoRepository.getBuildVersions(); |     const buildVersions = await this.serverInfoRepository.getBuildVersions(); | ||||||
|  |     const licensed = await this.systemMetadataRepository.get(SystemMetadataKey.LICENSE); | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       version, |       version, | ||||||
|       versionUrl: `https://github.com/immich-app/immich/releases/tag/${version}`, |       versionUrl: `https://github.com/immich-app/immich/releases/tag/${version}`, | ||||||
|  |       licensed: !!licensed, | ||||||
|       ...buildMetadata, |       ...buildMetadata, | ||||||
|       ...buildVersions, |       ...buildVersions, | ||||||
|     }; |     }; | ||||||
| @@ -154,4 +159,36 @@ export class ServerService implements OnEvents { | |||||||
|       sidecar: Object.keys(mimeTypes.sidecar), |       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 { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; | ||||||
|  | import { UserMetadataKey } from 'src/entities/user-metadata.entity'; | ||||||
| import { UserEntity } from 'src/entities/user.entity'; | import { UserEntity } from 'src/entities/user.entity'; | ||||||
| import { IAlbumRepository } from 'src/interfaces/album.interface'; | import { IAlbumRepository } from 'src/interfaces/album.interface'; | ||||||
| import { IAssetStackRepository } from 'src/interfaces/asset-stack.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', () => { |   describe('handleUserSyncUsage', () => { | ||||||
|     it('should sync usage', async () => { |     it('should sync usage', async () => { | ||||||
|       await sut.handleUserSyncUsage(); |       await sut.handleUserSyncUsage(); | ||||||
|   | |||||||
| @@ -1,13 +1,15 @@ | |||||||
| import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | ||||||
| import { DateTime } from 'luxon'; | import { DateTime } from 'luxon'; | ||||||
|  | import { getClientLicensePublicKey } from 'src/config'; | ||||||
| import { SALT_ROUNDS } from 'src/constants'; | import { SALT_ROUNDS } from 'src/constants'; | ||||||
| import { StorageCore, StorageFolder } from 'src/cores/storage.core'; | import { StorageCore, StorageFolder } from 'src/cores/storage.core'; | ||||||
| import { SystemConfigCore } from 'src/cores/system-config.core'; | import { SystemConfigCore } from 'src/cores/system-config.core'; | ||||||
| import { AuthDto } from 'src/dtos/auth.dto'; | 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 { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; | ||||||
| import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; | import { mapUser, mapUserAdmin, UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; | ||||||
| import { UserMetadataKey } from 'src/entities/user-metadata.entity'; | import { UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity'; | ||||||
| import { UserEntity } from 'src/entities/user.entity'; | import { UserEntity } from 'src/entities/user.entity'; | ||||||
| import { IAlbumRepository } from 'src/interfaces/album.interface'; | import { IAlbumRepository } from 'src/interfaces/album.interface'; | ||||||
| import { IAssetStackRepository } from 'src/interfaces/asset-stack.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> { |   async handleUserSyncUsage(): Promise<JobStatus> { | ||||||
|     await this.userRepository.syncUsage(); |     await this.userRepository.syncUsage(); | ||||||
|     return JobStatus.SUCCESS; |     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 { AuthDto } from 'src/dtos/auth.dto'; | ||||||
| import { SessionEntity } from 'src/entities/session.entity'; | import { SessionEntity } from 'src/entities/session.entity'; | ||||||
| import { SharedLinkEntity } from 'src/entities/shared-link.entity'; | import { SharedLinkEntity } from 'src/entities/shared-link.entity'; | ||||||
|  | import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; | ||||||
| import { UserEntity } from 'src/entities/user.entity'; | import { UserEntity } from 'src/entities/user.entity'; | ||||||
|  |  | ||||||
| export const authStub = { | export const authStub = { | ||||||
| @@ -9,6 +10,7 @@ export const authStub = { | |||||||
|       id: 'admin_id', |       id: 'admin_id', | ||||||
|       email: 'admin@test.com', |       email: 'admin@test.com', | ||||||
|       isAdmin: true, |       isAdmin: true, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|   }), |   }), | ||||||
|   user1: Object.freeze<AuthDto>({ |   user1: Object.freeze<AuthDto>({ | ||||||
| @@ -16,6 +18,7 @@ export const authStub = { | |||||||
|       id: 'user-id', |       id: 'user-id', | ||||||
|       email: 'immich@test.com', |       email: 'immich@test.com', | ||||||
|       isAdmin: false, |       isAdmin: false, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|     session: { |     session: { | ||||||
|       id: 'token-id', |       id: 'token-id', | ||||||
| @@ -26,6 +29,7 @@ export const authStub = { | |||||||
|       id: 'user-2', |       id: 'user-2', | ||||||
|       email: 'user2@immich.app', |       email: 'user2@immich.app', | ||||||
|       isAdmin: false, |       isAdmin: false, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|     session: { |     session: { | ||||||
|       id: 'token-id', |       id: 'token-id', | ||||||
| @@ -36,6 +40,7 @@ export const authStub = { | |||||||
|       id: 'user-id', |       id: 'user-id', | ||||||
|       email: 'immich@test.com', |       email: 'immich@test.com', | ||||||
|       isAdmin: false, |       isAdmin: false, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|     session: { |     session: { | ||||||
|       id: 'token-id', |       id: 'token-id', | ||||||
| @@ -46,6 +51,7 @@ export const authStub = { | |||||||
|       id: 'admin_id', |       id: 'admin_id', | ||||||
|       email: 'admin@test.com', |       email: 'admin@test.com', | ||||||
|       isAdmin: true, |       isAdmin: true, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|     sharedLink: { |     sharedLink: { | ||||||
|       id: '123', |       id: '123', | ||||||
| @@ -60,6 +66,7 @@ export const authStub = { | |||||||
|       id: 'admin_id', |       id: 'admin_id', | ||||||
|       email: 'admin@test.com', |       email: 'admin@test.com', | ||||||
|       isAdmin: true, |       isAdmin: true, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|     sharedLink: { |     sharedLink: { | ||||||
|       id: '123', |       id: '123', | ||||||
| @@ -74,6 +81,7 @@ export const authStub = { | |||||||
|       id: 'admin_id', |       id: 'admin_id', | ||||||
|       email: 'admin@test.com', |       email: 'admin@test.com', | ||||||
|       isAdmin: true, |       isAdmin: true, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|     sharedLink: { |     sharedLink: { | ||||||
|       id: '123', |       id: '123', | ||||||
| @@ -87,6 +95,7 @@ export const authStub = { | |||||||
|       id: 'admin_id', |       id: 'admin_id', | ||||||
|       email: 'admin@test.com', |       email: 'admin@test.com', | ||||||
|       isAdmin: true, |       isAdmin: true, | ||||||
|  |       metadata: [] as UserMetadataEntity[], | ||||||
|     } as UserEntity, |     } as UserEntity, | ||||||
|     sharedLink: { |     sharedLink: { | ||||||
|       id: '123', |       id: '123', | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ export const newCryptoRepositoryMock = (): Mocked<ICryptoRepository> => { | |||||||
|     compareBcrypt: vitest.fn().mockReturnValue(true), |     compareBcrypt: vitest.fn().mockReturnValue(true), | ||||||
|     hashBcrypt: vitest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)), |     hashBcrypt: vitest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)), | ||||||
|     hashSha256: vitest.fn().mockImplementation((input) => `${input} (hashed)`), |     hashSha256: vitest.fn().mockImplementation((input) => `${input} (hashed)`), | ||||||
|  |     verifySha256: vitest.fn().mockImplementation(() => true), | ||||||
|     hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)), |     hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)), | ||||||
|     hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`), |     hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`), | ||||||
|     newPassword: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')), |     newPassword: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')), | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMet | |||||||
|   return { |   return { | ||||||
|     get: vitest.fn() as any, |     get: vitest.fn() as any, | ||||||
|     set: vitest.fn(), |     set: vitest.fn(), | ||||||
|  |     delete: vitest.fn(), | ||||||
|     readFile: vitest.fn(), |     readFile: vitest.fn(), | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -23,5 +23,6 @@ export const newUserRepositoryMock = (reset = true): Mocked<IUserRepository> => | |||||||
|     updateUsage: vitest.fn(), |     updateUsage: vitest.fn(), | ||||||
|     syncUsage: vitest.fn(), |     syncUsage: vitest.fn(), | ||||||
|     upsertMetadata: vitest.fn(), |     upsertMetadata: vitest.fn(), | ||||||
|  |     deleteMetadata: vitest.fn(), | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -26,4 +26,9 @@ export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({ | |||||||
|   shouldChangePassword: false, |   shouldChangePassword: false, | ||||||
|   status: UserStatus.Active, |   status: UserStatus.Active, | ||||||
|   storageLabel: null, |   storageLabel: null, | ||||||
|  |   license: { | ||||||
|  |     licenseKey: 'IMCL-license-key', | ||||||
|  |     activationKey: 'activation-key', | ||||||
|  |     activatedAt: new Date().toISOString(), | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user