From 171b6bb0a6571262420c0bd3789d66267bc097e1 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 19 Apr 2024 20:36:15 -0400 Subject: [PATCH] refactor: system metadata (#8923) refactor(server): system metadata --- e2e/src/api/specs/server-info.e2e-spec.ts | 17 +- e2e/src/api/specs/system-metadata.e2e-spec.ts | 76 +++++++++ e2e/src/utils.ts | 7 +- mobile/openapi/.openapi-generator/FILES | 9 ++ mobile/openapi/README.md | Bin 26365 -> 26826 bytes .../openapi/doc/AdminOnboardingUpdateDto.md | Bin 0 -> 423 bytes .../doc/ReverseGeocodingStateResponseDto.md | Bin 0 -> 474 bytes mobile/openapi/doc/ServerInfoApi.md | Bin 11069 -> 9228 bytes mobile/openapi/doc/SystemMetadataApi.md | Bin 0 -> 6367 bytes mobile/openapi/lib/api.dart | Bin 9114 -> 9254 bytes mobile/openapi/lib/api/server_info_api.dart | Bin 12548 -> 11619 bytes .../openapi/lib/api/system_metadata_api.dart | Bin 0 -> 4697 bytes mobile/openapi/lib/api_client.dart | Bin 25021 -> 25237 bytes .../model/admin_onboarding_update_dto.dart | Bin 0 -> 2989 bytes .../reverse_geocoding_state_response_dto.dart | Bin 0 -> 3748 bytes .../admin_onboarding_update_dto_test.dart | Bin 0 -> 600 bytes ...rse_geocoding_state_response_dto_test.dart | Bin 0 -> 745 bytes mobile/openapi/test/server_info_api_test.dart | Bin 1583 -> 1473 bytes .../test/system_metadata_api_test.dart | Bin 0 -> 936 bytes open-api/immich-openapi-specs.json | 150 +++++++++++++++--- open-api/typescript-sdk/src/fetch-client.ts | 38 ++++- server/src/controllers/index.ts | 2 + .../src/controllers/server-info.controller.ts | 9 +- .../controllers/system-metadata.controller.ts | 28 ++++ server/src/dtos/system-metadata.dto.ts | 15 ++ .../src/repositories/metadata.repository.ts | 2 +- server/src/services/index.ts | 2 + .../src/services/server-info.service.spec.ts | 8 - server/src/services/server-info.service.ts | 8 +- .../services/system-metadata.service.spec.ts | 31 ++++ .../src/services/system-metadata.service.ts | 29 ++++ web/src/routes/auth/onboarding/+page.svelte | 4 +- 32 files changed, 362 insertions(+), 73 deletions(-) create mode 100644 e2e/src/api/specs/system-metadata.e2e-spec.ts create mode 100644 mobile/openapi/doc/AdminOnboardingUpdateDto.md create mode 100644 mobile/openapi/doc/ReverseGeocodingStateResponseDto.md create mode 100644 mobile/openapi/doc/SystemMetadataApi.md create mode 100644 mobile/openapi/lib/api/system_metadata_api.dart create mode 100644 mobile/openapi/lib/model/admin_onboarding_update_dto.dart create mode 100644 mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart create mode 100644 mobile/openapi/test/admin_onboarding_update_dto_test.dart create mode 100644 mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart create mode 100644 mobile/openapi/test/system_metadata_api_test.dart create mode 100644 server/src/controllers/system-metadata.controller.ts create mode 100644 server/src/dtos/system-metadata.dto.ts create mode 100644 server/src/services/system-metadata.service.spec.ts create mode 100644 server/src/services/system-metadata.service.ts diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index 5cfd6a8b9..690bfae74 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { LoginResponseDto } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, utils } from 'src/utils'; @@ -162,19 +162,4 @@ describe('/server-info', () => { }); }); }); - - describe('POST /server-info/admin-onboarding', () => { - it('should set admin onboarding', async () => { - const config = await getServerConfig({}); - expect(config.isOnboarded).toBe(false); - - const { status } = await request(app) - .post('/server-info/admin-onboarding') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(204); - - const newConfig = await getServerConfig({}); - expect(newConfig.isOnboarded).toBe(true); - }); - }); }); diff --git a/e2e/src/api/specs/system-metadata.e2e-spec.ts b/e2e/src/api/specs/system-metadata.e2e-spec.ts new file mode 100644 index 000000000..bd17bf252 --- /dev/null +++ b/e2e/src/api/specs/system-metadata.e2e-spec.ts @@ -0,0 +1,76 @@ +import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { createUserDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('/server-info', () => { + let admin: LoginResponseDto; + let nonAdmin: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); + }); + + describe('POST /system-metadata/admin-onboarding', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should set admin onboarding', async () => { + const config = await getServerConfig({}); + expect(config.isOnboarded).toBe(false); + + const { status } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(204); + + const newConfig = await getServerConfig({}); + expect(newConfig.isOnboarded).toBe(true); + }); + }); + + describe('GET /system-metadata/reverse-geocoding-state', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should get the reverse geocoding state', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + lastUpdate: expect.any(String), + lastImportFileName: 'cities500.txt', + }); + }); + }); +}); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 004750202..96994c7f0 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -24,8 +24,8 @@ import { getConfigDefaults, login, searchMetadata, - setAdminOnboarding, signUpAdmin, + updateAdminOnboarding, updateConfig, validate, } from '@immich/sdk'; @@ -264,7 +264,10 @@ export const utils = { await signUpAdmin({ signUpDto: signupDto.admin }); const response = await login({ loginCredentialDto: loginDto.admin }); if (options.onboarding) { - await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) }); + await updateAdminOnboarding( + { adminOnboardingUpdateDto: { isOnboarded: true } }, + { headers: asBearerAuth(response.accessToken) }, + ); } return response; }, diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 42f1034dc..64229329a 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -13,6 +13,7 @@ doc/ActivityCreateDto.md doc/ActivityResponseDto.md doc/ActivityStatisticsResponseDto.md doc/AddUsersDto.md +doc/AdminOnboardingUpdateDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md @@ -123,6 +124,7 @@ doc/QueueStatusDto.md doc/ReactionLevel.md doc/ReactionType.md doc/RecognitionConfig.md +doc/ReverseGeocodingStateResponseDto.md doc/ScanLibraryDto.md doc/SearchAlbumResponseDto.md doc/SearchApi.md @@ -174,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md doc/SystemConfigTrashDto.md doc/SystemConfigUserDto.md +doc/SystemMetadataApi.md doc/TagApi.md doc/TagResponseDto.md doc/TagTypeEnum.md @@ -226,6 +229,7 @@ lib/api/sessions_api.dart lib/api/shared_link_api.dart lib/api/sync_api.dart lib/api/system_config_api.dart +lib/api/system_metadata_api.dart lib/api/tag_api.dart lib/api/timeline_api.dart lib/api/trash_api.dart @@ -242,6 +246,7 @@ lib/model/activity_create_dto.dart lib/model/activity_response_dto.dart lib/model/activity_statistics_response_dto.dart lib/model/add_users_dto.dart +lib/model/admin_onboarding_update_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/all_job_status_response_dto.dart @@ -343,6 +348,7 @@ lib/model/queue_status_dto.dart lib/model/reaction_level.dart lib/model/reaction_type.dart lib/model/recognition_config.dart +lib/model/reverse_geocoding_state_response_dto.dart lib/model/scan_library_dto.dart lib/model/search_album_response_dto.dart lib/model/search_asset_response_dto.dart @@ -419,6 +425,7 @@ test/activity_create_dto_test.dart test/activity_response_dto_test.dart test/activity_statistics_response_dto_test.dart test/add_users_dto_test.dart +test/admin_onboarding_update_dto_test.dart test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart @@ -534,6 +541,7 @@ test/queue_status_dto_test.dart test/reaction_level_test.dart test/reaction_type_test.dart test/recognition_config_test.dart +test/reverse_geocoding_state_response_dto_test.dart test/scan_library_dto_test.dart test/search_album_response_dto_test.dart test/search_api_test.dart @@ -585,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart test/system_config_trash_dto_test.dart test/system_config_user_dto_test.dart +test/system_metadata_api_test.dart test/tag_api_test.dart test/tag_response_dto_test.dart test/tag_type_enum_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 3ebd65025b606cd030899cfffaad5099acb6832b..27d631e4fd03f971074a7eb7a3764485ec64ad20 100644 GIT binary patch delta 491 zcmex+mhsd@#tocCo4bsjvr7h77MG;v`lgm7rX-dmIu>MVDby%LYiXrVmNAl6LzmRc zO;Jt`wX|Y2Qu34a^)WO;olpePl@8OzS`5+#a)~qCB~UfGa7DUcMR*-lS^x}( z)X8#2l2X__50&;Zl9K~V>%uKV^Y-R!s~w!ipm6eshf^rnY?qRJsNWp1NpBW&nImL_ cHBf?5iwpAeic=9LU{yA`AyRbn<}fb-0JILcP5=M^ delta 76 zcmV-S0JHzf(ETZ*F35VRB@%|1t3gv#vX`3A3O@j}o(gSW6IS@g4^N diff --git a/mobile/openapi/doc/AdminOnboardingUpdateDto.md b/mobile/openapi/doc/AdminOnboardingUpdateDto.md new file mode 100644 index 0000000000000000000000000000000000000000..b25084301943eb5495f27b1e781338f4732efdd4 GIT binary patch literal 423 zcma)2!A`?4488j+EcGxJDcv1TMco03jS0bNn$)cEMnn@wsW~9=@g$`ZgA3&1Jp1|C zeid@0V4|x%TN>)ddKVtTkxjBLoS&mLLai`BRpC7wi}FG^aWR_y)tm{suCpcyI3GC) zF6R98th_4fg|N6O)JbWVaxsac5w^w?e&Ex4ETN^bPcDk%kkOVUGxWvF$qS_QUl^2f z<9rWUr7~ZsBl9qQGXmJ}Z}TB2jGJNs4sMjg;i|4zkL#x0tZQ#8%l(_DAf_y)Pn`5* e!MMcl?q+xQ&)#f^reGtxAU`C&2L2kp1b|PEh=TtB literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md new file mode 100644 index 0000000000000000000000000000000000000000..87f8aa8ab7cfc8c3ee7b938638b9538caa64edfc GIT binary patch literal 474 zcma)2!Ab)$5WVLs2KG?9knLShm90{+rEK+73L7?K4en+_G75qp?_^!DilDiKH+gSf z-Yb9tdJ|kVkj1`g@D*diTcnPx^QS!<#s*U$pRi_9swm)&j0Ju|7&Q<~_iRq6+P1YO zvT{S9$S|Acmy^RHyJW)bDpSWzyC{#59VWFk1HwDpzlD6&n7Zf!JMx0I#KO>LM}}ue z>%Xun%ibhjk6*iSJ!ZSI6j@pBIZDzI2%eY^!AkPq8R~GjmdwBeo7@`oF7ferQB{j( ywX8R*YOsZ+?qxtP{YeJ>NKub%r8#`5m%H^pwbGqNZv#2c-&TAY{xF{_r9J=#W|SBJ literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/ServerInfoApi.md b/mobile/openapi/doc/ServerInfoApi.md index cb5cf0fd3ec98fc4cd06b4afb039859add422bee..e8121a8001e11a58ed44ca847a54b7d8ff49eff2 100644 GIT binary patch delta 12 TcmdlR*5k2Z5%XpqmMjGTBS{2` delta 261 zcmeD2*c-NC5wmEtmR50UiDOD`W}bguQhs7lN@iaA8O4x3;5&X!k zkXVozpP5%&l9-pAs)wmiV{(D0lMtr(=wHp1YMM7r8)H;1j znTT)1kiDy`t3>3+ljBV1hU(S$UbS4)E@utuzgknzYo#fjb>9{wa;BV9VdO27(G*zI z!1EAwq!YTQH~s!8EdH5rVes>ii#V5=ky?4-{0DRMKf~qU-gyuV*|d_eR2_A#MV?4C z41x=PWt-7BslilOQ#~_nV8tLfYWIVHnnhWfa9q`rSU zNcNh|ga=}h8npUBDp{GxW&~UOE927mH_RRL)DG_Zb>~=b*z1B(`km_UJ7?WX#JN0f zzel$thlz^yc#I|Jnp6G3c6q^Ol2MlPK;Dth(J-RdjH{40XrS}QG+&V%kYY|a`SRRO zT`*+mBDvhQs}FW}iL(8VBr4BuvqH*xP4M@lr2}r|HZ}s1N~>) zoIZ?XJi;AQoC6ThhDa!~*;W}&^&~IXJ#r>Ge{}U5^pT<{qKB`%fz2VY4{soqNXed( zR6sNpGcMQ)TKm@2ukL;oRRUJ7ls}Pw;jdu0laQm zXHak7#axmPs>upgX=)F5C8=6MAqYXkqPC^lKN&q0Crr_GjG;)a81&HK|%rs-qlFcSqwPdqt4paw(wTakPn?WMhbI^9-M^Ure zZXLdBTUK;~jaPJD-JPZRYSbFIo?V)EgH^-@W*@OgY!N&PK^#DA1HcUX*jo_n(Fy7R zY?ca0SQ^+{kQqBAvMjBzx%qd2rvmT%{576REX*36U0nFJ20aZ~21sRLFut&}M8^}0 z(Iv=-0O1gO2_H|Rm|xe@q*i%Mz<7WiFc n;~Eiut7QUQ7-wdRSJqCM(X~#d--E_p5zaw- literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 8520bab3056ca6340d4b5fabdb3bdfadb00de704..44bd35a6838a99692a4c97328b6ed3feef902bc6 100644 GIT binary patch delta 86 zcmbQ`zRY7oIV*p0WpPPrZhUTPNn%Q3N#f>2)^m*fDY=<>@%ed4`H4j-nR)4x4LB7x o>$7j<6E8|FOD!r+jZaU_PtFHP#TS<(mZVND@FnPR7ap>=Wsi#j#~6%-b- J)N<8w0RXDpIMo0E diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart new file mode 100644 index 0000000000000000000000000000000000000000..f3952fda8a565f4974b78534c216d7bf97c17544 GIT binary patch literal 4697 zcmeHKU2oeq6n)pPxa~u253=0_dl=lto|`ynfHn3K2gMKsDq~&S>||0UskmN||GrC7 zlI>Jpx@G8EAP@c!$@{@`&pjgB?V#O;{>6CMJRY15&idoQAsn6jItbx#2&cm_ycr&z z9R2=;mf4lR#7vp}z1EL=fqmpNAr(ebsiG-QumhRMG{zAoxIkgT;+Zc}sZ6B>OO|Y< zqck&-!Vei&WG>i(zEh^?*PznmTfWjG&Q;;bNmhu*TUIalfNto7fzS0I4Z_u!q8P-oZda+Lf3F(QSwF%4#{XUqVw|!pF{L2|1(pYBjPH(-ipSZ{=h6^6JG# zW^-gQs`bKiqX@ z*dLwL-8_IrEzJMh^4E%ojm;H91qj9S=Dv!tb^67~W|A$g2aD9Kpa&vL65{mMtm{#4 z*RYt}+Fj%%y2G1`l)4BHjqFvi>JF29xl~R~+dWIh&An{&W#Ln(7p5%|3X|yr86$M} z!4mh}jkK;=d`8-X+B9^%NS#N;T}XqVQQe>_yHH5DCKs@z#-c?&VJk{|&wmPGN$cl~kQdUBY}kOnjqO$E zxo=z-8@@nmvQ*pBrLxfge-q6^sE%6v&zjQsk$F z>m%YdGbIwRw3{$$<^0Pc7xILTAlmhaPi(6Kc5_6dQ)xVto;{Qm&bDGCJUL7FpV+rrRQFWbU7QeVyQoRqn>|>o>x+7gRZ%*-?!`m`aT+-|EC9W_ZBJdLLGYN(1`zrAK-G=8|n(!_G!I5 zb=f14ziBnBx6cmxnLD1{@w?THcjnffcZGB>({KBBU{97i0Q){J4mFmh37@#TUHiS3 zqV{jE5|+W=-d*A=^}&tgaTh9kp#D(a@3_lU2HR@5UCa#g>&4b7+g}_0zkAMYa2xyu Dpg1Ye literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0a0cd80088bbdd96df0fef2da6db126d89dea032..a92f1df7a7bed8c052cc85c84d8993ab98f23266 100644 GIT binary patch delta 108 zcmdmcm~rY+#tolk_)>B+^ZfIY@)L_vGV{_WD=NyN3nfZxZT>71uP7RnT9#T=oa&yM fpPUa;99)uEk~(>!gS-efnaLmQbvFmwWhnyy6;vu) delta 19 bcmbPwlyUE2#tolkHiyf`D{j`Z&sGKiS`i2B diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..50c4ae090ede8409ad0d2003b51e6b03e8eaf215 GIT binary patch literal 2989 zcmbVOZExE)5dQ98aRG)}0aSVGry;4m28%PaYh$2w2Mk6a&=O^_lSz%FY8a{i`|e0d zp;s#jY9O&i-5a0hxg#f&(PRQwpO=f5f6Q;^AFr14Yq)v)ejda10&W*e_^`Nsd-L}h znvvz3oM}6KmHhH*M2})4mF8)obXtgtUqB-p!}F9^e9NVcyJxYjm9{56ShZt&lh%z* zHUGB}8r>z^;{Tdy{I*;h46e;-_e@D+nY5|MF`*~~*UsIXOjZfWO2osf+>7weSf%g<%6?W z>2Z)`e1{(YC)%59r!D703kTx;hcpLF=HOe=fUpL>vZ~<*3az$n%#~Y0nHFd;COxlk z-*+*{rmDOkcEULI23*ye7uZm)7j=`N(o9MkOtCes$WrX$Qsu;*Ul|;xX$!Hh zvBMY3id==HN-V5f2RK6BnIsuA_B#$DFB9Ki)U$)NL|Cr>1TQ&KulZ|g zz*$6$$8b#O`5E;oeJ~|`9o+wxO~SKhvqj~-z=DV5IfDvc-yl^-@c7L4ZVqg;+Oc(o z-2?@z437^TJ{}#-O;cG?=3tW5ov)RErqF~lm4%(|1c*(3tL%JR+kF=svB$|0wJFpN zN1QW#c;B((nVSwGo{$jT?rt|CZ#4t3?N!nJz^4x19@br*(&>s=ddRax= zi#q5op!?;bxL#}73HyH#Ka$5uj=GXPOlTDjh{15(#0crYIv!=i(KhvL4YdHykM``o zfc}_G@ub58oepk4W{;S~RlV=HP3TE*)9s2_@;iGz$1Y&v>;uN0;2MEjdB=bFo<^qg z_nM_59w8iE`-a2q<0FkXhnDhZ;!fBSm#EL^A~3|tOK*SD6$)ZNmHH_5%efRQ8 zwp=NZw*^RIQ}_EE9#VrrZ!m!OUnb+%A1-e%@7_-?FW~C>^JM@RW4Ikp;AVVref8%N znvvzZL>M=G8~*aPM_<)ru8oXRZK70V@)VZ&(#j-~nOw-+1=V-8D70~{9wJ+bwT+6U zi;VnxsSLX3Vu61PVer3`#$s?|+udWGTPJcCsRR?MR7vCAZELbj<c&7=tAbAP$B-F^?Gq8tc5%I4a<-pq>go>^2d88oV=5^!0htEX?W@Z z3~(TK(k)GHK|n57Fzxc2f%4_YGQl)PT-gxobJu>nSBStZPAquF>PwMe z)%0|8f}H`}JA=KPLo-CDm@CTWGC?RF3PUlAH4ZN&8q`hrUPRxQkxXX6?hDnrne?fs5eh{CRfl>$nlcZ=5!OWQ{vn#Urfbqt!Xq!3<7S zE;9IvOE83n_0#6ePeG;NNz(@T5zn*=2tL@<95>57o?(t>b_(a&fXu8cOGBJHVi6jp zW3$W>w3o_bll`)It-G9>?~i2|uROkXinE~&{=XCnM4kW(w*}=ad=uGHT1bsvl(DCN z2}LYLgJV*?DZ)PsnD2hSu9WrKR@*;?ktxcrS8lV6Q8{cxni8RqI~7N$`E#Ane&U5r zT|DLsg`|K?go6T=oY@AzRw&BG|1DWlk;${Bocd>lgXq$X z5oBm~g_PJEKR5E^kB3L2SK=WQ?`L=orRydi!F!0~s##Z|xiOVzJwPA3Dg z&wWyKAr}(F+tJpD7;I%>ClJAwolLGhb-~m%QVn1-ms|d9-!hP@7B5f&X|Y6VJxIU< y++teVRsufWk8kPar93tKslI(h`9!|kE8ghIkEiXD#;2QqQl3}-J>{&2+V&4Yf7_Y> literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/admin_onboarding_update_dto_test.dart b/mobile/openapi/test/admin_onboarding_update_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..09cc73e977c0748ac9d4babb2828f7e65a246e9d GIT binary patch literal 600 zcmZ`#&1=Fi6u`=PVc^92l#$9Ca_M7i5)4gK`KYL+_E_p{)b3@&su_SKnWqPTk zbzC|oDl!zcKojWpqFE~>TGlGr5X|kkWDj?&+;L;0=$H{=&C3to;8bapT#JO>*k2vb z{y`jCm-uo9cvHVypf7<%)d`u)t>UHIj$yc3rjL`+Y&0J(M+uClZ=*IODa_I(yr;=@{PDoB8u^+_M}uzXxf|7^DztGZ zHpYpKDzOKZt~}-fOWdKR_IXx2YaCr=$nskrys#AshhLR)OxJS9rj4<8-*^={Z+G4Y4oBrx?mV>r4}zwBHK#mqv(_o8dheXvcR6PsHIggf1SE@ zqkjP*^c2^}8Bm+nvj<)Y%yG}*ae^i@xzfdgB;g#r<;<9niS|b2u#-wRTksPBFngo4 zEJ1}*8N%SENVCD5a-qc&9Nx<&0O)vJmR9t_c^Q zErNBt8b=3MUsBd5ZRrm>3zcdKuC+{)G{_xKasL~yY|gv6%kTuf&xi$s;W7FJA!qh$ literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/server_info_api_test.dart b/mobile/openapi/test/server_info_api_test.dart index 68cd1c348bcf9c43aacbf924cc90ca0391480ca8..dac465116eb42c185c525ec1eee5ca4a399c6078 100644 GIT binary patch delta 14 VcmZ3_bC7$(ZdSHhO>3@NE&wM_1aJTV delta 65 zcmX@ey`E>oZq~^T%v{0>#i=EZDY=<>{&`9HiA5=ydFhk?GD`?y5njlw&Q_~w%~i_< E01$~64gdfE diff --git a/mobile/openapi/test/system_metadata_api_test.dart b/mobile/openapi/test/system_metadata_api_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..bc1ce6f6f33849d10723585ed976cf7f29255adc GIT binary patch literal 936 zcmb7>PfNo<5XJBL6yvECs!>nkKP;pbK@HS;@U)D{G%hB)>+VD;;&*p8RgoG+57{uu z@6Eh+d-eI*i6jW_ZB^ckMQTscpxJMt7WaSYC4vPn@=)gKM=yWR2 zcHDY!_zhtSoW|n=h@qqqzXXs_;CdODnoD9Vk#X#F@?>=h4QeyogqH+B@P#&5gYNby z2#&w2hMR|pu$8y18xDSHKN!J#-`IKDa_i}6TsGR|=FXxowW)5Lc%h8#2~@;uKShmU zX$qMJ-5#rPJ>+Us2X~7^fh;r6v%C|(t~`J7F{3BBOFAQ4*l6UM#m9_-lPmo6(+mCL hs+a$_H)y+1*27q+SwWlQGBF("/system-metadata/admin-onboarding", { + ...opts + })); +} +export function updateAdminOnboarding({ adminOnboardingUpdateDto }: { + adminOnboardingUpdateDto: AdminOnboardingUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/system-metadata/admin-onboarding", oazapfts.json({ + ...opts, + method: "POST", + body: adminOnboardingUpdateDto + }))); +} +export function getReverseGeocodingState(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ReverseGeocodingStateResponseDto; + }>("/system-metadata/reverse-geocoding-state", { + ...opts + })); +} export function getAllTags(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index ad2f6e8de..bd10c41a4 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -21,6 +21,7 @@ import { SessionController } from 'src/controllers/session.controller'; import { SharedLinkController } from 'src/controllers/shared-link.controller'; import { SyncController } from 'src/controllers/sync.controller'; import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { SystemMetadataController } from 'src/controllers/system-metadata.controller'; import { TagController } from 'src/controllers/tag.controller'; import { TimelineController } from 'src/controllers/timeline.controller'; import { TrashController } from 'src/controllers/trash.controller'; @@ -51,6 +52,7 @@ export const controllers = [ SharedLinkController, SyncController, SystemConfigController, + SystemMetadataController, TagController, TimelineController, TrashController, diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index e32b0d191..35e5e1759 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ServerConfigDto, @@ -65,11 +65,4 @@ export class ServerInfoController { getSupportedMediaTypes(): ServerMediaTypesResponseDto { return this.service.getSupportedMediaTypes(); } - - @AdminRoute() - @Post('admin-onboarding') - @HttpCode(HttpStatus.NO_CONTENT) - setAdminOnboarding(): Promise { - return this.service.setAdminOnboarding(); - } } diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts new file mode 100644 index 000000000..7f186fec0 --- /dev/null +++ b/server/src/controllers/system-metadata.controller.ts @@ -0,0 +1,28 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; + +@ApiTags('System Metadata') +@Controller('system-metadata') +@Authenticated({ admin: true }) +export class SystemMetadataController { + constructor(private service: SystemMetadataService) {} + + @Get('admin-onboarding') + getAdminOnboarding(): Promise { + return this.service.getAdminOnboarding(); + } + + @Post('admin-onboarding') + @HttpCode(HttpStatus.NO_CONTENT) + updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { + return this.service.updateAdminOnboarding(dto); + } + + @Get('reverse-geocoding-state') + getReverseGeocodingState(): Promise { + return this.service.getReverseGeocodingState(); + } +} diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts new file mode 100644 index 000000000..1c0443534 --- /dev/null +++ b/server/src/dtos/system-metadata.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean } from 'class-validator'; + +export class AdminOnboardingUpdateDto { + @IsBoolean() + isOnboarded!: boolean; +} + +export class AdminOnboardingResponseDto { + isOnboarded!: boolean; +} + +export class ReverseGeocodingStateResponseDto { + lastUpdate!: string | null; + lastImportFileName!: string | null; +} diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 8eeb0064a..e7d37407d 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -36,8 +36,8 @@ export class MetadataRepository implements IMetadataRepository { this.logger.log('Initializing metadata repository'); const geodataDate = await readFile(geodataDatePath, 'utf8'); + // TODO move to metadata service init const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); - if (geocodingMetadata?.lastUpdate === geodataDate) { return; } diff --git a/server/src/services/index.ts b/server/src/services/index.ts index db3d6083e..2305708ca 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -25,6 +25,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; import { SyncService } from 'src/services/sync.service'; import { SystemConfigService } from 'src/services/system-config.service'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; import { TagService } from 'src/services/tag.service'; import { TimelineService } from 'src/services/timeline.service'; import { TrashService } from 'src/services/trash.service'; @@ -58,6 +59,7 @@ export const services = [ StorageTemplateService, SyncService, SystemConfigService, + SystemMetadataService, TagService, TimelineService, TrashService, diff --git a/server/src/services/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts index 836909b74..115ab4b6a 100644 --- a/server/src/services/server-info.service.spec.ts +++ b/server/src/services/server-info.service.spec.ts @@ -1,5 +1,4 @@ import { serverVersion } from 'src/constants'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; import { IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -207,13 +206,6 @@ describe(ServerInfoService.name, () => { }); }); - describe('setAdminOnboarding', () => { - it('should set admin onboarding to true', async () => { - await sut.setAdminOnboarding(); - expect(systemMetadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - }); - }); - describe('getStats', () => { it('should total up usage by user', async () => { userMock.getUserStats.mockResolvedValue([ diff --git a/server/src/services/server-info.service.ts b/server/src/services/server-info.service.ts index bb092896b..52bf8bd1d 100644 --- a/server/src/services/server-info.service.ts +++ b/server/src/services/server-info.service.ts @@ -51,7 +51,9 @@ export class ServerInfoService { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { - await this.setAdminOnboarding(); + await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: true, + }); } } @@ -105,10 +107,6 @@ export class ServerInfoService { }; } - setAdminOnboarding(): Promise { - return this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - } - async getStatistics(): Promise { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts new file mode 100644 index 000000000..9d11c1c72 --- /dev/null +++ b/server/src/services/system-metadata.service.spec.ts @@ -0,0 +1,31 @@ +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { Mocked } from 'vitest'; + +describe(SystemMetadataService.name, () => { + let sut: SystemMetadataService; + let metadataMock: Mocked; + + beforeEach(() => { + metadataMock = newSystemMetadataRepositoryMock(); + sut = new SystemMetadataService(metadataMock); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('updateAdminOnboarding', () => { + it('should update isOnboarded to true', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); + }); + + it('should update isOnboarded to false', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false }); + }); + }); +}); diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts new file mode 100644 index 000000000..e8fddfc13 --- /dev/null +++ b/server/src/services/system-metadata.service.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + AdminOnboardingResponseDto, + AdminOnboardingUpdateDto, + ReverseGeocodingStateResponseDto, +} from 'src/dtos/system-metadata.dto'; +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; + +@Injectable() +export class SystemMetadataService { + constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {} + + async getAdminOnboarding(): Promise { + const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING); + return { isOnboarded: false, ...value }; + } + + async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise { + await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: dto.isOnboarded, + }); + } + + async getReverseGeocodingState(): Promise { + const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + return { lastUpdate: null, lastImportFileName: null, ...value }; + } +} diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 09139a7f7..4647ad8bd 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -5,7 +5,7 @@ import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte'; import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; - import { setAdminOnboarding } from '@immich/sdk'; + import { updateAdminOnboarding } from '@immich/sdk'; let index = 0; @@ -28,7 +28,7 @@ const handleDoneClicked = async () => { if (index >= onboardingSteps.length - 1) { - await setAdminOnboarding(); + await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); await goto(AppRoute.PHOTOS); } else { index++;