From efa6efd200439d052cd17f0658e2d37c8e9d6e07 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Thu, 29 Feb 2024 19:35:37 +0100 Subject: [PATCH] feat(server,web): remove external path nonsense and make libraries admin-only (#7237) * remove external path * open-api * make sql * move library settings to admin panel * Add documentation * show external libraries only * fix library list * make user library settings look good * fix test * fix tests * fix tests * can pick user for library * fix tests * fix e2e * chore: make sql * Use unauth exception * delete user library list * cleanup * fix e2e * fix await lint * chore: remove unused code * chore: cleanup * revert docs * fix: is admin stuff * table alignment --------- Co-authored-by: Jason Rasmussen Co-authored-by: Alex Tran --- e2e/src/fixtures.ts | 2 - e2e/src/responses.ts | 1 - mobile/openapi/README.md | Bin 24959 -> 24957 bytes mobile/openapi/doc/CreateLibraryDto.md | Bin 708 -> 750 bytes mobile/openapi/doc/CreateUserDto.md | Bin 658 -> 611 bytes mobile/openapi/doc/LibraryApi.md | Bin 18679 -> 18820 bytes mobile/openapi/doc/PartnerResponseDto.md | Bin 1063 -> 1027 bytes mobile/openapi/doc/UpdateUserDto.md | Bin 887 -> 840 bytes mobile/openapi/doc/UserResponseDto.md | Bin 1017 -> 981 bytes mobile/openapi/lib/api/library_api.dart | Bin 13728 -> 13975 bytes .../openapi/lib/model/create_library_dto.dart | Bin 5627 -> 6294 bytes mobile/openapi/lib/model/create_user_dto.dart | Bin 5158 -> 4770 bytes .../lib/model/partner_response_dto.dart | Bin 8905 -> 8488 bytes mobile/openapi/lib/model/update_user_dto.dart | Bin 9561 -> 8824 bytes .../openapi/lib/model/user_response_dto.dart | Bin 8146 -> 7729 bytes .../openapi/test/create_library_dto_test.dart | Bin 1154 -> 1253 bytes mobile/openapi/test/create_user_dto_test.dart | Bin 1196 -> 1087 bytes mobile/openapi/test/library_api_test.dart | Bin 1708 -> 1726 bytes .../test/partner_response_dto_test.dart | Bin 2285 -> 2176 bytes mobile/openapi/test/update_user_dto_test.dart | Bin 1621 -> 1512 bytes .../openapi/test/user_response_dto_test.dart | Bin 2173 -> 2064 bytes open-api/immich-openapi-specs.json | 36 +- open-api/typescript-sdk/src/fetch-client.ts | 15 +- server/e2e/api/specs/asset.e2e-spec.ts | 12 +- server/e2e/api/specs/library.e2e-spec.ts | 185 +++---- server/e2e/client/user-api.ts | 9 +- .../jobs/specs/library-watcher.e2e-spec.ts | 4 - server/e2e/jobs/specs/library.e2e-spec.ts | 110 +---- server/src/domain/library/library.dto.ts | 40 +- .../domain/library/library.service.spec.ts | 110 +---- server/src/domain/library/library.service.ts | 49 +- .../domain/partner/partner.service.spec.ts | 2 - .../domain/repositories/library.repository.ts | 3 - .../search/dto/search-suggestion.dto.ts | 11 +- server/src/domain/user/dto/create-user.dto.ts | 4 - server/src/domain/user/dto/update-user.dto.ts | 4 - .../user/response-dto/user-response.dto.ts | 2 - server/src/domain/user/user.core.ts | 8 - .../immich/controllers/library.controller.ts | 12 +- server/src/infra/entities/user.entity.ts | 3 - .../1708425975121-RemoveExternalPath.ts | 13 + server/src/infra/sql/album.repository.sql | 13 - server/src/infra/sql/api.key.repository.sql | 1 - server/src/infra/sql/library.repository.sql | 4 - .../src/infra/sql/shared.link.repository.sql | 3 - server/src/infra/sql/user.repository.sql | 4 - .../src/infra/sql/user.token.repository.sql | 1 - server/test/fixtures/auth.stub.ts | 1 - server/test/fixtures/library.stub.ts | 20 +- server/test/fixtures/user.stub.ts | 44 -- .../repositories/library.repository.mock.ts | 3 - .../components/forms/edit-user-form.svelte | 19 +- .../forms/library-import-paths-form.svelte | 4 +- .../forms/library-user-picker-form.svelte | 54 +++ .../side-bar/admin-side-bar.svelte | 9 +- .../user-settings-page/library-list.svelte | 416 ---------------- .../user-profile-settings.svelte | 8 - .../user-settings-list.svelte | 5 - web/src/lib/constants.ts | 1 + .../admin/library-management/+page.svelte | 450 ++++++++++++++++++ .../routes/admin/library-management/+page.ts | 16 + .../routes/admin/user-management/+page.svelte | 13 +- web/src/test-data/factories/user-factory.ts | 1 - 63 files changed, 718 insertions(+), 1007 deletions(-) create mode 100644 server/src/infra/migrations/1708425975121-RemoveExternalPath.ts create mode 100644 web/src/lib/components/forms/library-user-picker-form.svelte delete mode 100644 web/src/lib/components/user-settings-page/library-list.svelte create mode 100644 web/src/routes/admin/library-management/+page.svelte create mode 100644 web/src/routes/admin/library-management/+page.ts 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 8d8cf234e8e03fef28bd3fb66abadf875e679231..b8548c79e60aba7c0bc38b2e5da2af7401dd1b58 100644 GIT binary patch delta 54 zcmV-60LlOV!U6rl0k9<@3qfpbOle|rVRC6@lkg!M17U1zlSm;wlkp)TlTjfQlQ1F$ MlMEprv*sb0KU13&H2?qr delta 47 zcmex+i1Gg+#to{9lkX|AP1aMCo$RM*%i@`rmOt50QHnb!GpQ)Cs4^4C-@HcgqCEh* CfD#%2 diff --git a/mobile/openapi/doc/CreateLibraryDto.md b/mobile/openapi/doc/CreateLibraryDto.md index 9e4859cee9f0ef884b512e3e701343740c808012..e0caf1c8a5b33023fc49601759777726e1172b3b 100644 GIT binary patch delta 19 bcmX@Y`i^zOJ4W{W^1Rd{&y>lh8Gix*Q>_S- delta 11 ScmaFIdW3buJI2WpOxFM(S_Iz! diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md index 716571752c3d44cc178df22a6f29c6ef213333e5..0dcc8eca1f678a7c8d0a1a995b6e36abdf314bff 100644 GIT binary patch delta 11 ScmbQl`j}wu^>|~H$^!;wIneoXYvF_y~)>^ zgeLbgaxzv-}abk0<;E0@8ks>LA)pulMVFR^+3jGX(`kwL~Cgw83S@U z(7~ED3LvTcf|AU9prx^sFW51jsa~7LCjlu-hh|F|nI0DZo8Bz*c6m dts2+lK(+YECrsx|KBp%%Szdy5^Imx?O#s%gX9@rS delta 262 zcmZpf%=mpGRwJ+&kUCOi27qsZjVjDjkj zd1?80lvZW}b^HK`(GxJImQu0%a74q^+6!KD2Qxp>ODisP6ixP8FOHzyUCdb)JY(6M) zgpmd0l*tE}B__|6;sSFvKh(@-nY>O-4I*_v!GiMhpki76>SOElt<#5B~^brjS=T7fi(($usD>#Uu;fmvme$QvOfyCLqi zn!K1#LmAn%xLg>(s;Y_4V_*xdpdQ-1QTiU^=8po47$@J*=G}aOV+!NsXsO$iW3~CX VfQCSwFK3MenIxmHwJ0RRtgdbR)n delta 156 zcmbQ9yC8c*1JC3wJR+0(h1mo$Q;WkhOENr4N(wyl((*MXuNC&2{D8@NaszM2WP8Q1 z$=ms+PHx~=lo>~GIK`~(SN1c0&o4*JyVq^gsxOtlJ6sF1Z ogk>kkv5HTQ65|4kPVVMjve`y<4&&qt?D3Pmcz8DROLoWt05D!TvH$=8 diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index ca4217dcfa03970eb470b3a6963c0d0830616e81..ef656ea2a3f2b326b14e52587fb47df9b11dd967 100644 GIT binary patch delta 205 zcmeyZJgo;A!c&gA9%iZJ%(GyM8&02ebv`Tzg` delta 41 zcmV+^0M`GOG5ag90|K)Z0<8qIGYDz{v%Lu~0h1gHzO(ELIs&ty4wnP7a}py4E-DS% diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index bfa26622f75a7cfc085998b17b53b891247426c2..f272842cbe45ea0cd74ab7f068326142e700bf8b 100644 GIT binary patch delta 48 zcmZ3cu}F2pOvcG>ocxpbGd69GVhUs0e2YbiakCklI^*O@_P-#h%_5wWnK!@Tv0?`R DpUMy` delta 279 zcmZ3ax=drkOh%s6ijvf#yu_S<#FC83eT;KNf=h}r^V01VkR>*&GDS1VAd4v2+M=o` zVliM;M^}+onvahy8 q3MCnt#d^r1lP7Y@q3YA&l-qoaeGxON=zDHOK4g9Bnw#UeUDyGUvSp3{ diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index aa96f764bd8be231f713072444355e2eec72c054..008e0c4f2673a6e20190ef23e857e4cf0bf8ca2f 100644 GIT binary patch delta 46 zcmV+}0MY-+MW{ls3Iek#0>S~a1q8tav+xM40<+c&djhj<4#EPnz7d`Vv!NJM36lsS E!K73Xpa1{> delta 324 zcmZ4CbkcPL7ZXovMM-K=USdu_VoAniMJCbB+)M`<`Ro;t#Wx>hKEon|ETUj*i>jiY zV;7S;x{AEgoE!yv1p@^u6vdMZxJ5QwaaS=(A#1QzLFR4V#CwD(ATv!tqa-7waP#4DTx01usWBLDyZ diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index 52024017872f23409063befd6d1b4b2e857596e3..8fc85b48680ea76ef84a6629062c5a1a357962f0 100644 GIT binary patch delta 42 zcmV+_0M-B5O87*up#ihF0jC4A)dr9WvyTwp0kfzQ`vJ4R6cqxqV;9;5vw|Jh2c}pO AV*mgE delta 284 zcmez2a?@+WLPo)ojLc%a)QXbSqP)bMfW(rF&9fQTGH(uJjb)t7&&R=oBIV3Fl~V>; zM8Vcp0hzz~6yIA$bz~8cioDXC90hv?0|hHo#Y+VEn52*u+o~Y*Hg6CTXPO)@%8BBL w$&BKXO32F8V-;)_FuXCjUqqf8$y<{ZB}6wL7rDiXYFL4cB8tG~sZx*G0sReU`v3p{ diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 9da67392bd788efb8cd54a93abebd706ab672851..d4e0bf07dd3b2b5a76188875a5f5daceed9e3154 100644 GIT binary patch delta 46 zcmV+}0MY-_Ke0Tp0Rpoj0<8hF=L1Xwvt0*<0<#?oLISgU45|XNv=0sjvj-OT2$K;X E!F-bsp#T5? delta 300 zcmdmJbIE=KBNI<*MM-K=USdu_VoAp2049;mj7)18`Ro;t#Wycz4ri7@7E!RZMOE>J zZ3>e*x{AEgoE!yv1p@^u6vdM_atUv4;B;b=Le^lbg3Q}|fO`#-Xl9y%MoC6yu^zJ2 zr~YsB11R5aC4Cl_7f) E0EBIA5&!@I diff --git a/mobile/openapi/test/create_library_dto_test.dart b/mobile/openapi/test/create_library_dto_test.dart index dea1d4e631c05f6a9fc0186d8856ff7d19cd7680..88911249e7b9938292d49d62d565eba276450c79 100644 GIT binary patch delta 35 jcmZqTe9E~&mSyq*W_I@c^1Rd{&y>k?nH1sd56qJQ+o=rE delta 11 ScmaFL*~Ga)mSu7SODX^u;RDA2 diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index 6614b54a625faa0db286e56eb2e1e987252fdb4b..9658c02c8a4b626bfd081e132ca8ee31114274cc 100644 GIT binary patch delta 12 TcmZ3(xu0W$7SrbGOrlHx9Q*^k delta 50 ucmdnbv4(Sl7SrVO%Px# delta 61 zcmdnTyM}keCnheR%%q~kqRiA{jmh;);>_u(C6f)9BUwE2(()(wuqaMm#moiaOk`7^ NoWLx!`7QHBCIFbx6#@VN diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart index d6b7769ab401b7632a53f9509172ba1efc036ef2..7fce31d5ebd4679146f4532520d6455ed004ba6d 100644 GIT binary patch delta 12 TcmaDW*dVx}jd}BD=3guTBm@O; delta 46 pcmZn=d@H!2jhQF4q9nB_FEJ+|u_R-10+S-DfFrBP<~HVEEC7`z5gq^l diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index aaacfa7cbd66ecac628899f40451dc01ef8188b4..10c506666dd48dd933d69e5fdf43af4795f6a954 100644 GIT binary patch delta 12 Tcmcc0^MZTBQl`y9%+AaJB4-3T delta 42 ncmaFCeU)d!QYN0%ijvf#yu_S<#FC83^H~&81U45kl`{hXX&(?9 diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index bef6c812e9c39679a863651bf7f94c82136b9a0b..d0fdf97e1295478c37c7d1e62a8620ea5232cf9e 100644 GIT binary patch delta 12 Tcmew>FhO8LE%WAA%#&FFBp(G8 delta 46 qcmbOr@K<0%Ei+GQMM-K=USdu_VoAp215Apj0vB0KHrFywW&r@D("/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)}