diff --git a/e2e/src/fixtures.ts b/e2e/src/fixtures.ts index 309ba6b939..6a1a1b3968 100644 --- a/e2e/src/fixtures.ts +++ b/e2e/src/fixtures.ts @@ -44,7 +44,6 @@ export const userDto = { email: signupDto.admin.email, password: signupDto.admin.password, storageLabel: 'admin', - externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -63,7 +62,6 @@ export const userDto = { email: createUserDto.user1.email, password: createUserDto.user1.password, storageLabel: null, - externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 5e6a01eda7..76e289ade2 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -65,7 +65,6 @@ export const signupResponseDto = { name: 'Immich Admin', email: 'admin@immich.cloud', storageLabel: 'admin', - externalPath: null, profileImagePath: '', // why? lol shouldChangePassword: true, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 8d8cf234e8..b8548c79e6 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/CreateLibraryDto.md b/mobile/openapi/doc/CreateLibraryDto.md index 9e4859cee9..e0caf1c8a5 100644 Binary files a/mobile/openapi/doc/CreateLibraryDto.md and b/mobile/openapi/doc/CreateLibraryDto.md differ diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md index 716571752c..0dcc8eca1f 100644 Binary files a/mobile/openapi/doc/CreateUserDto.md and b/mobile/openapi/doc/CreateUserDto.md differ diff --git a/mobile/openapi/doc/LibraryApi.md b/mobile/openapi/doc/LibraryApi.md index ecaa73d86b..8a204788bb 100644 Binary files a/mobile/openapi/doc/LibraryApi.md and b/mobile/openapi/doc/LibraryApi.md differ diff --git a/mobile/openapi/doc/PartnerResponseDto.md b/mobile/openapi/doc/PartnerResponseDto.md index 46d16e6b28..ce45b32594 100644 Binary files a/mobile/openapi/doc/PartnerResponseDto.md and b/mobile/openapi/doc/PartnerResponseDto.md differ diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index 8c0572d1d2..ef4ebf869b 100644 Binary files a/mobile/openapi/doc/UpdateUserDto.md and b/mobile/openapi/doc/UpdateUserDto.md differ diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index 4ea44bb0cb..700a5b849e 100644 Binary files a/mobile/openapi/doc/UserResponseDto.md and b/mobile/openapi/doc/UserResponseDto.md differ diff --git a/mobile/openapi/lib/api/library_api.dart b/mobile/openapi/lib/api/library_api.dart index 21cec23eba..befd0aeeff 100644 Binary files a/mobile/openapi/lib/api/library_api.dart and b/mobile/openapi/lib/api/library_api.dart differ diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index ca4217dcfa..ef656ea2a3 100644 Binary files a/mobile/openapi/lib/model/create_library_dto.dart and b/mobile/openapi/lib/model/create_library_dto.dart differ diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index bfa26622f7..f272842cbe 100644 Binary files a/mobile/openapi/lib/model/create_user_dto.dart and b/mobile/openapi/lib/model/create_user_dto.dart differ diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index aa96f764bd..008e0c4f26 100644 Binary files a/mobile/openapi/lib/model/partner_response_dto.dart and b/mobile/openapi/lib/model/partner_response_dto.dart differ diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index 5202401787..8fc85b4868 100644 Binary files a/mobile/openapi/lib/model/update_user_dto.dart and b/mobile/openapi/lib/model/update_user_dto.dart differ diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 9da67392bd..d4e0bf07dd 100644 Binary files a/mobile/openapi/lib/model/user_response_dto.dart and b/mobile/openapi/lib/model/user_response_dto.dart differ diff --git a/mobile/openapi/test/create_library_dto_test.dart b/mobile/openapi/test/create_library_dto_test.dart index dea1d4e631..88911249e7 100644 Binary files a/mobile/openapi/test/create_library_dto_test.dart and b/mobile/openapi/test/create_library_dto_test.dart differ diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index 6614b54a62..9658c02c8a 100644 Binary files a/mobile/openapi/test/create_user_dto_test.dart and b/mobile/openapi/test/create_user_dto_test.dart differ diff --git a/mobile/openapi/test/library_api_test.dart b/mobile/openapi/test/library_api_test.dart index 20abcffe70..21afeff544 100644 Binary files a/mobile/openapi/test/library_api_test.dart and b/mobile/openapi/test/library_api_test.dart differ diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart index d6b7769ab4..7fce31d5eb 100644 Binary files a/mobile/openapi/test/partner_response_dto_test.dart and b/mobile/openapi/test/partner_response_dto_test.dart differ diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index aaacfa7cbd..10c506666d 100644 Binary files a/mobile/openapi/test/update_user_dto_test.dart and b/mobile/openapi/test/update_user_dto_test.dart differ diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index bef6c812e9..d0fdf97e12 100644 Binary files a/mobile/openapi/test/user_response_dto_test.dart and b/mobile/openapi/test/user_response_dto_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5dab83da12..3092c6cc63 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3299,8 +3299,17 @@ }, "/library": { "get": { - "operationId": "getLibraries", - "parameters": [], + "operationId": "getAllLibraries", + "parameters": [ + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/LibraryType" + } + } + ], "responses": { "200": { "content": { @@ -3407,7 +3416,7 @@ ] }, "get": { - "operationId": "getLibraryInfo", + "operationId": "getLibrary", "parameters": [ { "name": "id", @@ -7592,6 +7601,10 @@ "name": { "type": "string" }, + "ownerId": { + "format": "uuid", + "type": "string" + }, "type": { "$ref": "#/components/schemas/LibraryType" } @@ -7648,10 +7661,6 @@ "email": { "type": "string" }, - "externalPath": { - "nullable": true, - "type": "string" - }, "memoriesEnabled": { "type": "boolean" }, @@ -8549,10 +8558,6 @@ "email": { "type": "string" }, - "externalPath": { - "nullable": true, - "type": "string" - }, "id": { "type": "string" }, @@ -8601,7 +8606,6 @@ "createdAt", "deletedAt", "email", - "externalPath", "id", "isAdmin", "name", @@ -10326,9 +10330,6 @@ "email": { "type": "string" }, - "externalPath": { - "type": "string" - }, "id": { "format": "uuid", "type": "string" @@ -10455,10 +10456,6 @@ "email": { "type": "string" }, - "externalPath": { - "nullable": true, - "type": "string" - }, "id": { "type": "string" }, @@ -10504,7 +10501,6 @@ "createdAt", "deletedAt", "email", - "externalPath", "id", "isAdmin", "name", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9b1319f9b5..ce768d518b 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -66,7 +66,6 @@ export type UserResponseDto = { createdAt: string; deletedAt: string | null; email: string; - externalPath: string | null; id: string; isAdmin: boolean; memoriesEnabled?: boolean; @@ -462,6 +461,7 @@ export type CreateLibraryDto = { isVisible?: boolean; isWatched?: boolean; name?: string; + ownerId?: string; "type": LibraryType; }; export type UpdateLibraryDto = { @@ -506,7 +506,6 @@ export type PartnerResponseDto = { createdAt: string; deletedAt: string | null; email: string; - externalPath: string | null; id: string; inTimeline?: boolean; isAdmin: boolean; @@ -950,7 +949,6 @@ export type UpdateTagDto = { }; export type CreateUserDto = { email: string; - externalPath?: string | null; memoriesEnabled?: boolean; name: string; password: string; @@ -960,7 +958,6 @@ export type CreateUserDto = { export type UpdateUserDto = { avatarColor?: UserAvatarColor; email?: string; - externalPath?: string; id: string; isAdmin?: boolean; memoriesEnabled?: boolean; @@ -1841,11 +1838,15 @@ export function sendJobCommand({ id, jobCommandDto }: { body: jobCommandDto }))); } -export function getLibraries(opts?: Oazapfts.RequestOpts) { +export function getAllLibraries({ $type }: { + $type?: LibraryType; +}, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: LibraryResponseDto[]; - }>("/library", { + }>(`/library${QS.query(QS.explode({ + "type": $type + }))}`, { ...opts })); } @@ -1869,7 +1870,7 @@ export function deleteLibrary({ id }: { method: "DELETE" })); } -export function getLibraryInfo({ id }: { +export function getLibrary({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index d869775c98..7484187182 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -41,6 +41,7 @@ describe(`${AssetController.name} (e2e)`, () => { let app: INestApplication; let server: any; let assetRepository: IAssetRepository; + let admin: LoginResponseDto; let user1: LoginResponseDto; let user2: LoginResponseDto; let userWithQuota: LoginResponseDto; @@ -72,7 +73,7 @@ describe(`${AssetController.name} (e2e)`, () => { await testApp.reset(); await api.authApi.adminSignUp(server); - const admin = await api.authApi.adminLogin(server); + admin = await api.authApi.adminLogin(server); await Promise.all([ api.userApi.create(server, admin.accessToken, userDto.user1), @@ -86,12 +87,7 @@ describe(`${AssetController.name} (e2e)`, () => { api.authApi.login(server, userDto.userWithQuota), ]); - const [user1Libraries, user2Libraries] = await Promise.all([ - api.libraryApi.getAll(server, user1.accessToken), - api.libraryApi.getAll(server, user2.accessToken), - ]); - - libraries = [...user1Libraries, ...user2Libraries]; + libraries = await api.libraryApi.getAll(server, admin.accessToken); }); beforeEach(async () => { @@ -615,7 +611,7 @@ describe(`${AssetController.name} (e2e)`, () => { it("should not upload to another user's library", async () => { const content = randomBytes(32); - const [library] = await api.libraryApi.getAll(server, user2.accessToken); + const [library] = await api.libraryApi.getAll(server, admin.accessToken); await api.assetApi.upload(server, user1.accessToken, 'example-image', { content }); const { body, status } = await request(server) diff --git a/server/e2e/api/specs/library.e2e-spec.ts b/server/e2e/api/specs/library.e2e-spec.ts index 5be9e3035b..edb0a9feb7 100644 --- a/server/e2e/api/specs/library.e2e-spec.ts +++ b/server/e2e/api/specs/library.e2e-spec.ts @@ -10,6 +10,7 @@ import { testApp } from '../utils'; describe(`${LibraryController.name} (e2e)`, () => { let server: any; let admin: LoginResponseDto; + let user: LoginResponseDto; beforeAll(async () => { const app = await testApp.create(); @@ -25,6 +26,9 @@ describe(`${LibraryController.name} (e2e)`, () => { await testApp.reset(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); + + await api.userApi.create(server, admin.accessToken, userDto.user1); + user = await api.authApi.login(server, userDto.user1); }); describe('GET /library', () => { @@ -39,18 +43,19 @@ describe(`${LibraryController.name} (e2e)`, () => { .get('/library') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body).toEqual([ - expect.objectContaining({ - ownerId: admin.userId, - type: LibraryType.UPLOAD, - name: 'Default Library', - refreshedAt: null, - assetCount: 0, - importPaths: [], - exclusionPatterns: [], - }), - ]); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ownerId: admin.userId, + type: LibraryType.UPLOAD, + name: 'Default Library', + refreshedAt: null, + assetCount: 0, + importPaths: [], + exclusionPatterns: [], + }), + ]), + ); }); }); @@ -61,6 +66,16 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(body).toEqual(errorStub.unauthorized); }); + it('should require admin authentication', async () => { + const { status, body } = await request(server) + .post('/library') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ type: LibraryType.EXTERNAL }); + + expect(status).toBe(403); + expect(body).toEqual(errorStub.forbidden); + }); + it('should create an external library with defaults', async () => { const { status, body } = await request(server) .post('/library') @@ -184,29 +199,6 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest('Upload libraries cannot have exclusion patterns')); }); - - it('should allow a non-admin to create a library', async () => { - await api.userApi.create(server, admin.accessToken, userDto.user1); - const user1 = await api.authApi.login(server, userDto.user1); - - const { status, body } = await request(server) - .post('/library') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ type: LibraryType.EXTERNAL }); - - expect(status).toBe(201); - expect(body).toEqual( - expect.objectContaining({ - ownerId: user1.userId, - type: LibraryType.EXTERNAL, - name: 'New External Library', - refreshedAt: null, - assetCount: 0, - importPaths: [], - exclusionPatterns: [], - }), - ); - }); }); describe('PUT /library/:id', () => { @@ -249,7 +241,6 @@ describe(`${LibraryController.name} (e2e)`, () => { }); it('should change the import paths', async () => { - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, IMMICH_TEST_ASSET_TEMP_PATH); const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) @@ -327,6 +318,14 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(body).toEqual(errorStub.unauthorized); }); + it('should require admin access', async () => { + const { status, body } = await request(server) + .get(`/library/${uuidStub.notFound}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(403); + expect(body).toEqual(errorStub.forbidden); + }); + it('should get library by id', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL }); @@ -347,27 +346,6 @@ describe(`${LibraryController.name} (e2e)`, () => { }), ); }); - - it("should not allow getting another user's library", async () => { - await Promise.all([ - api.userApi.create(server, admin.accessToken, userDto.user1), - api.userApi.create(server, admin.accessToken, userDto.user2), - ]); - - const [user1, user2] = await Promise.all([ - api.authApi.login(server, userDto.user1), - api.authApi.login(server, userDto.user2), - ]); - - const library = await api.libraryApi.create(server, user1.accessToken, { type: LibraryType.EXTERNAL }); - - const { status, body } = await request(server) - .get(`/library/${library.id}`) - .set('Authorization', `Bearer ${user2.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Not found or no library.read access')); - }); }); describe('DELETE /library/:id', () => { @@ -390,7 +368,7 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(body).toEqual(errorStub.noDeleteUploadLibrary); }); - it('should delete an empty library', async () => { + it('should delete an external library', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL }); const { status, body } = await request(server) @@ -401,7 +379,6 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(body).toEqual({}); const libraries = await api.libraryApi.getAll(server, admin.accessToken); - expect(libraries).toHaveLength(1); expect(libraries).not.toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -455,74 +432,42 @@ describe(`${LibraryController.name} (e2e)`, () => { library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL }); }); - it('should fail with no external path set', async () => { - const { status, body } = await request(server) - .post(`/library/${library.id}/validate`) - .set('Authorization', `Bearer ${admin.accessToken}`) - - .send({ importPaths: [] }); - - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('User has no external path set')); + it('should pass with no import paths', async () => { + const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] }); + expect(response.importPaths).toEqual([]); }); - describe('With external path set', () => { - beforeEach(async () => { - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, IMMICH_TEST_ASSET_TEMP_PATH); + it('should fail if path does not exist', async () => { + const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`; + + const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { + importPaths: [pathToTest], }); - it('should pass with no import paths', async () => { - const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] }); - expect(response.importPaths).toEqual([]); + expect(response.importPaths?.length).toEqual(1); + const pathResponse = response?.importPaths?.at(0); + + expect(pathResponse).toEqual({ + importPath: pathToTest, + isValid: false, + message: `Path does not exist (ENOENT)`, + }); + }); + + it('should fail if path is a file', async () => { + const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`; + + const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { + importPaths: [pathToTest], }); - it('should not allow paths outside of the external path', async () => { - const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/../`; - const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { - importPaths: [pathToTest], - }); - expect(response.importPaths?.length).toEqual(1); - const pathResponse = response?.importPaths?.at(0); + expect(response.importPaths?.length).toEqual(1); + const pathResponse = response?.importPaths?.at(0); - expect(pathResponse).toEqual({ - importPath: pathToTest, - isValid: false, - message: `Not contained in user's external path`, - }); - }); - - it('should fail if path does not exist', async () => { - const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`; - - const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { - importPaths: [pathToTest], - }); - - expect(response.importPaths?.length).toEqual(1); - const pathResponse = response?.importPaths?.at(0); - - expect(pathResponse).toEqual({ - importPath: pathToTest, - isValid: false, - message: `Path does not exist (ENOENT)`, - }); - }); - - it('should fail if path is a file', async () => { - const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`; - - const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { - importPaths: [pathToTest], - }); - - expect(response.importPaths?.length).toEqual(1); - const pathResponse = response?.importPaths?.at(0); - - expect(pathResponse).toEqual({ - importPath: pathToTest, - isValid: false, - message: `Path does not exist (ENOENT)`, - }); + expect(pathResponse).toEqual({ + importPath: pathToTest, + isValid: false, + message: `Path does not exist (ENOENT)`, }); }); }); diff --git a/server/e2e/client/user-api.ts b/server/e2e/client/user-api.ts index 9123b06219..c538db3a8f 100644 --- a/server/e2e/client/user-api.ts +++ b/server/e2e/client/user-api.ts @@ -26,7 +26,12 @@ export const userApi = { return body as UserResponseDto; }, - setExternalPath: async (server: any, accessToken: string, id: string, externalPath: string) => { - return await userApi.update(server, accessToken, { id, externalPath }); + delete: async (server: any, accessToken: string, id: string) => { + const { status, body } = await request(server).delete(`/user/${id}`).set('Authorization', `Bearer ${accessToken}`); + + expect(status).toBe(200); + expect(body).toMatchObject({ id, deletedAt: expect.any(String) }); + + return body as UserResponseDto; }, }; diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts index 0215a4976e..93f7163531 100644 --- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts +++ b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts @@ -30,8 +30,6 @@ describe(`Library watcher (e2e)`, () => { await restoreTempFolder(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); - - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); }); afterEach(async () => { @@ -205,8 +203,6 @@ describe(`Library watcher (e2e)`, () => { ], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true }); diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index cb19117668..33208fde29 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -40,8 +40,6 @@ describe(`${LibraryController.name} (e2e)`, () => { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); @@ -79,8 +77,6 @@ describe(`${LibraryController.name} (e2e)`, () => { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); @@ -118,16 +114,12 @@ describe(`${LibraryController.name} (e2e)`, () => { }); it('should scan external library with exclusion pattern', async () => { - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path'); - const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], exclusionPatterns: ['**/el_corcal*'], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); @@ -163,7 +155,6 @@ describe(`${LibraryController.name} (e2e)`, () => { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); @@ -190,39 +181,11 @@ describe(`${LibraryController.name} (e2e)`, () => { ); }); - it('should offline files outside of changed external path', async () => { - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], - }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/some/other/path'); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - isOffline: true, - originalFileName: 'el_torcal_rocks', - }), - expect.objectContaining({ - isOffline: true, - originalFileName: 'tanners_ridge', - }), - ]), - ); - }); - it('should scan new files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/silver_fir.jpg`, @@ -258,7 +221,6 @@ describe(`${LibraryController.name} (e2e)`, () => { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, @@ -305,7 +267,6 @@ describe(`${LibraryController.name} (e2e)`, () => { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, @@ -345,7 +306,6 @@ describe(`${LibraryController.name} (e2e)`, () => { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, @@ -387,72 +347,6 @@ describe(`${LibraryController.name} (e2e)`, () => { }); }); - describe('External path', () => { - let library: LibraryResponseDto; - - beforeEach(async () => { - library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], - }); - }); - - it('should not scan assets for user without external path', async () => { - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toEqual([]); - }); - - it("should not import assets outside of user's external path", async () => { - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path'); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(assets).toEqual([]); - }); - - it.each([`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_PATH}/albums/nature/`])( - 'should scan external library with external path %s', - async (externalPath: string) => { - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, externalPath); - - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: AssetType.IMAGE, - originalFileName: 'el_torcal_rocks', - libraryId: library.id, - resized: true, - exifInfo: expect.objectContaining({ - exifImageWidth: 512, - exifImageHeight: 341, - latitude: null, - longitude: null, - }), - }), - expect.objectContaining({ - type: AssetType.IMAGE, - originalFileName: 'silver_fir', - libraryId: library.id, - resized: true, - exifInfo: expect.objectContaining({ - exifImageWidth: 511, - exifImageHeight: 323, - latitude: null, - longitude: null, - }), - }), - ]), - ); - }, - ); - }); - it('should not scan an upload library', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.UPLOAD, @@ -484,7 +378,6 @@ describe(`${LibraryController.name} (e2e)`, () => { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); @@ -506,12 +399,11 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(assets).toEqual([]); }); - it('should not remvove online files', async () => { + it('should not remove online files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); diff --git a/server/src/domain/library/library.dto.ts b/server/src/domain/library/library.dto.ts index db6119d4de..b57d56e7b2 100644 --- a/server/src/domain/library/library.dto.ts +++ b/server/src/domain/library/library.dto.ts @@ -1,59 +1,62 @@ import { LibraryEntity, LibraryType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { ValidateUUID } from '../domain.util'; +import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { Optional, ValidateUUID } from '../domain.util'; export class CreateLibraryDto { @IsEnum(LibraryType) @ApiProperty({ enumName: 'LibraryType', enum: LibraryType }) type!: LibraryType; + @ValidateUUID({ optional: true }) + ownerId?: string; + @IsString() - @IsOptional() + @Optional() @IsNotEmpty() name?: string; - @IsOptional() + @Optional() @IsBoolean() isVisible?: boolean; - @IsOptional() + @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @ArrayUnique() @ArrayMaxSize(128) importPaths?: string[]; - @IsOptional() + @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @ArrayUnique() @ArrayMaxSize(128) exclusionPatterns?: string[]; - @IsOptional() + @Optional() @IsBoolean() isWatched?: boolean; } export class UpdateLibraryDto { - @IsOptional() + @Optional() @IsString() @IsNotEmpty() name?: string; - @IsOptional() + @Optional() @IsBoolean() isVisible?: boolean; - @IsOptional() + @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @ArrayUnique() @ArrayMaxSize(128) importPaths?: string[]; - @IsOptional() + @Optional() @IsNotEmpty({ each: true }) @IsString({ each: true }) @ArrayUnique() @@ -68,14 +71,14 @@ export class CrawlOptionsDto { } export class ValidateLibraryDto { - @IsOptional() + @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @ArrayUnique() @ArrayMaxSize(128) importPaths?: string[]; - @IsOptional() + @Optional() @IsNotEmpty({ each: true }) @IsString({ each: true }) @ArrayUnique() @@ -100,14 +103,21 @@ export class LibrarySearchDto { export class ScanLibraryDto { @IsBoolean() - @IsOptional() + @Optional() refreshModifiedFiles?: boolean; @IsBoolean() - @IsOptional() + @Optional() refreshAllFiles?: boolean = false; } +export class SearchLibraryDto { + @IsEnum(LibraryType) + @ApiProperty({ enumName: 'LibraryType', enum: LibraryType }) + @Optional() + type?: LibraryType; +} + export class LibraryResponseDto { id!: string; ownerId!: string; diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index cafe70b4d7..ba1dd8374b 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -140,24 +140,6 @@ describe(LibraryService.name, () => { }); describe('handleQueueAssetRefresh', () => { - it("should not queue assets outside of user's external path", async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, - }; - - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - storageMock.crawl.mockResolvedValue(['/data/user2/photo.jpg']); - assetMock.getByLibraryId.mockResolvedValue([]); - libraryMock.getOnlineAssetPaths.mockResolvedValue([]); - userMock.get.mockResolvedValue(userStub.externalPath1); - - await sut.handleQueueAssetRefresh(mockLibraryJob); - - expect(jobMock.queue.mock.calls).toEqual([]); - }); - it('should queue new assets', async () => { const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibrary1.id, @@ -168,8 +150,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']); assetMock.getByLibraryId.mockResolvedValue([]); - libraryMock.getOnlineAssetPaths.mockResolvedValue([]); - userMock.get.mockResolvedValue(userStub.externalPath1); + userMock.get.mockResolvedValue(userStub.admin); await sut.handleQueueAssetRefresh(mockLibraryJob); @@ -196,8 +177,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']); assetMock.getByLibraryId.mockResolvedValue([]); - libraryMock.getOnlineAssetPaths.mockResolvedValue([]); - userMock.get.mockResolvedValue(userStub.externalPath1); + userMock.get.mockResolvedValue(userStub.admin); await sut.handleQueueAssetRefresh(mockLibraryJob); @@ -214,45 +194,6 @@ describe(LibraryService.name, () => { ]); }); - it("should mark assets outside of the user's external path as offline", async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, - }; - - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']); - assetMock.getByLibraryId.mockResolvedValue([assetStub.external]); - libraryMock.getOnlineAssetPaths.mockResolvedValue([]); - userMock.get.mockResolvedValue(userStub.externalPath2); - - await sut.handleQueueAssetRefresh(mockLibraryJob); - - expect(assetMock.updateAll.mock.calls).toEqual([ - [ - [assetStub.external.id], - { - isOffline: true, - }, - ], - ]); - }); - - it('should not scan libraries owned by user without external path', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, - }; - - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - - userMock.get.mockResolvedValue(userStub.user1); - - await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(false); - }); - it('should not scan upload libraries', async () => { const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibrary1.id, @@ -287,7 +228,6 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); storageMock.crawl.mockResolvedValue([]); assetMock.getByLibraryId.mockResolvedValue([]); - libraryMock.getOnlineAssetPaths.mockResolvedValue([]); userMock.get.mockResolvedValue(userStub.externalPathRoot); await sut.handleQueueAssetRefresh(mockLibraryJob); @@ -303,7 +243,7 @@ describe(LibraryService.name, () => { let mockUser: UserEntity; beforeEach(() => { - mockUser = userStub.externalPath1; + mockUser = userStub.admin; userMock.get.mockResolvedValue(mockUser); storageMock.stat.mockResolvedValue({ @@ -780,26 +720,6 @@ describe(LibraryService.name, () => { }); }); - describe('getAllForUser', () => { - it('should return all libraries for user', async () => { - libraryMock.getAllByUserId.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]); - await expect(sut.getAllForUser(authStub.admin)).resolves.toEqual([ - expect.objectContaining({ - id: libraryStub.uploadLibrary1.id, - name: libraryStub.uploadLibrary1.name, - ownerId: libraryStub.uploadLibrary1.ownerId, - }), - expect.objectContaining({ - id: libraryStub.externalLibrary1.id, - name: libraryStub.externalLibrary1.name, - ownerId: libraryStub.externalLibrary1.ownerId, - }), - ]); - - expect(libraryMock.getAllByUserId).toHaveBeenCalledWith(authStub.admin.user.id); - }); - }); - describe('getStatistics', () => { it('should return library statistics', async () => { libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 }); @@ -1144,12 +1064,12 @@ describe(LibraryService.name, () => { storageMock.checkFileExists.mockResolvedValue(true); await expect( - sut.update(authStub.external1, authStub.external1.user.id, { importPaths: ['/data/user1/foo'] }), + sut.update(authStub.admin, authStub.admin.user.id, { importPaths: ['/data/user1/foo'] }), ).resolves.toEqual(mapLibrary(libraryStub.externalLibraryWithImportPaths1)); expect(libraryMock.update).toHaveBeenCalledWith( expect.objectContaining({ - id: authStub.external1.user.id, + id: authStub.admin.user.id, }), ); expect(storageMock.watch).toHaveBeenCalledWith( @@ -1584,26 +1504,6 @@ describe(LibraryService.name, () => { ]); }); - it('should error when no external path is set', async () => { - await expect( - sut.validate(authStub.admin, libraryStub.externalLibrary1.id, { importPaths: ['/photos'] }), - ).rejects.toBeInstanceOf(BadRequestException); - }); - - it('should detect when path is outside external path', async () => { - const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, { - importPaths: ['/data/user2'], - }); - - expect(result.importPaths).toEqual([ - { - importPath: '/data/user2', - isValid: false, - message: "Not contained in user's external path", - }, - ]); - }); - it('should detect when path does not exist', async () => { storageMock.stat.mockImplementation(() => { const error = { code: 'ENOENT' } as any; diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 6a5982abef..4d89126859 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -29,6 +29,7 @@ import { LibraryResponseDto, LibraryStatsResponseDto, ScanLibraryDto, + SearchLibraryDto, UpdateLibraryDto, ValidateLibraryDto, ValidateLibraryImportPathResponseDto, @@ -182,6 +183,7 @@ export class LibraryService extends EventEmitter { async getStatistics(auth: AuthDto, id: string): Promise { await this.access.requirePermission(auth, Permission.LIBRARY_READ, id); + return this.repository.getStatistics(id); } @@ -189,17 +191,18 @@ export class LibraryService extends EventEmitter { return this.repository.getCountForUser(auth.user.id); } - async getAllForUser(auth: AuthDto): Promise { - const libraries = await this.repository.getAllByUserId(auth.user.id); - return libraries.map((library) => mapLibrary(library)); - } - async get(auth: AuthDto, id: string): Promise { await this.access.requirePermission(auth, Permission.LIBRARY_READ, id); + const library = await this.findOrFail(id); return mapLibrary(library); } + async getAll(auth: AuthDto, dto: SearchLibraryDto): Promise { + const libraries = await this.repository.getAll(false, dto.type); + return libraries.map((library) => mapLibrary(library)); + } + async handleQueueCleanup(): Promise { this.logger.debug('Cleaning up any pending library deletions'); const pendingDeletion = await this.repository.getAllDeleted(); @@ -234,8 +237,14 @@ export class LibraryService extends EventEmitter { } } + let ownerId = auth.user.id; + + if (dto.ownerId) { + ownerId = dto.ownerId; + } + const library = await this.repository.create({ - ownerId: auth.user.id, + ownerId, name: dto.name, type: dto.type, importPaths: dto.importPaths ?? [], @@ -300,24 +309,11 @@ export class LibraryService extends EventEmitter { public async validate(auth: AuthDto, id: string, dto: ValidateLibraryDto): Promise { await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id); - if (!auth.user.externalPath) { - throw new BadRequestException('User has no external path set'); - } - const response = new ValidateLibraryResponseDto(); if (dto.importPaths) { response.importPaths = await Promise.all( dto.importPaths.map(async (importPath) => { - const normalizedPath = path.normalize(importPath); - - if (!this.isInExternalPath(normalizedPath, auth.user.externalPath)) { - const validation = new ValidateLibraryImportPathResponseDto(); - validation.importPath = importPath; - validation.message = `Not contained in user's external path`; - return validation; - } - return await this.validateImportPath(importPath); }), ); @@ -328,6 +324,7 @@ export class LibraryService extends EventEmitter { async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise { await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id); + const library = await this.repository.update({ id, ...dto }); if (dto.importPaths) { @@ -404,7 +401,7 @@ export class LibraryService extends EventEmitter { return true; } else { // File can't be accessed and does not already exist in db - throw new BadRequestException("Can't access file", { cause: error }); + throw new BadRequestException('Cannot access file', { cause: error }); } } @@ -591,12 +588,6 @@ export class LibraryService extends EventEmitter { return false; } - const user = await this.userRepository.get(library.ownerId, {}); - if (!user?.externalPath) { - this.logger.warn('User has no external path set, cannot refresh library'); - return false; - } - this.logger.verbose(`Refreshing library: ${job.id}`); const pathValidation = await Promise.all( @@ -618,11 +609,7 @@ export class LibraryService extends EventEmitter { exclusionPatterns: library.exclusionPatterns, }); - const crawledAssetPaths = rawPaths - // Normalize file paths. This is important to prevent security issues like path traversal - .map((filePath) => path.normalize(filePath)) - // Filter out paths that are not within the user's external path - .filter((assetPath) => this.isInExternalPath(assetPath, user.externalPath)) as string[]; + const crawledAssetPaths = rawPaths.map((filePath) => path.normalize(filePath)); this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`); const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]); diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index 0f9e173b70..2bc5f3ca90 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -18,7 +18,6 @@ const responseDto = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - externalPath: null, memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, quotaSizeInBytes: null, @@ -37,7 +36,6 @@ const responseDto = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - externalPath: null, memoriesEnabled: true, avatarColor: UserAvatarColor.PRIMARY, inTimeline: true, diff --git a/server/src/domain/repositories/library.repository.ts b/server/src/domain/repositories/library.repository.ts index 7ba6cd409f..395373bcc9 100644 --- a/server/src/domain/repositories/library.repository.ts +++ b/server/src/domain/repositories/library.repository.ts @@ -5,7 +5,6 @@ export const ILibraryRepository = 'ILibraryRepository'; export interface ILibraryRepository { getCountForUser(ownerId: string): Promise; - getAllByUserId(userId: string, type?: LibraryType): Promise; getAll(withDeleted?: boolean, type?: LibraryType): Promise; getAllDeleted(): Promise; get(id: string, withDeleted?: boolean): Promise; @@ -16,7 +15,5 @@ export interface ILibraryRepository { getUploadLibraryCount(ownerId: string): Promise; update(library: Partial): Promise; getStatistics(id: string): Promise; - getOnlineAssetPaths(id: string): Promise; getAssetIds(id: string, withDeleted?: boolean): Promise; - existsByName(name: string, withDeleted?: boolean): Promise; } diff --git a/server/src/domain/search/dto/search-suggestion.dto.ts b/server/src/domain/search/dto/search-suggestion.dto.ts index 36a7524587..824a1066c4 100644 --- a/server/src/domain/search/dto/search-suggestion.dto.ts +++ b/server/src/domain/search/dto/search-suggestion.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { Optional } from '../../domain.util'; export enum SearchSuggestionType { COUNTRY = 'country', @@ -16,18 +17,18 @@ export class SearchSuggestionRequestDto { type!: SearchSuggestionType; @IsString() - @IsOptional() + @Optional() country?: string; @IsString() - @IsOptional() + @Optional() state?: string; @IsString() - @IsOptional() + @Optional() make?: string; @IsString() - @IsOptional() + @Optional() model?: string; } diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts index 179974eae8..e6dbb81675 100644 --- a/server/src/domain/user/dto/create-user.dto.ts +++ b/server/src/domain/user/dto/create-user.dto.ts @@ -21,10 +21,6 @@ export class CreateUserDto { @Transform(toSanitized) storageLabel?: string | null; - @Optional({ nullable: true }) - @IsString() - externalPath?: string | null; - @Optional() @IsBoolean() memoriesEnabled?: boolean; diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts index 3c6977ff05..1cab11627b 100644 --- a/server/src/domain/user/dto/update-user.dto.ts +++ b/server/src/domain/user/dto/update-user.dto.ts @@ -25,10 +25,6 @@ export class UpdateUserDto { @Transform(toSanitized) storageLabel?: string; - @Optional() - @IsString() - externalPath?: string; - @IsNotEmpty() @IsUUID('4') @ApiProperty({ format: 'uuid' }) diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index 15800b9933..a82337945e 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -22,7 +22,6 @@ export class UserDto { export class UserResponseDto extends UserDto { storageLabel!: string | null; - externalPath!: string | null; shouldChangePassword!: boolean; isAdmin!: boolean; createdAt!: Date; @@ -50,7 +49,6 @@ export function mapUser(entity: UserEntity): UserResponseDto { return { ...mapSimpleUser(entity), storageLabel: entity.storageLabel, - externalPath: entity.externalPath, shouldChangePassword: entity.shouldChangePassword, isAdmin: entity.isAdmin, createdAt: entity.createdAt, diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index 691bc7de49..6134e97cea 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -1,6 +1,5 @@ import { LibraryType, UserEntity } from '@app/infra/entities'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import path from 'node:path'; import sanitize from 'sanitize-filename'; import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories'; import { UserResponseDto } from './response-dto'; @@ -42,7 +41,6 @@ export class UserCore { // Users can never update the isAdmin property. delete dto.isAdmin; delete dto.storageLabel; - delete dto.externalPath; } else if (dto.isAdmin && user.id !== id) { // Admin cannot create another admin. throw new BadRequestException('The server already has an admin'); @@ -70,12 +68,6 @@ export class UserCore { dto.storageLabel = null; } - if (dto.externalPath === '') { - dto.externalPath = null; - } else if (dto.externalPath) { - dto.externalPath = path.normalize(dto.externalPath); - } - return this.userRepository.update(id, dto); } diff --git a/server/src/immich/controllers/library.controller.ts b/server/src/immich/controllers/library.controller.ts index 173e632110..fb68b2626f 100644 --- a/server/src/immich/controllers/library.controller.ts +++ b/server/src/immich/controllers/library.controller.ts @@ -5,13 +5,14 @@ import { LibraryStatsResponseDto, LibraryResponseDto as ResponseDto, ScanLibraryDto, + SearchLibraryDto, UpdateLibraryDto as UpdateDto, ValidateLibraryDto, ValidateLibraryResponseDto, } from '@app/domain'; -import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated } from '../app.guard'; +import { AdminRoute, Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -19,12 +20,13 @@ import { UUIDParamDto } from './dto/uuid-param.dto'; @Controller('library') @Authenticated() @UseValidation() +@AdminRoute() export class LibraryController { constructor(private service: LibraryService) {} @Get() - getLibraries(@Auth() auth: AuthDto): Promise { - return this.service.getAllForUser(auth); + getAllLibraries(@Auth() auth: AuthDto, @Query() dto: SearchLibraryDto): Promise { + return this.service.getAll(auth, dto); } @Post() @@ -38,7 +40,7 @@ export class LibraryController { } @Get(':id') - getLibraryInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + getLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } diff --git a/server/src/infra/entities/user.entity.ts b/server/src/infra/entities/user.entity.ts index dbc563ee0a..c574595ea8 100644 --- a/server/src/infra/entities/user.entity.ts +++ b/server/src/infra/entities/user.entity.ts @@ -43,9 +43,6 @@ export class UserEntity { @Column({ type: 'varchar', unique: true, default: null }) storageLabel!: string | null; - @Column({ type: 'varchar', default: null }) - externalPath!: string | null; - @Column({ default: '', select: false }) password?: string; diff --git a/server/src/infra/migrations/1708425975121-RemoveExternalPath.ts b/server/src/infra/migrations/1708425975121-RemoveExternalPath.ts new file mode 100644 index 0000000000..6c43e351f9 --- /dev/null +++ b/server/src/infra/migrations/1708425975121-RemoveExternalPath.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveExternalPath1708425975121 implements MigrationInterface { + name = 'RemoveExternalPath1708425975121'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "externalPath"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "externalPath" character varying`); + } +} diff --git a/server/src/infra/sql/album.repository.sql b/server/src/infra/sql/album.repository.sql index 5e104bc1a2..3997dd1a22 100644 --- a/server/src/infra/sql/album.repository.sql +++ b/server/src/infra/sql/album.repository.sql @@ -21,7 +21,6 @@ FROM "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", @@ -37,7 +36,6 @@ FROM "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", - "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", @@ -97,7 +95,6 @@ SELECT "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", @@ -113,7 +110,6 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", - "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", @@ -155,7 +151,6 @@ SELECT "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", @@ -171,7 +166,6 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", - "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", @@ -285,7 +279,6 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", - "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", @@ -313,7 +306,6 @@ SELECT "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", @@ -358,7 +350,6 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", - "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", @@ -386,7 +377,6 @@ SELECT "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", @@ -468,7 +458,6 @@ SELECT "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", - "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", @@ -496,7 +485,6 @@ SELECT "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", @@ -559,7 +547,6 @@ SELECT "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/infra/sql/api.key.repository.sql index 78eab13b30..3f6b207ce1 100644 --- a/server/src/infra/sql/api.key.repository.sql +++ b/server/src/infra/sql/api.key.repository.sql @@ -15,7 +15,6 @@ FROM "APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin", "APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email", "APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel", - "APIKeyEntity__APIKeyEntity_user"."externalPath" AS "APIKeyEntity__APIKeyEntity_user_externalPath", "APIKeyEntity__APIKeyEntity_user"."oauthId" AS "APIKeyEntity__APIKeyEntity_user_oauthId", "APIKeyEntity__APIKeyEntity_user"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath", "APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword", diff --git a/server/src/infra/sql/library.repository.sql b/server/src/infra/sql/library.repository.sql index c791b2c8a5..433ab6fbac 100644 --- a/server/src/infra/sql/library.repository.sql +++ b/server/src/infra/sql/library.repository.sql @@ -23,7 +23,6 @@ FROM "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", @@ -139,7 +138,6 @@ SELECT "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", @@ -185,7 +183,6 @@ SELECT "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", @@ -225,7 +222,6 @@ SELECT "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/infra/sql/shared.link.repository.sql index 5f43210685..6cac1c44fc 100644 --- a/server/src/infra/sql/shared.link.repository.sql +++ b/server/src/infra/sql/shared.link.repository.sql @@ -150,7 +150,6 @@ FROM "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword", @@ -254,7 +253,6 @@ SELECT "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword", @@ -308,7 +306,6 @@ FROM "SharedLinkEntity__SharedLinkEntity_user"."isAdmin" AS "SharedLinkEntity__SharedLinkEntity_user_isAdmin", "SharedLinkEntity__SharedLinkEntity_user"."email" AS "SharedLinkEntity__SharedLinkEntity_user_email", "SharedLinkEntity__SharedLinkEntity_user"."storageLabel" AS "SharedLinkEntity__SharedLinkEntity_user_storageLabel", - "SharedLinkEntity__SharedLinkEntity_user"."externalPath" AS "SharedLinkEntity__SharedLinkEntity_user_externalPath", "SharedLinkEntity__SharedLinkEntity_user"."oauthId" AS "SharedLinkEntity__SharedLinkEntity_user_oauthId", "SharedLinkEntity__SharedLinkEntity_user"."profileImagePath" AS "SharedLinkEntity__SharedLinkEntity_user_profileImagePath", "SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword", diff --git a/server/src/infra/sql/user.repository.sql b/server/src/infra/sql/user.repository.sql index 29a2ee5301..e4c7d3a314 100644 --- a/server/src/infra/sql/user.repository.sql +++ b/server/src/infra/sql/user.repository.sql @@ -8,7 +8,6 @@ SELECT "UserEntity"."isAdmin" AS "UserEntity_isAdmin", "UserEntity"."email" AS "UserEntity_email", "UserEntity"."storageLabel" AS "UserEntity_storageLabel", - "UserEntity"."externalPath" AS "UserEntity_externalPath", "UserEntity"."oauthId" AS "UserEntity_oauthId", "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", @@ -55,7 +54,6 @@ SELECT "user"."isAdmin" AS "user_isAdmin", "user"."email" AS "user_email", "user"."storageLabel" AS "user_storageLabel", - "user"."externalPath" AS "user_externalPath", "user"."oauthId" AS "user_oauthId", "user"."profileImagePath" AS "user_profileImagePath", "user"."shouldChangePassword" AS "user_shouldChangePassword", @@ -79,7 +77,6 @@ SELECT "UserEntity"."isAdmin" AS "UserEntity_isAdmin", "UserEntity"."email" AS "UserEntity_email", "UserEntity"."storageLabel" AS "UserEntity_storageLabel", - "UserEntity"."externalPath" AS "UserEntity_externalPath", "UserEntity"."oauthId" AS "UserEntity_oauthId", "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", @@ -105,7 +102,6 @@ SELECT "UserEntity"."isAdmin" AS "UserEntity_isAdmin", "UserEntity"."email" AS "UserEntity_email", "UserEntity"."storageLabel" AS "UserEntity_storageLabel", - "UserEntity"."externalPath" AS "UserEntity_externalPath", "UserEntity"."oauthId" AS "UserEntity_oauthId", "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/infra/sql/user.token.repository.sql index 0f496504f5..b51e53106e 100644 --- a/server/src/infra/sql/user.token.repository.sql +++ b/server/src/infra/sql/user.token.repository.sql @@ -18,7 +18,6 @@ FROM "UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin", "UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email", "UserTokenEntity__UserTokenEntity_user"."storageLabel" AS "UserTokenEntity__UserTokenEntity_user_storageLabel", - "UserTokenEntity__UserTokenEntity_user"."externalPath" AS "UserTokenEntity__UserTokenEntity_user_externalPath", "UserTokenEntity__UserTokenEntity_user"."oauthId" AS "UserTokenEntity__UserTokenEntity_user_oauthId", "UserTokenEntity__UserTokenEntity_user"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath", "UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword", diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 232c1477f5..79993a4da2 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -52,7 +52,6 @@ export const authStub = { id: 'user-id', email: 'immich@test.com', isAdmin: false, - externalPath: '/data/user1', } as UserEntity, userToken: { id: 'token-id', diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts index 3a203ce87c..ec8e196835 100644 --- a/server/test/fixtures/library.stub.ts +++ b/server/test/fixtures/library.stub.ts @@ -20,8 +20,8 @@ export const libraryStub = { id: 'library-id', name: 'test_library', assets: [], - owner: userStub.externalPath1, - ownerId: 'user-id', + owner: userStub.admin, + ownerId: 'admin_id', type: LibraryType.EXTERNAL, importPaths: [], createdAt: new Date('2023-01-01'), @@ -34,8 +34,8 @@ export const libraryStub = { id: 'library-id2', name: 'test_library2', assets: [], - owner: userStub.externalPath1, - ownerId: 'user-id', + owner: userStub.admin, + ownerId: 'admin_id', type: LibraryType.EXTERNAL, importPaths: [], createdAt: new Date('2021-01-01'), @@ -48,8 +48,8 @@ export const libraryStub = { id: 'library-id-with-paths1', name: 'library-with-import-paths1', assets: [], - owner: userStub.externalPath1, - ownerId: 'user-id', + owner: userStub.admin, + ownerId: 'admin_id', type: LibraryType.EXTERNAL, importPaths: ['/foo', '/bar'], createdAt: new Date('2023-01-01'), @@ -62,8 +62,8 @@ export const libraryStub = { id: 'library-id-with-paths2', name: 'library-with-import-paths2', assets: [], - owner: userStub.externalPath1, - ownerId: 'user-id', + owner: userStub.admin, + ownerId: 'admin_id', type: LibraryType.EXTERNAL, importPaths: ['/xyz', '/asdf'], createdAt: new Date('2023-01-01'), @@ -76,7 +76,7 @@ export const libraryStub = { id: 'library-id', name: 'test_library', assets: [], - owner: userStub.externalPath1, + owner: userStub.admin, ownerId: 'user-id', type: LibraryType.EXTERNAL, importPaths: [], @@ -90,7 +90,7 @@ export const libraryStub = { id: 'library-id1337', name: 'importpath-exclusion-library1', assets: [], - owner: userStub.externalPath1, + owner: userStub.admin, ownerId: 'user-id', type: LibraryType.EXTERNAL, importPaths: ['/xyz', '/asdf'], diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 1b8a3b3fed..e0d9113c65 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -31,7 +31,6 @@ export const userStub = { password: 'admin_password', name: 'admin_name', storageLabel: 'admin', - externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -50,7 +49,6 @@ export const userStub = { password: 'immich_password', name: 'immich_name', storageLabel: null, - externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -69,7 +67,6 @@ export const userStub = { password: 'immich_password', name: 'immich_name', storageLabel: null, - externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -88,45 +85,6 @@ export const userStub = { password: 'immich_password', name: 'immich_name', storageLabel: 'label-1', - externalPath: null, - oauthId: '', - shouldChangePassword: false, - profileImagePath: '', - createdAt: new Date('2021-01-01'), - deletedAt: null, - updatedAt: new Date('2021-01-01'), - tags: [], - assets: [], - memoriesEnabled: true, - avatarColor: UserAvatarColor.PRIMARY, - quotaSizeInBytes: null, - quotaUsageInBytes: 0, - }), - externalPath1: Object.freeze({ - ...authStub.user1.user, - password: 'immich_password', - name: 'immich_name', - storageLabel: 'label-1', - externalPath: '/data/user1', - oauthId: '', - shouldChangePassword: false, - profileImagePath: '', - createdAt: new Date('2021-01-01'), - deletedAt: null, - updatedAt: new Date('2021-01-01'), - tags: [], - assets: [], - memoriesEnabled: true, - avatarColor: UserAvatarColor.PRIMARY, - quotaSizeInBytes: null, - quotaUsageInBytes: 0, - }), - externalPath2: Object.freeze({ - ...authStub.user1.user, - password: 'immich_password', - name: 'immich_name', - storageLabel: 'label-1', - externalPath: '/data/user2', oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -145,7 +103,6 @@ export const userStub = { password: 'immich_password', name: 'immich_name', storageLabel: 'label-1', - externalPath: '/', oauthId: '', shouldChangePassword: false, profileImagePath: '', @@ -164,7 +121,6 @@ export const userStub = { password: 'immich_password', name: 'immich_name', storageLabel: 'label-1', - externalPath: null, oauthId: '', shouldChangePassword: false, profileImagePath: '/path/to/profile.jpg', diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts index 264acc08ef..740f4c4837 100644 --- a/server/test/repositories/library.repository.mock.ts +++ b/server/test/repositories/library.repository.mock.ts @@ -4,7 +4,6 @@ export const newLibraryRepositoryMock = (): jest.Mocked => { return { get: jest.fn(), getCountForUser: jest.fn(), - getAllByUserId: jest.fn(), create: jest.fn(), delete: jest.fn(), softDelete: jest.fn(), @@ -12,9 +11,7 @@ export const newLibraryRepositoryMock = (): jest.Mocked => { getStatistics: jest.fn(), getDefaultUploadLibrary: jest.fn(), getUploadLibraryCount: jest.fn(), - getOnlineAssetPaths: jest.fn(), getAssetIds: jest.fn(), - existsByName: jest.fn(), getAllDeleted: jest.fn(), getAll: jest.fn(), }; diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index 4caa5d6ab1..f55d341a71 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -34,14 +34,13 @@ const editUser = async () => { try { - const { id, email, name, storageLabel, externalPath } = user; + const { id, email, name, storageLabel } = user; await updateUser({ updateUserDto: { id, email, name, storageLabel: storageLabel || '', - externalPath: externalPath || '', quotaSizeInBytes: quotaSize ? convertToBytes(Number(quotaSize), 'GiB') : null, }, }); @@ -126,22 +125,6 @@

-
- - - -

- Note: Absolute path of parent import directory. A user can only import files if they exist at or under this - path. -

-
- {#if error}

{error}

{/if} diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index 039ac8ea04..845cd9e2c4 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -243,8 +243,8 @@ addImportPath = true; }}>Add path + > +
diff --git a/web/src/lib/components/forms/library-user-picker-form.svelte b/web/src/lib/components/forms/library-user-picker-form.svelte new file mode 100644 index 0000000000..b3c70adb6d --- /dev/null +++ b/web/src/lib/components/forms/library-user-picker-form.svelte @@ -0,0 +1,54 @@ + + + handleCancel()}> +
+
+ +

Select library owner

+
+ +
handleSubmit()} autocomplete="off"> +

NOTE: This cannot be changed later!

+ + + +
+ + + +
+ +
+
diff --git a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte index 019063ef87..0d79ae9611 100644 --- a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte @@ -4,7 +4,7 @@ import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; import StatusBox from '$lib/components/shared-components/status-box.svelte'; import { AppRoute } from '$lib/constants'; - import { mdiAccountMultipleOutline, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js'; + import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js'; @@ -21,6 +21,13 @@ + + + diff --git a/web/src/lib/components/user-settings-page/library-list.svelte b/web/src/lib/components/user-settings-page/library-list.svelte deleted file mode 100644 index 440becde0b..0000000000 --- a/web/src/lib/components/user-settings-page/library-list.svelte +++ /dev/null @@ -1,416 +0,0 @@ - - -{#if confirmDeleteLibrary} - (confirmDeleteLibrary = null)} - /> -{/if} - -
-
- {#if libraries.length > 0} - - - - - - - - - - - {#each libraries as library, index (library.id)} - - - - - {#if totalCount[index] == undefined} - - {:else} - - - {/if} - - - - {#if renameLibrary === index} -
- handleUpdate(detail)} - on:cancel={() => (renameLibrary = null)} - /> -
- {/if} - {#if editImportPaths === index} -
- handleUpdate(detail)} - on:cancel={() => (editImportPaths = null)} - /> -
- {/if} - {#if editScanSettings === index} -
- handleUpdate(detail.library)} - on:cancel={() => (editScanSettings = null)} - /> -
- {/if} - {/each} - -
TypeNameAssetsSize -
- {#if library.type === LibraryType.External} - - {:else if library.type === LibraryType.Upload} - - {/if}{library.name} - - - {totalCount[index].toLocaleString($locale)} - {diskUsage[index]} {diskUsageUnit[index]} - - - {#if showContextMenu} - - - - - {#if selectedLibrary && selectedLibrary.type === LibraryType.External} - - -
- - - -
- - -

Delete library

-
- {/if} -
-
- {/if} -
- {/if} -
- - -
-
-
diff --git a/web/src/lib/components/user-settings-page/user-profile-settings.svelte b/web/src/lib/components/user-settings-page/user-profile-settings.svelte index f24f0ac7f6..053d852dda 100644 --- a/web/src/lib/components/user-settings-page/user-profile-settings.svelte +++ b/web/src/lib/components/user-settings-page/user-profile-settings.svelte @@ -66,14 +66,6 @@ required={false} /> - -
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index a14c2a8c85..b46768c3e7 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -9,7 +9,6 @@ import AppearanceSettings from './appearance-settings.svelte'; import ChangePasswordSettings from './change-password-settings.svelte'; import DeviceList from './device-list.svelte'; - import LibraryList from './library-list.svelte'; import MemoriesSettings from './memories-settings.svelte'; import OAuthSettings from './oauth-settings.svelte'; import PartnerSettings from './partner-settings.svelte'; @@ -43,10 +42,6 @@ - - - - diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index dbb60df3e9..af5558c261 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -11,6 +11,7 @@ export enum AssetAction { export enum AppRoute { ADMIN_USER_MANAGEMENT = '/admin/user-management', + ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management', ADMIN_SETTINGS = '/admin/system-settings', ADMIN_STATS = '/admin/server-status', ADMIN_JOBS = '/admin/jobs-status', diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte new file mode 100644 index 0000000000..e9ac41b83e --- /dev/null +++ b/web/src/routes/admin/library-management/+page.svelte @@ -0,0 +1,450 @@ + + +{#if confirmDeleteLibrary} + (confirmDeleteLibrary = null)} + /> +{/if} + +{#if toCreateLibrary} + handleCreate(detail.ownerId)} + on:cancel={() => (toCreateLibrary = false)} + /> +{/if} + + +
+
+ {#if libraries.length > 0} + + + + + + + + + + + + {#each libraries as library, index (library.id)} + + + + + + + {#if totalCount[index] == undefined} + + {:else} + + + {/if} + + + + {#if renameLibrary === index} +
+ handleUpdate(detail)} + on:cancel={() => (renameLibrary = null)} + /> +
+ {/if} + {#if editImportPaths === index} +
+ handleUpdate(detail)} + on:cancel={() => (editImportPaths = null)} + /> +
+ {/if} + {#if editScanSettings === index} +
+ handleUpdate(detail.library)} + on:cancel={() => (editScanSettings = null)} + /> +
+ {/if} + {/each} + +
TypeNameOwnerAssetsSize +
+ {#if library.type === LibraryType.External} + + {:else if library.type === LibraryType.Upload} + + {/if}{library.name} + {#if owner[index] == undefined} + + {:else}{owner[index].name}{/if} + + + + {totalCount[index]} + {diskUsage[index]} {diskUsageUnit[index]} + + + {#if showContextMenu} + + onMenuExit()}> + onRenameClicked()} text={`Rename`} /> + + {#if selectedLibrary && selectedLibrary.type === LibraryType.External} + onEditImportPathClicked()} text="Edit Import Paths" /> + onScanSettingClicked()} text="Scan Settings" /> +
+ onScanNewLibraryClicked()} text="Scan New Library Files" /> + onScanAllLibraryFilesClicked()} + text="Re-scan All Library Files" + subtitle={'Only refreshes modified files'} + /> + onForceScanAllLibraryFilesClicked()} + text="Force Re-scan All Library Files" + subtitle={'Refreshes every file'} + /> +
+ onRemoveOfflineFilesClicked()} text="Remove Offline Files" /> + onDeleteLibraryClicked()}> +

Delete library

+
+ {/if} +
+
+ {/if} +
+ {/if} +
+ + +
+
+
+
diff --git a/web/src/routes/admin/library-management/+page.ts b/web/src/routes/admin/library-management/+page.ts new file mode 100644 index 0000000000..78831d38c7 --- /dev/null +++ b/web/src/routes/admin/library-management/+page.ts @@ -0,0 +1,16 @@ +import { authenticate, requestServerInfo } from '$lib/utils/auth'; +import { getAllUsers } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async () => { + await authenticate({ admin: true }); + await requestServerInfo(); + const allUsers = await getAllUsers({ isAll: false }); + + return { + allUsers, + meta: { + title: 'External Library Management', + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index a3a93e0f35..84917d591e 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -12,7 +12,7 @@ import { user } from '$lib/stores/user.store'; import { asByteUnitString } from '$lib/utils/byte-units'; import { getAllUsers, type UserResponseDto } from '@immich/sdk'; - import { mdiCheck, mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; + import { mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { onMount } from 'svelte'; import type { PageData } from './$types'; @@ -175,7 +175,6 @@ Email Name Has quota - Can import Action @@ -204,16 +203,6 @@ {/if}
- -
- {#if immichUser.externalPath} - - {:else} - - {/if} -
- - {#if !isDeleted(immichUser)}