From 155fdbb16a16968defa2f88b13c00db7f915ef52 Mon Sep 17 00:00:00 2001 From: Schubert Ferenc Date: Sat, 11 Jul 2026 15:50:15 +0200 Subject: [PATCH] fix(auth): correct local cookie security configuration --- validation-suite/.env | 4 + validation-suite/.env.example | 4 + ...202607110005_reference_masterdata_notes.py | 21 + .../app/__pycache__/cli.cpython-313.pyc | Bin 0 -> 6093 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 0 -> 1879 bytes .../__pycache__/dependencies.cpython-313.pyc | Bin 0 -> 2551 bytes .../api/v1/__pycache__/auth.cpython-313.pyc | Bin 0 -> 3192 bytes .../api/v1/__pycache__/domain.cpython-313.pyc | Bin 0 -> 40006 bytes .../api/v1/__pycache__/router.cpython-313.pyc | Bin 0 -> 486 bytes .../backend/mercury/app/api/v1/auth.py | 9 +- validation-suite/backend/mercury/app/cli.py | 114 ++++++ .../core/__pycache__/config.cpython-313.pyc | Bin 0 -> 1536 bytes .../core/__pycache__/security.cpython-313.pyc | Bin 0 -> 1763 bytes .../backend/mercury/app/core/security.py | 3 +- .../app/db/__pycache__/base.cpython-313.pyc | Bin 0 -> 1937 bytes .../app/db/__pycache__/seed.cpython-313.pyc | Bin 0 -> 1578 bytes .../db/__pycache__/session.cpython-313.pyc | Bin 0 -> 890 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 897 bytes .../__pycache__/contact.cpython-313.pyc | Bin 0 -> 1357 bytes .../__pycache__/customer.cpython-313.pyc | Bin 0 -> 2072 bytes .../models/__pycache__/device.cpython-313.pyc | Bin 0 -> 2076 bytes .../__pycache__/document.cpython-313.pyc | Bin 0 -> 1479 bytes .../__pycache__/equipment.cpython-313.pyc | Bin 0 -> 2205 bytes .../__pycache__/location.cpython-313.pyc | Bin 0 -> 1438 bytes .../__pycache__/program.cpython-313.pyc | Bin 0 -> 1134 bytes .../report_template.cpython-313.pyc | Bin 0 -> 6469 bytes .../models/__pycache__/user.cpython-313.pyc | Bin 0 -> 2229 bytes .../__pycache__/validation.cpython-313.pyc | Bin 0 -> 3947 bytes .../backend/mercury/app/models/contact.py | 2 +- .../backend/mercury/app/models/device.py | 2 +- .../backend/mercury/app/models/equipment.py | 2 +- .../__pycache__/service.cpython-313.pyc | Bin 0 -> 10529 bytes .../orion/__pycache__/assets.cpython-313.pyc | Bin 0 -> 847 bytes .../orion/__pycache__/context.cpython-313.pyc | Bin 0 -> 4813 bytes .../orion/__pycache__/html.cpython-313.pyc | Bin 0 -> 3636 bytes .../orion/__pycache__/service.cpython-313.pyc | Bin 0 -> 7054 bytes .../template_service.cpython-313.pyc | Bin 0 -> 20373 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 816 bytes .../__pycache__/base.cpython-313.pyc | Bin 0 -> 833 bytes .../__pycache__/chapters.cpython-313.pyc | Bin 0 -> 28772 bytes .../__pycache__/report.cpython-313.pyc | Bin 0 -> 3383 bytes .../__pycache__/base.cpython-313.pyc | Bin 0 -> 3540 bytes .../__pycache__/domain.cpython-313.pyc | Bin 0 -> 3728 bytes .../schemas/__pycache__/auth.cpython-313.pyc | Bin 0 -> 1774 bytes .../__pycache__/common.cpython-313.pyc | Bin 0 -> 1222 bytes .../__pycache__/domain.cpython-313.pyc | Bin 0 -> 12527 bytes .../backend/mercury/app/schemas/auth.py | 2 + .../__pycache__/auth_service.cpython-313.pyc | Bin 0 -> 2086 bytes .../domain_service.cpython-313.pyc | Bin 0 -> 5533 bytes .../reference_masterdata.cpython-313.pyc | Bin 0 -> 16513 bytes .../validation_workflow.cpython-313.pyc | Bin 0 -> 25344 bytes .../mercury/app/services/auth_service.py | 10 +- .../app/services/reference_masterdata.py | 380 ++++++++++++++++++ ...tion_workflow.cpython-313-pytest-8.3.4.pyc | Bin 0 -> 93493 bytes .../mercury/tests/test_validation_workflow.py | 93 ++++- validation-suite/docker-compose.yml | 5 +- .../frontend/atlas/app/(auth)/login/page.tsx | 91 ++++- .../frontend/atlas/app/api/login/route.ts | 32 ++ .../frontend/atlas/app/api/logout/route.ts | 13 + .../frontend/atlas/app/api/me/route.ts | 18 + .../atlas/app/api/v1/[...path]/route.ts | 41 ++ .../frontend/atlas/components/auth.tsx | 72 ++-- validation-suite/frontend/atlas/lib/api.ts | 28 +- validation-suite/frontend/atlas/lib/auth.ts | 13 + validation-suite/frontend/atlas/middleware.ts | 60 +++ validation-suite/frontend/atlas/package.json | 4 +- .../frontend/atlas/tests/auth-cookie.test.mjs | 42 ++ 67 files changed, 1003 insertions(+), 62 deletions(-) create mode 100644 validation-suite/.env create mode 100644 validation-suite/backend/mercury/alembic/versions/202607110005_reference_masterdata_notes.py create mode 100644 validation-suite/backend/mercury/app/__pycache__/cli.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/__pycache__/main.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/api/__pycache__/dependencies.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/api/v1/__pycache__/auth.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/api/v1/__pycache__/domain.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/api/v1/__pycache__/router.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/cli.py create mode 100644 validation-suite/backend/mercury/app/core/__pycache__/config.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/core/__pycache__/security.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/db/__pycache__/base.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/db/__pycache__/seed.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/db/__pycache__/session.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/__init__.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/contact.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/customer.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/device.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/document.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/equipment.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/location.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/program.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/report_template.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/user.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/models/__pycache__/validation.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/helios/__pycache__/service.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/__pycache__/assets.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/__pycache__/context.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/__pycache__/html.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/__pycache__/service.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/__pycache__/template_service.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/components/__pycache__/__init__.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/components/__pycache__/base.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/components/__pycache__/chapters.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/modules/orion/templates/__pycache__/report.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/repositories/__pycache__/base.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/repositories/__pycache__/domain.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/schemas/__pycache__/auth.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/schemas/__pycache__/common.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/schemas/__pycache__/domain.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/services/__pycache__/auth_service.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/services/__pycache__/domain_service.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/services/__pycache__/reference_masterdata.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/services/__pycache__/validation_workflow.cpython-313.pyc create mode 100644 validation-suite/backend/mercury/app/services/reference_masterdata.py create mode 100644 validation-suite/backend/mercury/tests/__pycache__/test_validation_workflow.cpython-313-pytest-8.3.4.pyc create mode 100644 validation-suite/frontend/atlas/app/api/login/route.ts create mode 100644 validation-suite/frontend/atlas/app/api/logout/route.ts create mode 100644 validation-suite/frontend/atlas/app/api/me/route.ts create mode 100644 validation-suite/frontend/atlas/app/api/v1/[...path]/route.ts create mode 100644 validation-suite/frontend/atlas/lib/auth.ts create mode 100644 validation-suite/frontend/atlas/middleware.ts create mode 100644 validation-suite/frontend/atlas/tests/auth-cookie.test.mjs diff --git a/validation-suite/.env b/validation-suite/.env new file mode 100644 index 00000000..5c89bdc4 --- /dev/null +++ b/validation-suite/.env @@ -0,0 +1,4 @@ +AUTH_COOKIE_SECURE=false +AUTH_COOKIE_NAME=atlas_access_token +AUTH_COOKIE_SAMESITE=lax +MERCURY_INTERNAL_URL=http://mercury-api:8000 diff --git a/validation-suite/.env.example b/validation-suite/.env.example index 943a7c26..2408343e 100644 --- a/validation-suite/.env.example +++ b/validation-suite/.env.example @@ -6,3 +6,7 @@ JWT_SECRET=replace-this-secret ADMIN_EMAIL=admin@schubamed.de ADMIN_PASSWORD=ValidationSuite!2026 NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api/v1 +AUTH_COOKIE_NAME=atlas_access_token +AUTH_COOKIE_SECURE=false +AUTH_COOKIE_SAMESITE=lax +MERCURY_INTERNAL_URL=http://mercury-api:8000 diff --git a/validation-suite/backend/mercury/alembic/versions/202607110005_reference_masterdata_notes.py b/validation-suite/backend/mercury/alembic/versions/202607110005_reference_masterdata_notes.py new file mode 100644 index 00000000..8aa9ce16 --- /dev/null +++ b/validation-suite/backend/mercury/alembic/versions/202607110005_reference_masterdata_notes.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "202607110005" +down_revision = "202607110004" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("contacts", sa.Column("notes", sa.String(length=500), nullable=True)) + op.add_column("devices", sa.Column("notes", sa.Text(), nullable=True)) + op.add_column("equipment", sa.Column("notes", sa.String(length=500), nullable=True)) + + +def downgrade() -> None: + op.drop_column("equipment", "notes") + op.drop_column("devices", "notes") + op.drop_column("contacts", "notes") diff --git a/validation-suite/backend/mercury/app/__pycache__/cli.cpython-313.pyc b/validation-suite/backend/mercury/app/__pycache__/cli.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58d34d2bf7be6f0b69306e01c7582d62446b21e3 GIT binary patch literal 6093 zcmbVQU2qfE6}~I2{{JQ07-NgYHeeZKeho1(5CSG(gG01d%0x8kT3Xwp(n`6z0<5GR zKboOaOs5^3X=h5Nc_1@=Af3#FJ~h*%?^4&Tl#S?QCjFVbks;I0v7UY@=n%lzq~H9kgtna!$Iiij4+x@kfgjKD4Dz@Ps6)yUQ`N#EJKmKBq$4zyLnNTL4P5i z60$@dmqiRY7Zc}1EJ`Wyq97|GP78`~K3B+Ng%|O)OiD~Od#3%9RW~EVJbA%H);3}2 zMyVPh6Oxz&)M%tQqmNfI#Z57bE^p8-jCueZ%Xv&65c#!)@Q?^ zm*;pkgw7pBNV2pn#w}jNTM~VGo68eq$@ErUbg0cjLkBisK9aR%gSW=5P}8Kh;?Ttq zLM{aPSR;ou2hM-Oo}%$vXYVJwLl7sL~u>WW&}u)=TkQ@`8t}z5Mr`Lv5P#&w$yBb zoJ>QXn)|Apmy&WZmlN=!W+~u|q-ZP=YL;{{SCBQctYFQY6(!BHZZHv>#tL{|)@*Xa ze@TJ*rDnyV3^Uhk1z|Cp7t)#~E6j>njm;=xP9}cEHIn8O3Wa^CY-XgesBKT>(#fZ0 zlQVoD?}CQJedK?^V+qwAoj(b$9<2utJ~*ldpI^4s0^QZXNF^{*cl%d5?uPG#RX11b z=)Qa5&V^bpSMA+j>D^xo_gBM*E8)YB9Voc(F|q`m%htzk6xw=s<2q8U)9wM z|8;lZ=Yw~Le>S}Oy$7S83{-+=|Kfh>i>>^MrS9)s?Oe63;&%t`&A%h5{iEd_N6KAC z*Zj}bdIx?ry>h1Bv32##y^ed<)#zV4hMt&F-=425sAq&`ZP5=xE;E)ZmA;nGoC}BXC?y520hHCKIsuhES zZes$t+C&-yhu@g=F&#VC0u;SpfIT*~-t7qxZeqFJ?L{WBMel{T7`7~JQmY}gUVio1 zbvW)|E!x3F*NaS?0l+k@#M!K|IHi# zATK1zDSADR(;%?QVpdEkB|kYiIXG{*xqMLqxF~t2#p|426W#+cu>i=`Cis~bwUAER-{#z$Wi@s4cc%)fJH<{~Bs1(*(uSs%&ur%R`u=`4 zU1_ss_}a6q0oiEvZ_~ZynJugd15H}~CE41f4&9DqzkFj8FL&ztcEr%<2N>yyJ7Qb) zRcYmMGhn1apL82B!`oXmTeUVLHgR@S%yY(_v+L2wrS}17l+!D6} zrR~>stvp7y`-Vi(%~)^V&3kz7nDNdb=x_IiCh)4S5$qZQ05tJFeU$$<#`&iy#$m$F zAOf%xJd5SCA|}yC$vzRi2;c!Q=6E%aB_St5(K#)o7W8rxvXo<7kuYHWm75i1<>Pmi z%)BT8huTYwg(y}ESs-%hMUFB%dFCvD%mL79PKqZQM2 zE{#bx@RRmMk(#u^zU^o?k=VA)FcAW{!uupX|M<`tbZT$po zn-p0ftAMAV+7bB4ULYz>tQ{h7>kh08tM0z4d!XVTP~AJL?mZRv9@QOPny3f5R^C(t z1JyvJ5{RgQJxh~7m{zi?f1v7*RQwUuzh~)u-Pg79mg*Z^I#=%9d?G&01s=-5L_aTDDuA44kmbI>JwQW0VJp;AQzPqpAdA-^hsdPp@4ez;s zP7RONdcw7x!(aLA9`Dy?r^ETU3pxDlINrZzA0RkBVBk1`y+8K87@jbj|73Pf1Z*h~ z7>N*h3grK!6fa>YVk8vUkoqYF9AhLBvyzq@5^v^N-ZEyAfYiju&e@Q71FC6MY|zG5 zHQojm>|;PG0b(}OJ9sDWdWPQp9eU4q=)G-vviUZo7AfU@V+@RP^LhGpsG>A@vv~xJ zd2ULR6C@C!>6!fmjeda0Zbr8g29<5v0ZI}HO_u_NMP(r`anT${Iaf9Vc*quTejcJp zAVq?do`H~)O#`xNb}YV8%wREH+SZ7lqDJ5sZN*O|SF|OMF!&M-zvPHUAHvwL>!tOg2{q_Wl{5U} zXi4j9ZbI;yakot4MauFh{6HLTZ8mbY8S^wFPTihP5^2w19nh=UmrNXrfLs&h+_S|@ zRv!pXg7Q`P$xe!Jbx&v2Gx%rEV0rj>#dBimY%Q>*8rW3{?7DycBeopaRSrxnO@7`H zu69H!9g*_zOKQitrHeH-P-VjvHoW?y2TGZg##^E{AB|C4d@WU&# z-ej9>-$OT*O&GGZ!P8W7=JQZrH8fNS4V5EfYUtR~g&OOxvV9e{Z#DB^uuMuv4bj@b zD3u&BTHOz;q37BftqmNaH4fKYfu)O2U`f!rm=(aR0cfv5VY7=m3QnZfh7>oho4)PJ zE^b^c4WjoLxWb`dMRU?`OLmEjc`yoBJLKl#8YUy4L4{1ZGd?T}A&P`W5d9E-GNC_k z+u+%H>vi~yt^iyNR9wBQGpeh9=?t*`+haG!R%ULUEVDiM2T)hDCzHfNGWjKJ1r%Xv z7vBV-OFEE|XvCD@amg=ml7&^BntDbkD_9@-9~1`j2dvGRz4IWT+uzLwrmZE!^TQRwoLl_C@i z21Hp6nZ$dDE@Fd7*vEk&6|>?Aj6p?KQ%=F-v6*3*N2vb`94IJCh+xMA3O&8;PCyD&J@od_y#x0S UuI^PkcdO2!GCTCxjGABaziBND3jhEB literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/__pycache__/main.cpython-313.pyc b/validation-suite/backend/mercury/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c52b4429b76ec275b5937820037027ddaa39f218 GIT binary patch literal 1879 zcmZuxOK%%h6h1TK$M~7X&eKU#6B1g7;-o+og-1jMs=zLx^0ccOMpMtVGmSlC-MLO_ zR_UrmAQ~2xx?K#&EyR9+yl;@laJ-KxxJ&i&3kk2&`|ZZDOJ z13vjw1M{|Hoo- zU@@ajyU0>P$f+)2%(5FolGKRnqUS44&1fP&ULkFVvTLN(ZW@ee zgl=0kvJ+k@UMQ{D<;GYBem|<)c1Zqr@aOvy7%2 zXtW61bj$&}NemMm?If3tc6+(gZaRkP;$Na|Jci9&u97b;m+8*djye!aJZ4&y)R;r> z@MvI!iLs3qM&>euvQE49e8MBhC;(FLdjt>RM!#nOf*Q$S!wuB%pX(4*rVT3t;dRJY4s(x^&jyyQZl*|UB)^#+2#u=7l-yzE{=j1kEF>f(gGBe4IaS~v5w1SXX{6x z^C<7(2lO-&9`Suqacu~Zn?kY+zbD7`%qNMNC!rboD(-AxmqaLG9cuH?hGUb0Oc!x~ zCv93acEe@#d>+Z?h0{E)>-7#Q5?!Yl5s2<$n@#H`4S8H0=LqM6ic<5*K38lF#;I6y zs7pl)@oXR!C$MUDF-w;EKWj;F=0d9&Cr zoL6o>FE(3_*+Hj893{st2F2E0e-8bYP*G9$nq5JJ&P-DuhRc&8-oY$}TU$6SJlt|R zHsfi_t~EO*>0bt1b!?q%><7yB@F0J$#Yet7%mg(m{7qL+KIyRrLA z-)%kK=uW)*YhgFmg)0N+d+$7jxswRU;d`@pXYX74(o|1Yr0Jdl^7M&7CrKfkygPY+ TdSA-+WJ#J8!m(!pc?bUwR~W=# literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/api/__pycache__/dependencies.cpython-313.pyc b/validation-suite/backend/mercury/app/api/__pycache__/dependencies.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc32d1512f6cba491c38d59ff81d3d49c3261159 GIT binary patch literal 2551 zcma)7O>7fa5Ps|3^=|C7o!HI~lqM#m5GUXOO#~@4RZ<%fTQ!v(LxrL4#$JaF&N{rc zflyS19$HWjhzdk*RO$(dTQ5Cx;utH3q^lsPs;a7bNffH0_R^W>zm!s`&+_cNH}l?` zo%!b5EtktdU^osYW`7e2`3WcWAygG^>p3X*NRlME7>RO}3!rX`*`hqZDFK7h_7)MO{p{$J|j5^)OwEwMN@$8`I^OH|nFlsGs@+ zD+J27jCFj~lxatC%wbf_Cwr*1YC0)suon%&+@VS#}eA%4X1J z?LxzNxpbjO^=oY3`&7$mMoCu-rsY8Egr-uBg0CDsGZmqbp|> zNG)bVOo`KNgrvkoSKZot_Se)L*wWxt9cgb}OZe+r^O+Qr!l+H-Qeq$3P15eVMIvf9 zA%^h3&*AZ$l%Uy?uxnh%b9wSE-$U|T&>n}{In=@oK%sPD&a`;5JbleIXQ^Q_OTour&s@l!R?~6qJvl5z{rE0)RH!u-*v6T+)^zAUR+l8JWu9 zQd}Pi^6_kA8=QmX^WmW%filM3!`=n$)iK2#gIy~20x&^!q#He+Xmobn20aM@uwEd2 zfH6O-X^Bp$TU-QaAA=xUnymVrgu zi%X6M3>K^a)h?BFs%6kEbi7c^0xyn*njQ!Wb03J8iv;R(d-T@mQe?%sd)?df=)~&& zNM(QI$;FEI?6Pup&Evc2+yYnfP+*r=0c?jRC{u$?AS!%y1~t$JP#*-r;`<~}jZfJg z6FmrK4BLDY1O%}j*!M_T35@(fc+tO!0$`_fEDP=Q0C@bXM38iD>kF{_SAm)ccpde= zu0ZcxsTCC4n2mIUcoW$FuZ-;lrF+2`lqixU#cs32@$d(eaZ3amDsL+)4Wc8*5@{mt zYT0qsdsfZO=>{DJ3(Fwp2}&e6zg)PnaQEc>_?`HwzrW(|Uy=H2eKcO_AAd4fai3n6 zPqSWQ8H?9=g+hlAidm*k6hkPGg6f6BXlV!+c36%~CSQiKYMBg0SX)wF1>&67tMzSC zTWf7S1Z-Fa;$5g-gP$==m@NYDFfL?#>NYPG)I#=xHn$Ke(m57}4tTp@h8Uw^^wlKWr`cDY0gpk7CGH_%8K}ZeJ`P*U1#OJJ z2iq{?=0y-dHXQeq^!`E)SIFUKB=U@O!}Mp;_mu2?Mh1Rt@qM*x(Y`5@Lr1^2f9Lt3 zvwP{&mCnHpv6X9GYdf&P!}Gbz&*s30cE|4$ti0H3=Q=jMB(U$B>nlCOfM%|h1@kvc Cx)`4T literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/api/v1/__pycache__/auth.cpython-313.pyc b/validation-suite/backend/mercury/app/api/v1/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29961934893b0cab784f139d6f464a7725454d49 GIT binary patch literal 3192 zcma)8|4$o77N6ahsg|zULphadyGP*pP zx#k_^Xw-U^2esZ=aakTm%i$R~8EeymHl+29pmpE6FypciIRyyUTa#mh7F`}{BLt5j zoY49^5Y*qWA?B^(R0|+ZEPDQoRDbONB91eDOYVwsB#NeuvxHZcz$v5-t8xOTw%)qMA5Kixnw9`B?Oyx zu4-Yz@K>>&v)W=`ma#<|Uem3$Tt&C6jWQ|Fp2wINE1PXb!bb;guIc6~zSmO8;Lobq zvT5jUdDSqzS$MJh2*b>(fccmP7;1%Nu_1QWI<*6dWhXoPRLd})R1zgOF$DAnxEg>Wv{vC+tXsHzs2xSG&M;VZJ zwk*o{I(e`LWf)#Zn`!M=g|YJ9QkN&ric9?Mv3VxsagRn#n1mOYl{9fydLJdEh1YW+ z1V@JSd>-78v)PT&Zv3QTzzK7PNqyW^3h4($>gVUDeq7RxA`N&RB7GnvF{Tka4wNey zCIp8C4pS=nX0fansM2yb_4B>RNmymEgR9VJRIgN~^olX{_#$^iva(4|F>ux~6ReM+ z+eSY}PwuSNqZ5tjc_(^)_mi(5eD&blJB_)!&fML4^v_Sz+qd_FC-w$L_WR?F{tHh3 zg>No7{WE*vzBh_5BsY=IFCPLD+LES9INx>=a2UN~i$dUt)&vrU{y;NwXprAc`OpcQ)X*qvc3;bps*26UXP`EQLM2cO!c7t@*D; z-+>~xP}m)3njfS?L482^Nu*gAbnw^aR+UB$&BR$D2}Rt?KmSM;KlUrRzF z3R;kct5z-#>4wS$Y{LWGn!IIT)x=e7ww7T6saUljJC}fG8u>L_t>TB6V8cwlW+rZ3 zX1lR9M?00OmZic|EkFvzrlnrIr2YlNT7X#7A7V4P^@m?CD(GaTwS!TBlx~2b>Q=R+ z>O}xiEw4csIDO+Iu8@g@uHiSa)ohyy_WhZ|5o5jY=9cSiFBRKB_=l3J=U8y$x zi4(bB3*6sRA`K<(DDj<*#@G#K>_%O=(bC8}kwPs{*jKurU1th!fV|33K{9@th!uP(|UMTiup%#qSd~u%1780_1 zF}-k3eldhq94ZrRSBc59f5PWp8;db=m353t1l)z}Ngk84yX6pEb1PCX^7fKtlk*U| z{Rlvr%9AQ4-UUVI(tmqI^P@Y(NCM?&v0TZ!4E9ylE+R|pSOiHeWF(@ZV zP)-*3#|bv`2DS)GdNjgBg@?&jIU+&E>|sn^(XAw_d;Aczk8$&5ffWUfZqSHE4p)bcW^i0bY0w*n=%puU&NorpWS~^+K$(R>6g>j zzD?Js=YK)6ARkIdoN0+b=%Y9;pfEUdG}(ck8jlFGvu2nd14T2vjrB zLac?y>Vo=4M$zG&(R$y+%b`;{f2|LlYbsG;bT2&Klwf?-9clVu1Un)FTmtLm*xN4P NG0f9%M4nsQqlEOd{DGC&mV*1YBSrRBErGYY1 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 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/api/v1/__pycache__/router.cpython-313.pyc b/validation-suite/backend/mercury/app/api/v1/__pycache__/router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5c6ef6e1d18aa66090a69b579a6eb32603a13d1 GIT binary patch literal 486 zcmey&%ge<81jUR&S&xA9V-N=hSRf3>&r5)esSLpk!HnJvUW`SI3LrLY>j5W}p*5X=(HYROo{9>X3a2r~{r$FK$oA$TyBDI9B42xKS0!3_4(aiWFur;^%GV_viN>fr{d^U*qN`}usl?=B;fCfM<)`!UG6;$5h zu*uC&Da}c>D*^=?C^(93fW!x8Mn=ZF3`$QKWS=uAKjamgk$I6 TokenResponse: - token = AuthService(session).login(payload.email, payload.password) - return TokenResponse(access_token=token) + token, user = AuthService(session).login(payload.email, payload.password) + return TokenResponse( + access_token=token, + expires_in=settings.access_token_minutes * 60, + user=user, + ) @router.get("/me", response_model=UserRead) diff --git a/validation-suite/backend/mercury/app/cli.py b/validation-suite/backend/mercury/app/cli.py new file mode 100644 index 00000000..cc225f84 --- /dev/null +++ b/validation-suite/backend/mercury/app/cli.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import argparse +import json +import sys +from getpass import getpass +from pathlib import Path + +from app.db.session import SessionLocal +from app.models.user import User +from app.services.reference_masterdata import ReferenceMasterdataImportService + + +def cmd_import_reference_masterdata(args: argparse.Namespace) -> int: + with SessionLocal() as session: + service = ReferenceMasterdataImportService(session) + result = service.import_reference_docx( + Path(args.reference), + dry_run=args.dry_run, + update_existing=args.update_existing, + create_validation=args.create_validation, + ) + payload = result.as_dict() + if args.json_summary: + print(json.dumps(payload, ensure_ascii=False, default=str)) + else: + for label in ["created", "updated", "unchanged", "conflicts", "errors"]: + print(f"{label}: {len(payload[label])}") + for item in payload[label]: + print(f" - {item}") + if result.validation_id: + print(f"validation_id: {result.validation_id}") + return 0 + + +def cmd_reset_password(args: argparse.Namespace) -> int: + from app.core.security import hash_password + from sqlalchemy import select + + with SessionLocal() as session: + user = session.scalar(select(User).where(User.email == args.email.lower())) + if user is None: + raise SystemExit(f"User not found: {args.email}") + first = getpass("New password: ") + second = getpass("Repeat password: ") + if first != second: + raise SystemExit("Passwords do not match") + user.password_hash = hash_password(first) + session.commit() + print("Password updated") + return 0 + + +def cmd_create_admin(_: argparse.Namespace) -> int: + from app.core.security import hash_password + from app.models.user import UserRole + from sqlalchemy import select + + email = input("E-Mail: ").strip().lower() + first_name = input("Vorname: ").strip() + last_name = input("Nachname: ").strip() + password = getpass("Passwort: ") + password_confirmation = getpass("Passwort bestätigen: ") + if password != password_confirmation: + raise SystemExit("Passwords do not match") + with SessionLocal() as session: + existing = session.scalar(select(User).where(User.email == email)) + if existing is not None: + raise SystemExit("User already exists") + session.add( + User( + email=email, + first_name=first_name, + last_name=last_name, + role=UserRole.ADMIN.value, + password_hash=hash_password(password), + is_active=True, + must_change_password=False, + ) + ) + session.commit() + print("Admin user created") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="python -m app.cli") + subparsers = parser.add_subparsers(dest="command", required=True) + + import_parser = subparsers.add_parser("import-reference-masterdata") + import_parser.add_argument("--reference", required=True) + import_parser.add_argument("--dry-run", action="store_true") + import_parser.add_argument("--update-existing", action="store_true") + import_parser.add_argument("--create-validation", action="store_true") + import_parser.add_argument("--json-summary", action="store_true") + import_parser.set_defaults(func=cmd_import_reference_masterdata) + + reset_parser = subparsers.add_parser("reset-password") + reset_parser.add_argument("--email", required=True) + reset_parser.set_defaults(func=cmd_reset_password) + + admin_parser = subparsers.add_parser("create-admin") + admin_parser.set_defaults(func=cmd_create_admin) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/validation-suite/backend/mercury/app/core/__pycache__/config.cpython-313.pyc b/validation-suite/backend/mercury/app/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3dd78d728eca970772ba9af936ca01be7a08b3e6 GIT binary patch literal 1536 zcmZuxTW=dh6rQ!$mpG1{i*s{s&J|3R3kgYS1W}xu634m7lQyLoMw|7<*|OesXV)pQ zcrZ^?;>9nO_y?4Kzzb4;f+I^P83|Ddc&Nk!QThWoGsbQo7-_#f-`O+Y%$##(Z`;}e z2*%$i{rDF*LVs}57@oQz-5!GR9ASi|U6hol6yvsQ*Ohcrx3Jy2a?(RRNiX%rP#+pY zSdJp>$#@n)lMm|4_{Pv;ognJZdE>s^; z=w{AloegWG9dhIO@9c7g}+t!+r{h-8WgNkurAt$BcR+ZSkJ6P zL%0`sUsIzWM}H9M$Y6#+`zFp<0F9^6cz+Q)&6F`QiM+Y9o4i@uP+LcfyD*77T7A zjuLALyh-k-Q?N&0Ry$RShQmA{sMstB+fz=U@T5qMQm=ZZm^p1auTAUbbb(sgqRRV( z1(L@{1aGdVH;-80@l%J`no2cL*~asWORSB%6eDL*-8swauOx(+TQ?mRQB+m4ZQ@v8 zX(q|*X3^2?QumqT6yQ+e^k;C~<%PMqxl)Uv+s@~Zp)XiawWv)%E~lIS7ymKDv+&{l zwbaf!X&jG*3&j&dS4s0cok|wgT(#A+;tAwCJCoH|Yhq(>XP<2DC3aG97kB{C@``Sh zB5%tjsx>-4H^0O}_4vaC#>WTg4Hl|TDJZu6)S}s+(0;s&`H3hfAuK>h9@1uLU2%%nRA{K;b%^<3{Wi-TH5ctj&g}`?3Qkg@9Ll1{O4p9yR z9ELcIa2N$(-GtcuimOC%9IBu2w1dzXe7l7Qu}67w11eyzJU4r(Q*-%G=M>nml#YJ6ztabk*eH>%O6@_AjqOgEt;`zqg#KjNc-+ z$9v;Py@S<>PhVy(d(!1(#a)xrSN*Z_US**s$F5>iRk`nnajrD7R2{wx^Wru6?)5;d z8egq$e|9Re v&H;rkNA@JZKW<5qenVq_q3G{u<&8HiJ(Qk(^9J$v%>)WYzF+>a6HcsoS^1= z%v8BrEL`G|vJ;S?)+9VC#xiLH&KB6J^-chv2qQsI^D4~6!pTTN@P(}K(KP_qQE9A4 z`B8396aZ4Fi1Ol(DY5YS$mhiW`jin&Q)qppTpxMQN1wGi_O^uXDqCNK+QH89J7$RM z+b-?40J>r=AIh})*M zpu5uQ^0;thk4@?GU~0h2R$$me_l?BWi~AREyl{K*=Hi|C{^>Ul-u^!GU8eumrM~gr zz{vHC+`Y?vqj0Ddj^GdEPEsb)m&7&z{C{GL_}_^i6S{<`C|6UZA`>2VtHiz1>d?4Q zD)c#_jE^(6MgB}golMe%SepHu(ArM-n3H+X&ce&i!Z73{v!A)~>h0B=t9Q=!&#Zm> z!63caOK;wr@25ZP8~H;mPfvpFQ)GleA<1|Jj^n?{C~06X zPNU*OjaQqdK}EZA)$==4kV-{aQb8fKg!E_!{uScc<9#B~D7NP`2^EaXvf(kfKy2t6 zN&+4i_}IV3!w^oLxB*=dPGb;3jS^-Jz4XS<)n6JvHGVC9 z+TXGdYo9$r=u5nz`~@}pzOr}uzH#E}h5ZX(Rc`GL7T0=G_Zwg!nqsIH*&iE7WX5^Sws+d#k=Qnc z`bY9KCJsXOs8;vckZwK;?5~Jx3I=bdR*@$q?4^jL9n)@9sJ=QTU;>Qt;E&O*x#UEkm>K|&XUxxqy literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/core/security.py b/validation-suite/backend/mercury/app/core/security.py index f6d1c4c1..dd03cd40 100644 --- a/validation-suite/backend/mercury/app/core/security.py +++ b/validation-suite/backend/mercury/app/core/security.py @@ -18,6 +18,5 @@ def verify_password(password: str, password_hash: str) -> bool: def create_access_token(subject: str, role: str) -> str: expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_minutes) - payload = {"sub": subject, "role": role, "exp": expires_at} + payload = {"sub": subject, "role": role, "iss": settings.app_name, "iat": datetime.now(UTC), "type": "access", "exp": expires_at} return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) - diff --git a/validation-suite/backend/mercury/app/db/__pycache__/base.cpython-313.pyc b/validation-suite/backend/mercury/app/db/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..334588473721f002f133e76fe5c9285bb1cbc762 GIT binary patch literal 1937 zcma)6O>Ep$5Ptqyuh-t}CV`}(q7dartwG8rQUi)WL9HTDHcAfhR%*ybmUq`1OuXLB z*bd2_A|XLcbLgcVV)HQ5Qd2t=f$E3V$4hPQc(su=H>ZR zQ30ixhICN{HI}93Ger%wq7Hg73t5yYX4+KEjH#LWL<)L_*a$PT7n#{p)}}EIT9AvS zIh5w7#Uoihmi3~n?^xEm)^EJ9PX>m-b=&uwp&hzSKftY8vqL9z8xHcRC~|984T-CB zC@i6ns|!wO0Gx1hbs$qtL4;qPb2%f#T4vc#h@Ucx`anDv`9utLx_(`9TP_>xV}N+i%s+HKXhC#$)f` z)Exk)W}UvZ-Pd~lu#w`TyT!$5gNxO5F4pmDtyh!8CR9#-!r?G7cUYN$U@{O*0i?1# z!pbRAoDo1VB}lKy=uf5$?f0WGrne6xa{V<-kiY5p1i)8p(NMW+#j#oz*DR~itVNg_ zi)SrsBeFeu1@7{eRd*o>J=b^qre#4IeRA!@6;~UG6oP5<0m}*q;HqVZA-I(&bOJmv zfF};|V?2jI6>6121%sPtZ3D#@6Jmlal26!P{=%29b@J06-+)V9^_f(pP>M@9oJ4vy z72}MOVTz`NWhl>xkTO$c#Z)e%rHN&~rWzTZxq0)wxdnI2_4fx+UOMBP@{Cn39A1f? zuL$^y*9uCLHQoIm>Bca{i-yQ0EE1sL;s+I)moU_1pR{vY8wpvoonX z9!mF6a~>|`RD#WnP*#?5X!P6~Vcpcm*r|wJCc!Q#@&9}JKfR}CB+z2-YFrr^(AP9W z=Q)}@Siy}}QXUsEz!Hrr8dL42PopXqII!t})rF(?2#)SFTgBc02d7vKo4^oZ0)23` z=|?TH;9Bx0e%e+P~-3Mx5}{N_hcpshh-`)ED{R2rH zQ)0WnnV3=%3FM7MdN&gK{UT zi6%54LmFy>q+}`lWI7BJ7eDK4tzuUt6KfXUL=)=8poHX!BnZM|Hu49%^q39)$qF4- z_>Eobuxn2grjP7ugl7qLQsBXJ#ykmQ-^=_D5 zC!{AXNU1=rdZ@}BE=au~kT@gw2P`Amj2v-5J#aHhrAnN5v-WhQ9@*8-``+7mZ{EE3 z&735YF$B-jdj0l25uv}B!k^F-2q*gh9wHT~LJd^~A`}=7)k0N~h&~o;QdK6hkEL3; z8X*xM%QdAMB~c%TYq4sa#H$IC06nHgvgo!%k~dPSa_JLDj_raPcCm$<9w5cVo@d%^W=(cGaY_x;wFz3amA#+P3%u#vfUypK2 zAcI+Tzx=@;h8hX3*Hat?vVJPS9Ptb|F6ID<{jRhX3p5Z8v^`!uF)Hw4T1vPk?+XBh=ri!Yh$`>PCFR&fugLxeB`uIeA zp$4y@5_)4c(SV+{LIspZ1$f)&eKC((LNUC3C;hQ*nTAilg4#1ZOp}{N*R)O7Bf961 zs~;32RK{K1v}mkl64%pgy^Cqo(m9j}FpefUJ2l2t8Z})_Z+hlFrVHI3Ol@{_yNv@5 zrwQu@@Er)O6C!~^;HV%fA9OImgw=^K`oRfd32DtIeVdYxC za&1_-*{|FjRJ49s8!Uf1nAeY#QGC81&yNx>J&T~&(ot@rplJGj{BC@lNRKnQVP>VD zSs7;5`6a>l+}0?Y8@;yj&zzD> zJr!fo*d)WkrS7JFSC*dTVTJp-d%1sIb{4+c%vZ|dFK=&N1o3M*wk1ZWqG`}rThq3o zHerIj9VXAo8evxhl;kr@x0)T?J@nf}=b&b5O^09@?3US{w(Hb$ ztBadGVtR+XD=^#GU7NLF+I~Mo*DD5+4H literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/db/__pycache__/session.cpython-313.pyc b/validation-suite/backend/mercury/app/db/__pycache__/session.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d74a48e0ea4d8eb62ef7ddb2b5a7469065a056e5 GIT binary patch literal 890 zcmah{&ui2`6rR~^HrZ^lYm2s}YK^Ue3uWtNu>}K z@#GKufzZ3)$(t9!Kf!BHYEV?9hu#WH!Gi}UKdQa?Lf(AeK~8YKS!gJrG*nO%E;ow}4Kx$2 z%F}37TOLbKJ)EtAr7Yw?UsZ)dw^Z>J7bGw&-O?t}dLGK%Qq|avw^VUdawuh?6MBqt zL5quo5^zEWf*39kCnPqZU5}D#E>bMQxll$rPeISQLt>@MNf>&x%WuZp0>*qh0AdHC zbge2y`Ibl9QPJs#tmF6jT6BbL1fCEuYP+o0^FsHqgS;R!2;pIv08@->MT|3zdbUzD zg>IpHXbYQSU6!))q2)knqAoi9Urh&Hl1`xxz94mQ6)y?5dgk^za~(gDU7v9x>?xUd z9=BvhBTf*^wOeypwH9nfr7j8WjKio2^}o%76v6^i=>7_JQ7?`g>O^S*Nq+5~C!| zx2u&%v+Yh_Xo+n@JfNuJGT$d|a@#ehNO10oXbcwJEAs@`@&r{!>B_rZ?}>3f0Lbz=Vw8VZPDB-W}Id#v5}$z?bxXgou{ zAkv4}7u@Rr literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/models/__pycache__/__init__.cpython-313.pyc b/validation-suite/backend/mercury/app/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a882f21346b345e11f539fcb3374fc7e39bcbfc7 GIT binary patch literal 897 zcmaKpO>fgc5QcZ14}Zl;oTQ=jfP#c7f~w@eg#+S1%7HZ?+7zyq<*bUS9lK+@f+Ify ziNC;KS}7+^+z=7!mEB2NIYPpcAH6%Xvpe(d+Hou*zZ2{9`p{2ev{>!BJ={bQHP@uEd-1f#?NUHc8S2 zkMl{maSCq`{c2+C8(u5iN@jsBVkbKli>;^Fo)}ar_>IHZ^iGi+8;8V6b~?*8?&Eag zpmG|Zn|`rsVM6SEZJFXlj%;hSW)^SJ*V!$=8`?2+%MFn30LYur09<026|w+j$hA2 z@|P|!v+uOnPL$UIFr9uq8|623&tu)FY{xS-Tr0Gls07SPRt`)LY2p{U&!Wm*(`J64 z+uo+RZ8}l$-o1^Rv1lX-B>48k(*i$nn}DDZ$Y=s&-^UY!*v>7Ib{@}=)kH1V%xHP7 z(9A$VliHG2T*4?ZqlgUoD#qX=ok5OS$|+VU^`e&gmXyh+J*y3Aq$yNgQBL}hzdNiObzAHc&%Nmo*{JNbo2 zE%rjoqKw9b!yZObFLVsv9+Q9hk{q*j)0%u8%iaJf=4&i|l*2A! zKJHxU=*DB}xn799!Th%|m)1}6Uc#7ZCVOP7J`+ha&VkZMrqp)3p~X-xQTim*EuI}} zQHleLN`W^&%yPKE;Sz^A4)1Z0I9x%9 zW+}zq4NQaT0k(f9#53UX1@s-`hXi~AE0`p+{FReSX?QzOMqdp4y8Isyz9)a2yE5E- zx$x26{e#Nk?fawU2Zu&~v@jU9p3RNL!O{GOd!Ih9?SD3&Umb2d{bnq#9@Ren_54Bh zx4Cic`tYm6N_8w=KdLV8^`2|{kH^((!&`^S{8+q}*zW8<8dsYqw&wrwUe;C)c1JZe zj`h2!0&lT6&8>QcSGyMmAz*zWi;hV4pIEwO44A!>)RI>%s=Tu{T(BX}W5o65Wy3#+ z=QX;WMh721@Y4hT&4I!pt_rVRvhfEfMbhWV6|ccEs{G;lb%ejOf*`ygi?2xiPqH>5 cYp=-ah^)TO%R)xdtgD@i>nTb zBJ3QN6(t8!uv&8z8wh>i+lL?Vh9Ci_U|mJCPsyE4%~)GB2TmVwb9M&cOY$T?Qos5n zJNU&FS_s`;Dd?OCv%d$)=uW|Ls20jrV&kO3!7qNMlde$Ggb=MH1$Gh^_E{;Yj9BOu zI3Ye4p=lujoaC!HA(v)^6tL1=tW4+)CC&P*Y!@pRdP7NbJ}ckF>IuD}qUd5N$*+gM3nGAs;xdmw0>n;`+~cg4$w1ku5I{tbebp%-JSQB^j# z)14UGp(`GXHM`yS(p3DsxMUiz2nT*eaoPj;mubs{y49dDt4ezlEv&3MTkQ`}jaeHI z(f-@l6M_SdCpt&i|M2bT&Xvt$pfrkIr zn1g=C#-n2h;|Q-KOdy;5k6T7aFM}E<4vM zPVcNey1skMEzZ?1H_}I6*o58NZt+~lcCH1x;r8rb((cqk{r$!uY-Zt=w&L`|s#~1x z#LYfE@#bT`@sZX%q1UfAa>rd(Z%w`R_*UZ+qd8^PuQl?Mzp`eK4@^a=59jb&&8bnC z!!!yek5%xBN!fQaNs~7eeJeOF|4cB~Uhbu9#-uupgE2T)JOH?|ENiB^I@KubG;}uMR4%`L3AIEm$(qz2=nv;ka42g?*RN6WfQ*|O)~$S poNbb`2V8_1x;OhZ`(5&z-3=9AO literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/models/__pycache__/device.cpython-313.pyc b/validation-suite/backend/mercury/app/models/__pycache__/device.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9ee4c657c07cc8d775f8f76719dcd1e329b7270 GIT binary patch literal 2076 zcmah~O>7fK6yCMh|G!C`5Mx42LK2*mfCw}VDHf`dw5&-=yQT*)TCMGg*|I-ob~jB- zRryv9J>*iUSJVp;mmqpD;=-{)02{C&ij@TSiin1{rW8*l^CUC<37bqp!6i%^eoM!uQU!EGK#wad$oZ>>2RHd(y zzC>byg8LicLS!Gz0&T?vMNxG@7ga?gO{OmBs1UH2B~n|_6%`|~p_I^;6}qPAsDZE* z!1F0kOk zAud=6bD=5n)6<0ir-^V8qKSH%DCsBWYTO#f?4IIpQN?Xl;tf^O?Zd5c!X7d8hRPd} zM9y&O1wT&Oef4>&l;@x3`boqC9@FPB{al7HgC29_Ghq7+c?`_*nCs`(IAcdP{1(-q zt;%|;A1sdnLQ8KGM{N+yJ6OFOL zxD{MN8)6OZ$H-4BRJR*iD_%1+U6m2mX2p7$WN<)K>S(hSbF%WbrsF$S%oXusy%m?# znvg z4Mn_fAe;d%p&={^60aC?6=82EQPI7jqFYG7eAN&oy)|$c3LHUCkgBP&ENU7#pQz5O ziZxifD?oSr2Ang*OQKFDzyB)?nAUV8$b17S2-{P%QeTkHFt1}I=rU4tD^XW##7%LI z{QeK%7S@brQxaij?rQB&YlRUj z%=3ys;^M6+&y$5RB(UQ=f8P+KLng)Z>mt^4NmP)c5<0>2x=@vn!(-TD40iw?g!qX3 zKerUm;~}8J0K))TfE>UGKnUO@0Ni+Z6d(^U1~3i~0XPjX2>^M9;{XK$)*#Q5D@qq@ zyr7f2u4<4KAfrwb?tpxhl_59XIXelobI$(HHn(sep=^NgwbSHY`kMMBGtyptHTurZ ztxnQB`{9rJ-e|d9+Rm9wxi>lYl*bc583)cDvN=8#kHTz5Lmo^#XX6M?=4;M{l4!l=7bI(fV9j zJRf@Le~~k1mb*-*J495=z3KVq!H)JKW==1G=ak7T_0C+{t(#{S+pFDy3FvwJ{L`V1 z|5?r)ztaAwo6MWc6*8U2bu&-m?#4z);IF3NeJSou-vEB%jyLOh?J8tI#Y^;?$63wbu#_$|FIR$%6p1fvr zwqBjBk_QPVfJ*|T0PH=4jdsGCBbF6@-$~;lULZ|)ZnSd*{_@i_{Tns*CzX9o6@Q|N hzf;9Ms`#3k-=pUL4))WZ&=1So3*RaKP+;2~{tGJIB^dw! literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/models/__pycache__/document.cpython-313.pyc b/validation-suite/backend/mercury/app/models/__pycache__/document.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd86b66ee1d9536bbfdd5ccfa62aedd38db018e9 GIT binary patch literal 1479 zcmZ8h&2QX96d$j>Ua$8vX;T^+)R2B4(~@vWs{{lp3aIsxQ0nv$vFXU!oor3l_U73x zNlz;wL5~$urE-F6luM8BUnE+Dwj&{gdZ_k>v_Tx@%?+hW&#UTB1i6RZZL*+5Ch$YpCDSZTRiT^$eLSAK3xp z<#oqpL1??3RcF`nc>eb78!NHeNOnt58j0a~R34K$0m&km0tsX*1BxX>)>1&VvXHY> z&{{b|;}a`>BXXG+e)g5eV12K{1_hkWwMG<#zRQ5CO?KaDFs|RX+fFl%f#=yyrfI{JKP4~poFL7YA~h42mOSEjqy`=zPw zC;f>xx@Nz6=Ia}9Iwg*!Zd5gC_zSwnQ+EM736y$vj?`8BbBm)iTe77rWk9pC^}MCN zh4W&Lj=l3Lr(kKw$&YdLbe&W}F=iFUSjE(vN+`vw(ip3pdQ%DIm{k!hs|uCYgfYpr zwA2HUPM%NG7p!B4W*x7|b{|Y9jrc`M{jV!Fx$cV&p`mDzo4CM(!iXD$u*tQ=>of=d zh{d&Sr_Ds+2fJceZ1`S?-4f4sjxmG0i0uV-i_wl9-i-^qW>nz>vf-qFDFLF6yhLee zZzaoNFXV(n+`pveX9Wd&>wh;4Nur+crhsz-E(o|N;57jv{-hD6g?1(dVGlfnZ%O~; z+3xBemzRD%fB50e-ty;%8=FU$?{v+>s?k&KJex80FFu_-c>AZ7qnT^n>kseslxvCc z#KEQB%$3A+<=N~z``b^ggL}Q%a~Y1{3_UFPm3!HWoa`K5K<2NzT{#tFg~r_-5lZq0Au`vKwwgS-IomU0;@mLxBZ zd^{Umdq26rAHV{VMY9J>2!G0wB>hI_o|D20a_)EX?hA7Dh+I8Z6=~*){AB%yjqf*( X2@a0SIqAKJ;iHWo%YPA67%g5Z<-d>wi0O{tIo~KohJ|r<9Z`Z3HTDD%B#W;Aq#oGB-}{;Gy_q*} zX5MZ*9ue^Saq8;Lbx9DO)4=10S~jfW<|?EFXt>J^_h*5|U|QSQrzO;1NLy6?=-ls|ip2 z%l0fG8GVvW6WJDg-45MLbmfkSTD>#-a zn#D4HF6FT0(wt$}xvW^A8`X@D%kx^Z$tpZvcU!gGsM&Rc=pSn)WiXQ@s1hNucOtHS!9G<3iX)BVvJKJFs@xYN}oWM`qy@|1KFal0sYJpS?j0r^fRXDMO)i%k0=u0(OKSeKRPYDp?_lq3$3 z^th5_S3*|ycuC1@Z=Kh|fV=x3 zv6LYgtPW>Jc(@xJJ9MQy+>O$eflhK??=mRc7P)kblJ0vXucY=FrUmJ5(pMS=Tja62 zX4p$wnGz9rz>WO{DiyY&FWZai%MxRe;I^zF;2^mO-)m$jN+ z!USg(wPEm-H^f4vR&0iIJD<@3hRO^qeMv7PQ>`@0Hq{ALuV5_;R@jne*Q}MHf1j+A z7>it{Y_?5(4}**cun;nHKPiR|O9~!WRZAJ-0FHOkHj(rQeksoA^Jjw=T6~Z#k0wAN9XU=R0+R zPL=7W`n+VjgFeTme1(5G8P*6hDS_=OW#!aQ%F+;_=W-=i&GeW^fr*Ci8QIzx<*o zJ`={C3(;r7$O|E}DP;Z-PQMV&Z3^f1WWPALI(xtW;mCuLJpl)MF~8WeTE0K^aQeYC X&GhWWqT+>BX;uH>*xv$nU3dHgv(^}= literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/models/__pycache__/location.cpython-313.pyc b/validation-suite/backend/mercury/app/models/__pycache__/location.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47e90ff91f06c9d5bc1d55b52e1a6f7ebdafe014 GIT binary patch literal 1438 zcmah}&1)M+6rYt=(n>4I@n@Xa#Ij8!l(sUsgdkEJ602@2RfcwxLSu<#wHnLYtag>R zt6)1l=++!_={==A)Z~)Vi;w*iBo<0Fw3LuT&@JHh-Zv|+Q$wK(`px_J&71e3nUhp1 zLGW{g&%e48Cgc@Yf-g1}gp&pe-x7r=LWPtC5Heg2RYK)3gv%m`z8EH90?k{6h)S(fc;u3=kF zKEx7RHc+EwRpnVN&lpuHB~K6pSW|ZQfQkZc%VCu3w0(3 zJ5{4Odd9)Sko7s zBeI>JW|FG%Tve4NRJCn4yC#>Ds`{|2nWvr!RoykfaZSUbmW}R|s=8Xkq+>tCFli7u zO!M@S{;xZss=)6J%yPKM;Sz@h4vQQl4p%s25ZJ7$Vmn+zS2Y)}Pos-9V~l0=o#2nK zIlMw^m?<@#wV>ES=KxaZ^nnl7`2pb@^4rW(uljQ7-J|=*>HcT;2k-6l_10i%uUC1x z;E8*~`K#aU{-}Kaz?)z1m7cugiR;7V!qKA_`t!%$^3C2>KY7IyZw|BX|Fm_y{&U&O z-s)BQldGP1YluO=c(M6>$6G4)O8vy5Cl>$9_HuRY`0-%1()+ld&Uj*Fm|g$n+8|r) zebi5_{wh`>AEdpVPxBe&U-&zYcv?0o4_*3A` z#3!lKDKr~}2EHc1y8-;&1et^1CEm>O!Pih?(g)+gZ@?O={3URBR&T4ui#DjrOEY_hNE>VCx8jhHIZ zn>~6dcvQUDn|Sh1@R$-1`#?lINN+;)?!4W`FI>p%%=~_{^YP|on9pkjpFp~PXC*<% z7h#MZC03+i6O|2O5JRdHT>_~j^h7qa1W(VKoer~j-i_Y0*NMi~@|c$I5%SvIu?~Om`=PUaodx zD_GSmcHeHXpkiA$aJVVkv)F^xY&^BwK?qAc6H};d?K~I3vS+$%b>A+<_QKMm(t_`? zB|aW9x5K~;yTE9RIf2~`_{56uSa=vT#8Fe%_PQYp__zhVf1R6T0V`E06$vQi8l|pp zbseGSD81J;on2&%(q$Wh(6K${`3UDJ4b7&*VjNVg6J#Owii(l`$I?&=X#tH3Ax;)1 zgxD{{0Tg^crMMEI-J)iQd(;#KW6(jwhWJAQ?$QjFh8@oAPO{SJK_0P4h#3cBfu4{L z6GztOKTjUtJpbAmoVfDt^w#9!+T8lXfxNg~oPM$V%6NHqP^_-i)=v!N>UQPA+iQbL ztuG(!A3{fMd*hpy>sq+X}4QA_ysLDKK#PPao=F4^sCnzhL`UeIPMcp-~>aE3mAx@2irBl z8d3w-+F)&fCw!oe)CG*h7^o-p0TVF=%*4$6_@E_VCDwqA*jT?VXb(7uBj6;?fQz_# zxE8LD6O3(~P(SaOcg;I5)M@h)H?x?)Vy>_>FpC8&)(T4_v)I65udp;Rivujq`R4hW z8*h~!vwXe47iT`kd|lw{{z2as=Gy?ijTJMtGD{O!nky`AOEq549!};pP-`TWN-Gg1 zo=(XjuaVY|rqc;2lA@OJG?C&!(tRv?LMA2ZY%WH3|xerWK$&|O2@}m)1qKVUq#66pl}I8UV-OIP)n#dKAGVh| zh_omXDHWB(<%n{3tA^Icq{T=kp|HDN*@_b`0AB7;Vrlh^M4f8q9W4R3ZxrNwX7tdSXIxjIB`=4**pZNh_M# zQFNl{Mu7oDj)S1hq6mOf;!!c809F?=iX_8L{a||u{~YZEq@IDLNeTH(#TE}PuM$W` z922+<`F9{b=3X=nWM}@-b9!rWN7%miy#K3MzDLMTub)zRq0n;T>EfpF={>dOY}WS} zRA&qAZ*F;ZYPQ?d_Vd|^$G27feBt=ppL=#|pS3^t<&RHgXV*bFRp>kOxp7B+W>x#H zWkc&hmA?kJZXVn1e;&)X2eadkr@!Wd1QP~ZQT3SlH<*8P%+vvbDjJp zB$VR?eU8S977OKgvFZ4?GyL0h{5u4@;DEWEkOWPFOV{JL?$TXC1M_jQdmD8hH;bJn zh@A#VXePnKV&@oXTx#*QQZr_v(L_3WZ_fieL9Gh(#RXKzuNo^}usj%#g-Hi)XvtX% zIWZpFdxE!{*_|wSFd5N0C9WhSS|3eA9!x2u29yvzy>QV3FP>8FP-~jRBqFkqByC_` zt9@tiZ4IYA&?S8+5TXo=A#W2oft>*q_(TLFlz36F2*9rqIfvpT3KkF-uyqNA8O3E3 zm^{f<6f7q6NFb+Bhq%`ebQ)V{N&+8W{v!xPcQ2s3`>m}Ha+hb+)1lnW5Axl&v%&Rt zmA_r+Jh^po=lJ&3-B`YJEIal1k;;!1S_hu4Y(_tQq_&>R`X66a`Ev-|CbfM~!~0;N z`>oAeyODhNb#0vMdQ9xj<=e+Jud(k%J1+BQh;}QWuZ(HLuDyia(zTfR9XfjLf>Uq- z@(<*F_MLJHjZE(nnqbss-QUU3?g6yBndKNmdkbk;YW22JyY{6JB*?kx)E;7rI*TBe zHKk0>g7yH)H4vRgS8LG+@y$T`s-gVx==rE8luk*I(Nme^f<(wgxJ5%7qMi()xPk)F zHqHP|-OG_BNn9XO z{u2nq_=(qG9I$_LXKefSb3Whg*8uA;^bT&V>?~}rsl8XS<2hTG%3rC5`y)UfIJYzL zd@er_%<;|ZAE|usdr^*Std8=3g5~Fu)9391@*c?GfYUOrZAE=$Mt2Er=I<67z`vZu z8}uyRcqofEu`KQ$yDLTSCE{|K1P#I-(8g#(elm@O8MDtGPxzXCEPvy@-Py#{ZD)JT@IPAU zIlUR%_2qj;vi_X4Q{_jh<@xFa23L3p>_sAPc!~VxFxZju3$>(9;E7SFL&8FU;sc2{ z5zCUJyoc$DS|?}6CMW#UBR+qKIz~bx)9(v@aeBl* zHy)FtUnbH@TzVivQjD!qGf)TMyGd}WgQS`!)CtTq61|Iy z7t^W4YSl0G3_J-zfsZ8jfcOLV!r76XeBtWJPO*IU!qovSTT|Bm!qfNrS?x2Aw^ZL} z9OL!-ykzv&)8-?48^+3D4DP0uTT`Lw*$Ht>xT&|_^^H?cQ9lw6kBm*u_(NfFHZ=Wy zaqmo3_s;Slo=T*b#HhSNOYP;@BJ~vYKW3BWVNnDHu1;SFb&`v@fVFi+9OWH}7QIEzDh%~7r?E9thqY(m0)4acx*_i5$l ze^BF2llW2`!U@Vmttcd2sJF>VnjqW2qIB;%s*Lj8$k_`Q8I_@qA`(qLrRk z3T?aEajbG7ss{_uZvB9U5%u!?4eXvW zr2*`$;BPT5C1UIZhm4@f zOd=1okx2{oU8yul!jaLM6f3H+(xp5cNGUpnf^t|dC9HBfL)baek_6R<*1-y0V^w-d zK`rbn8N)-T@dFg=JsjQ{O%npejXo(BwIOfIkBR6Na6J&w53u_X1uLLev9*Tc5ei1R zKEl>-Q2Z7JD>!f&1kZx12EB|16!>yv3Ic=v3KM&VKE8+Ks_WF&_|CcQAY_6rUpBCARe4{b|DBzFwf|CfD(CD~`AdaU zm%s2lul=%JJ>}2wO*zk-D(}~Gh}wNQ>&sa>RQ_@S%Mf+oS~i$-AIF0w==;=xp<;gs zX4{b9vWNxERI1c0|WcGO&VT+?Un@cc#1_re~yykph;8*^8+-2BD^F+S%kU z$;U7PGEw=@Aik?L7z}^o`o7_;-*OXwE_N3{v l)~Bue9CY?=X2W$u`L63Qvfbb}Y=oZ-ZKnRkVVk{%{{r6l1a$xa literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/models/__pycache__/user.cpython-313.pyc b/validation-suite/backend/mercury/app/models/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ffea153cacb68a8cb79f356d4624f37a06861d88 GIT binary patch literal 2229 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 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/modules/orion/__pycache__/assets.cpython-313.pyc b/validation-suite/backend/mercury/app/modules/orion/__pycache__/assets.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0833fc5be8e70461e28ce1606ec59c6e3f5bee43 GIT binary patch literal 847 zcmYjPPj3=I6rW|6KV=2tHbJ9_ZirCUq!2VcXh;*ZRA?(SyY*Cbvn?at26k~~m(-s6 z5ll=y`31cBQFem~W>OEvc<^RIygRe7g*m+Wy}$E*@69xoN&d1sG5dIxeyZj~%2WNmAF}y3_!5 z9kC^m_>gYAZyq7GOp>5(%8xqP|0S`ZfuQA(Oj~WXTY62?>IeE(Q$=hOLA!|kz2GsF z4hmFfgg4PGsM$o&Sle@fX8XCF%wspw0^vGi~vReY)Pwc$(Ei_G_I T-YZu9Y}Es+|6)A%C@B3OO9;C8 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/modules/orion/__pycache__/context.cpython-313.pyc b/validation-suite/backend/mercury/app/modules/orion/__pycache__/context.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff5671a67e424c06288db7783f5aad42e444850e GIT binary patch literal 4813 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 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/modules/orion/__pycache__/template_service.cpython-313.pyc b/validation-suite/backend/mercury/app/modules/orion/__pycache__/template_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1267f9292423ceea19ca2eacf3888b98ddc32a9f GIT binary patch literal 20373 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 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/modules/orion/components/__pycache__/base.cpython-313.pyc b/validation-suite/backend/mercury/app/modules/orion/components/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5080d6694b93661f5625f5e96fc80dc981f20d79 GIT binary patch literal 833 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 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/modules/orion/templates/__pycache__/report.cpython-313.pyc b/validation-suite/backend/mercury/app/modules/orion/templates/__pycache__/report.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b83517c0219b14ab9c87b61ee2ab5f9ca92e1722 GIT binary patch literal 3383 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 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/schemas/__pycache__/auth.cpython-313.pyc b/validation-suite/backend/mercury/app/schemas/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c42a344df72f1849433849ab67b5f249075c5ee7 GIT binary patch literal 1774 zcmaJ?&u<$=6rT02?e*G@6T42E2-0AvU@3Aq02LAfsnA1hVoGZV zQgNtMsc_7tQV;wC{V(_vkRnyqNIjtn<%Sd^uDmz94grN&+Hc=`^J8be@4e@fdfj38 z`{l;YH-8lv`;DB%ub3Ie$Xlx2B^tjMmT0t&f)6 z#xZT@8mbz^aUug5C9y!)9tM)jXu{oss@x0&-%f^ntn6EpAR2oTXv$r|VJ8`Lw4GbA zjO4)%4~AiOz6|rj8Yl0f|CH@AFgykY&j9lp60vJ}=G!>pS!fltRf6KE?AogKWipE5 z9ezLMLLOIf5|j{Bk<$s3DI_R+8VKs-3t;b)@mR6o@!{x>B`Rv@p;fvUTqUI`R2)Qb2u57d_+H#;Y zs=75*zmt5+ku@Wsn z&hnt_2^h85bhNzfv=M9?PKAXp_Jva&m0A!~gBbRO{<0+Q2gA6f0$>c*qq zp?PF&%$A8Pr}<=%Eu=N)lNIC0MP!|(5+zkh(}Q*6+1kQd@tjkus;wH*i*?~G{5%(7-T`UdHA&3&|e_}P)=&aQ9$YF&PI^@X*SNk+S! zFWCn;a)D&j2P{oDS$US{eW3lh=IQIa(iaV(gF9vdEQcuokL4+osuuGH|9ER>JwHrh z6l|jVT$a(Gi{w8U+okOZ=sSW^9En+7JF?dDyuI3aVRf==)3kxRL`~;jDpSCw1Jw7x zLckt%FbuFchCqJ`Sr2Pq2kByi;$zX(rKPi_NzuHobi-saNn)^RjqWYFhO}P#Z2vy5 zvOmfC-KTH^R~Yz+FA)AP4a0cJIO!>a>%QeX29z)8lRcGorC!}ar%qKlh~q>C zGD_kMT)P!WE~755my~lU$oS2q#XHKqoW!e9`$`l_NLbf+%taI`Yo(v^yMa)2B`gjv zaB>qJ&4K@d_(V{Th>}Nz=Ig+sNsm|7J{mjzOa z=s{2NY?CzWma-X(gDz)Gxr}v_R1^T~IlKhp5>gO;2axL*5Ot(_zx>Ch@7XFYxPfKqIRFtdoy32RB;p zX3h?7h}rG*4lyRJqN908?-b)0Na5?Ab{sOE4wmuTGIPrkM%%7C%Do!PNcNX`(At`U zVx?tLs8ZAtD7A7!!J%(0prhPgiakU-C)C_D1#S_$5%wTV;~voIkJbbGy_iA?3Lg8= zs%-as_Nqz^S`x&+VWc#Yk+K zXDJLT-$J?9tWK24dn@;q5lP<7lorKO84^aiqx3XrbLAkiEL!7*9^w$}`^6lSl%^g4BhJB`fzG`y;*R@!e+gSQ@GlbJpofFu+2p`?6ltspkJZD->#TH zQ})v+ZVL+|HiCnY4^%F4{sBAHSN6qX#uvp&D5mgaR{(x!l+sV+$TxEEJ2~;09Q~q~ upIsi@ctXb}rSs4FV*; literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/schemas/__pycache__/domain.cpython-313.pyc b/validation-suite/backend/mercury/app/schemas/__pycache__/domain.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b32962fcbe5b03e7c01339bbbe111f2c9eab203a GIT binary patch literal 12527 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 Q_Wr9xw{q@ZhA4FOKWLxW`Tzg` literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/services/__pycache__/domain_service.cpython-313.pyc b/validation-suite/backend/mercury/app/services/__pycache__/domain_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb96ed9faa7f1272bbfe03bc8af9ed8bb1cc82a7 GIT binary patch literal 5533 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 B3<&@L literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/app/services/auth_service.py b/validation-suite/backend/mercury/app/services/auth_service.py index 82724307..0b37bfb0 100644 --- a/validation-suite/backend/mercury/app/services/auth_service.py +++ b/validation-suite/backend/mercury/app/services/auth_service.py @@ -5,7 +5,9 @@ from datetime import UTC, datetime from fastapi import HTTPException, status from sqlalchemy.orm import Session +from app.core.config import settings from app.core.security import create_access_token, verify_password +from app.models.user import User from app.repositories.domain import UserRepository @@ -13,14 +15,16 @@ class AuthService: def __init__(self, session: Session) -> None: self.users = UserRepository(session) - def login(self, email: str, password: str) -> str: + def login(self, email: str, password: str) -> tuple[str, User]: user = self.users.by_email(email) - if user is None or not user.is_active or not verify_password(password, user.password_hash): + if user is None or not verify_password(password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) + if not user.is_active: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user") user.last_login_at = datetime.now(UTC) self.users.session.commit() - return create_access_token(user.id, user.role) + return create_access_token(user.id, user.role), user diff --git a/validation-suite/backend/mercury/app/services/reference_masterdata.py b/validation-suite/backend/mercury/app/services/reference_masterdata.py new file mode 100644 index 00000000..73a1a891 --- /dev/null +++ b/validation-suite/backend/mercury/app/services/reference_masterdata.py @@ -0,0 +1,380 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date +from pathlib import Path +import re +from typing import Any + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.models.contact import Contact +from app.models.customer import Customer, CustomerType +from app.models.device import Device +from app.models.equipment import Equipment, EquipmentKind, EquipmentStatus +from app.models.location import Location +from app.models.validation import Validation, ValidationStatus + + +@dataclass +class ImportResult: + created: list[str] = field(default_factory=list) + updated: list[str] = field(default_factory=list) + unchanged: list[str] = field(default_factory=list) + conflicts: list[str] = field(default_factory=list) + errors: list[str] = field(default_factory=list) + validation_id: str | None = None + + def as_dict(self) -> dict[str, Any]: + return { + "created": self.created, + "updated": self.updated, + "unchanged": self.unchanged, + "conflicts": self.conflicts, + "errors": self.errors, + "validation_id": self.validation_id, + } + + +class ReferenceMasterdataImportService: + def __init__(self, session: Session) -> None: + self.session = session + + def import_reference_docx( + self, + reference_path: Path, + *, + dry_run: bool = False, + update_existing: bool = False, + create_validation: bool = False, + ) -> ImportResult: + if not reference_path.exists(): + raise FileNotFoundError(reference_path) + + result = ImportResult() + payload = self._payload() + + customer = self._upsert_customer(payload["customer"], result, dry_run, update_existing) + location = self._upsert_location(customer, payload["location"], result, dry_run, update_existing) + self._upsert_contacts(customer, payload["contacts"], result, dry_run, update_existing) + device = self._upsert_device(customer, location, payload["device"], result, dry_run, update_existing) + self._upsert_equipment(payload["equipment"], result, dry_run, update_existing) + + if create_validation: + validation = self._create_validation(customer, location, device, result, dry_run) + result.validation_id = validation.id if validation is not None else None + + if not dry_run: + self.session.commit() + + return result + + def _payload(self) -> dict[str, Any]: + return { + "customer": { + "name": "Urologische Praxis Dr. Durmaz", + "customer_type": CustomerType.practice, + "specialty": "Urologie", + "display_name": "Urologie Dr. Durmaz", + }, + "location": { + "name": "Praxis Nürnberg", + "street": "Wölckernstr. 5", + "postal_code": "90459", + "city": "Nürnberg", + "country": "Deutschland", + }, + "contacts": [ + { + "full_name": "Dr. Durmaz", + "function": "Verantwortlicher Betreiber", + "notes": "Arzt; QM-Mitverantwortlicher", + }, + { + "full_name": "Frau Bal", + "function": "QM-Beauftragte", + "notes": "Hygienebeauftragte; Sachkunde A / Fachkenntnisse Aufbereitung und Freigabe von Medizinprodukten", + }, + ], + "device": { + "manufacturer": "Euronda SpA", + "model": "E10.7", + "device_type": "Dampf-Kleinsterilisator Klasse B", + "serial_number": "EXN250688", + "year_built": 2025, + "commissioned_on": date(2025, 12, 5), + "chamber_volume_liters": 23, + "steam_generation": "Eigendampferzeugung", + "water_treatment": "Wasserversorgung über Aquafilter Euronda", + "documentation": "interne CF-Card; Protokollausgabe am PC/Rechner möglich", + "supplier": "schubamed-Medizintechnik, 92421 Schwandorf", + "notes": "Sterilisationsverfahren: Konditionierung mit Wasserdampf; Konditionierung teilweise oberhalb und unterhalb des Umgebungsdruckes; Chargendokumentation über interne CF-Card.", + }, + "equipment": [ + { + "kind": EquipmentKind.temperature_logger, + "manufacturer": "Ebro", + "model": "EBI 11", + "serial_number": "15102807", + "calibrated_on": date(2025, 1, 17), + "status": EquipmentStatus.green, + "notes": "Bezeichnung T235; Messbereich 0 °C bis +150 °C", + }, + { + "kind": EquipmentKind.temperature_logger, + "manufacturer": "Ebro", + "model": "EBI 11", + "serial_number": "15211066", + "calibrated_on": date(2025, 1, 17), + "status": EquipmentStatus.green, + "notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C", + }, + { + "kind": EquipmentKind.temperature_logger, + "manufacturer": "Ebro", + "model": "EBI 11", + "serial_number": "15102538", + "calibrated_on": date(2025, 1, 17), + "status": EquipmentStatus.green, + "notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C", + }, + { + "kind": EquipmentKind.temperature_logger, + "manufacturer": "Ebro", + "model": "EBI 11", + "serial_number": "15211067", + "calibrated_on": date(2025, 1, 17), + "status": EquipmentStatus.green, + "notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C", + }, + { + "kind": EquipmentKind.temperature_logger, + "manufacturer": "Ebro", + "model": "EBI 11", + "serial_number": "15125738", + "calibrated_on": date(2025, 1, 17), + "status": EquipmentStatus.green, + "notes": "Bezeichnung T240; Quellenkonflikt: Referenz nennt auch 1525738, mehrfach belegt ist 15125738.", + }, + { + "kind": EquipmentKind.pressure_logger, + "manufacturer": "Ebro", + "model": "EBI 11", + "serial_number": "P111", + "calibrated_on": date(2025, 1, 17), + "status": EquipmentStatus.green, + "notes": "Drucklogger", + }, + ], + } + + def _normalize(self, value: str) -> str: + return re.sub(r"[^a-z0-9]+", "", value.lower()) + + def _upsert_customer(self, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Customer: + target_name = self._normalize(payload["name"]) + customer = next( + (item for item in self.session.scalars(select(Customer)) if self._normalize(item.name) == target_name), + None, + ) + if customer is None: + customer = Customer(customer_type=payload["customer_type"], name=payload["name"]) + customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}" + if not dry_run: + self.session.add(customer) + self.session.flush() + result.created.append("customer") + return customer + if update_existing: + changed = False + if customer.notes != f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}": + customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}" + changed = True + if customer.customer_type != payload["customer_type"]: + customer.customer_type = payload["customer_type"] + changed = True + if changed: + result.updated.append("customer") + else: + result.unchanged.append("customer") + else: + result.unchanged.append("customer") + return customer + + def _upsert_location(self, customer: Customer, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Location: + location = next( + ( + item + for item in self.session.scalars(select(Location).where(Location.customer_id == customer.id)) + if item.street == payload["street"] + and item.postal_code == payload["postal_code"] + and item.city == payload["city"] + ), + None, + ) + if location is None: + location = Location( + customer_id=customer.id, + name=payload["name"], + street=payload["street"], + postal_code=payload["postal_code"], + city=payload["city"], + ) + if not dry_run: + self.session.add(location) + self.session.flush() + result.created.append("location") + return location + if update_existing: + location.name = payload["name"] + result.updated.append("location") + else: + result.unchanged.append("location") + return location + + def _upsert_contacts( + self, + customer: Customer, + contacts: list[dict[str, Any]], + result: ImportResult, + dry_run: bool, + update_existing: bool, + ) -> None: + for payload in contacts: + contact = self.session.scalar( + select(Contact).where( + Contact.customer_id == customer.id, + Contact.full_name == payload["full_name"], + ) + ) + notes = payload["notes"] + if contact is None: + contact = Contact( + customer_id=customer.id, + full_name=payload["full_name"], + function=payload["function"], + notes=notes, + ) + if not dry_run: + self.session.add(contact) + self.session.flush() + result.created.append(f"contact:{payload['full_name']}") + continue + if update_existing: + contact.function = payload["function"] + contact.notes = notes + result.updated.append(f"contact:{payload['full_name']}") + else: + result.unchanged.append(f"contact:{payload['full_name']}") + + def _upsert_device( + self, + customer: Customer, + location: Location, + payload: dict[str, Any], + result: ImportResult, + dry_run: bool, + update_existing: bool, + ) -> Device: + device = self.session.scalar(select(Device).where(Device.serial_number == payload["serial_number"])) + if device is None: + device = Device( + customer_id=customer.id, + location_id=location.id, + manufacturer=payload["manufacturer"], + model=payload["model"], + device_type=payload["device_type"], + serial_number=payload["serial_number"], + year_built=payload["year_built"], + commissioned_on=payload["commissioned_on"], + chamber_volume_liters=payload["chamber_volume_liters"], + steam_generation=payload["steam_generation"], + water_treatment=payload["water_treatment"], + documentation=payload["documentation"], + supplier=payload["supplier"], + notes=payload["notes"], + ) + if not dry_run: + self.session.add(device) + self.session.flush() + result.created.append("device") + return device + if update_existing: + for key, value in payload.items(): + if hasattr(device, key): + setattr(device, key, value) + result.updated.append("device") + else: + result.unchanged.append("device") + return device + + def _upsert_equipment( + self, + items: list[dict[str, Any]], + result: ImportResult, + dry_run: bool, + update_existing: bool, + ) -> None: + for payload in items: + equipment = self.session.scalar( + select(Equipment).where(Equipment.serial_number == payload["serial_number"]) + ) + if equipment is None: + equipment = Equipment( + kind=payload["kind"], + manufacturer=payload["manufacturer"], + model=payload["model"], + serial_number=payload["serial_number"], + calibrated_on=payload["calibrated_on"], + status=payload["status"], + notes=payload["notes"], + ) + if not dry_run: + self.session.add(equipment) + self.session.flush() + result.created.append(f"equipment:{payload['serial_number']}") + continue + if update_existing: + equipment.kind = payload["kind"] + equipment.manufacturer = payload["manufacturer"] + equipment.model = payload["model"] + equipment.calibrated_on = payload["calibrated_on"] + equipment.status = payload["status"] + equipment.notes = payload["notes"] + result.updated.append(f"equipment:{payload['serial_number']}") + else: + result.unchanged.append(f"equipment:{payload['serial_number']}") + + def _create_validation( + self, + customer: Customer, + location: Location, + device: Device, + result: ImportResult, + dry_run: bool, + ) -> Validation | None: + existing = self.session.scalar( + select(Validation).where( + Validation.customer_id == customer.id, + Validation.device_id == device.id, + Validation.validation_type == "Erstvalidierung", + ) + ) + if existing is not None: + result.unchanged.append("validation") + return existing + validation = Validation( + report_number="REF-IMPORT-1", + customer_id=customer.id, + location_id=location.id, + device_id=device.id, + validation_type="Erstvalidierung", + status=ValidationStatus.draft.value, + examiner_name="nicht erfasst", + ) + if not dry_run: + self.session.add(validation) + self.session.flush() + result.created.append("validation") + return validation diff --git a/validation-suite/backend/mercury/tests/__pycache__/test_validation_workflow.cpython-313-pytest-8.3.4.pyc b/validation-suite/backend/mercury/tests/__pycache__/test_validation_workflow.cpython-313-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba2700468335ef9cb5140d00908c1b296d9685a8 GIT binary patch literal 93493 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 literal 0 HcmV?d00001 diff --git a/validation-suite/backend/mercury/tests/test_validation_workflow.py b/validation-suite/backend/mercury/tests/test_validation_workflow.py index 3b6eedb7..647dd59f 100644 --- a/validation-suite/backend/mercury/tests/test_validation_workflow.py +++ b/validation-suite/backend/mercury/tests/test_validation_workflow.py @@ -4,18 +4,22 @@ import re from datetime import date from pathlib import Path -from sqlalchemy import create_engine +from sqlalchemy import create_engine, select from sqlalchemy.orm import Session from app.db.base import Base from app.models.customer import Customer, CustomerType from app.models.contact import Contact from app.models.device import Device +from app.models.equipment import Equipment from app.models.user import User, UserRole from app.models.location import Location from app.models.validation import Validation, ValidationStatus from app.services.validation_workflow import ValidationWorkflowService from app.services.auth_service import AuthService +from app.services.reference_masterdata import ReferenceMasterdataImportService +from app.api.v1.auth import login as auth_login +from app.schemas.auth import LoginRequest from app.schemas.domain import ValidationCreate from app.modules.orion.service import OrionReportService from app.modules.orion.assets import SCHUBAMED_LOGO_PATH, schubamed_logo_uri @@ -229,6 +233,93 @@ def test_login_updates_last_login_at(): assert user.last_login_at is not None +def test_login_response_contains_user_and_expires_in(): + from app.core.security import hash_password + from app.core.config import settings + + db = session() + user = User( + email="response@schubamed.de", + first_name="Response", + last_name="Test", + role=UserRole.PRUEFER.value, + password_hash=hash_password("VerySecretPass123"), + is_active=True, + must_change_password=True, + ) + db.add(user) + db.flush() + + response = auth_login(LoginRequest(email="response@schubamed.de", password="VerySecretPass123"), db) + + assert response.expires_in == settings.access_token_minutes * 60 + assert response.user.email == "response@schubamed.de" + assert response.user.must_change_password is True + + +def test_login_rejects_inactive_user_with_403(): + from app.core.security import hash_password + from fastapi import HTTPException + + db = session() + user = User( + email="inactive@schubamed.de", + first_name="Inactive", + last_name="User", + role=UserRole.PRUEFER.value, + password_hash=hash_password("VerySecretPass123"), + is_active=False, + must_change_password=False, + ) + db.add(user) + db.flush() + + try: + AuthService(db).login("inactive@schubamed.de", "VerySecretPass123") + except HTTPException as exc: + assert exc.status_code == 403 + else: + raise AssertionError("Inactive user was accepted") + + +def test_reference_masterdata_import_is_idempotent_and_links_entities(tmp_path): + db = session() + reference = next( + candidate + for parent in Path(__file__).resolve().parents + if (candidate := parent / "docs/reference/reports/Erstvalidierung_Dr.Durmaz_Steri_12-25.docx").exists() + ) + + first = ReferenceMasterdataImportService(db).import_reference_docx(reference) + second = ReferenceMasterdataImportService(db).import_reference_docx(reference) + + assert "customer" in first.created + assert "location" in first.created + assert "device" in first.created + assert first.validation_id is None + assert "customer" in second.unchanged + assert "device" in second.unchanged + assert db.scalar(select(Customer).where(Customer.name == "Urologische Praxis Dr. Durmaz")) is not None + assert db.scalar(select(Device).where(Device.serial_number == "EXN250688")) is not None + assert db.scalar(select(Equipment).where(Equipment.serial_number == "15125738")) is not None + + +def test_reference_masterdata_import_can_create_validation(): + db = session() + reference = next( + candidate + for parent in Path(__file__).resolve().parents + if (candidate := parent / "docs/reference/reports/Erstvalidierung_Dr.Durmaz_Steri_12-25.docx").exists() + ) + + result = ReferenceMasterdataImportService(db).import_reference_docx(reference, create_validation=True) + + assert result.validation_id is not None + validation = db.get(Validation, result.validation_id) + assert validation is not None + assert validation.status == ValidationStatus.draft.value + + def test_revalidation_date_uses_calendar_months(): db = session() customer, location, device = seed(db) diff --git a/validation-suite/docker-compose.yml b/validation-suite/docker-compose.yml index 5accf211..7e589dff 100644 --- a/validation-suite/docker-compose.yml +++ b/validation-suite/docker-compose.yml @@ -40,7 +40,10 @@ services: depends_on: - mercury-api environment: - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000/api/v1 + AUTH_COOKIE_NAME: ${AUTH_COOKIE_NAME:-atlas_access_token} + AUTH_COOKIE_SECURE: ${AUTH_COOKIE_SECURE:-false} + AUTH_COOKIE_SAMESITE: ${AUTH_COOKIE_SAMESITE:-lax} + MERCURY_INTERNAL_URL: ${MERCURY_INTERNAL_URL:-http://mercury-api:8000} ports: - "3000:3000" diff --git a/validation-suite/frontend/atlas/app/(auth)/login/page.tsx b/validation-suite/frontend/atlas/app/(auth)/login/page.tsx index 5a85a65f..f6426315 100644 --- a/validation-suite/frontend/atlas/app/(auth)/login/page.tsx +++ b/validation-suite/frontend/atlas/app/(auth)/login/page.tsx @@ -2,32 +2,79 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { LogIn } from "lucide-react"; -import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { AuthProvider, useAuth } from "@/components/auth"; import { BrandLogo } from "@/components/brand/brand-logo"; -import { login } from "@/lib/api"; +import { login, User } from "@/lib/api"; const schema = z.object({ - email: z.string().email(), - password: z.string().min(8) + email: z.string().email("Bitte gib eine gueltige E-Mail-Adresse ein."), + password: z.string().min(1, "Bitte gib dein Passwort ein.") }); type LoginForm = z.infer; +type LoginError = { status: number; message: string }; + +function mapLoginError(error: unknown): LoginError { + const status = typeof error === "object" && error && "status" in error ? Number((error as { status?: number }).status) : 0; + switch (status) { + case 401: + return { status, message: "E-Mail-Adresse oder Passwort ist falsch." }; + case 403: + return { status, message: "Dieses Benutzerkonto ist deaktiviert." }; + case 422: + return { status, message: "Bitte pruefe deine Eingaben." }; + case 500: + case 502: + return { status, message: "Der Anmeldedienst ist momentan nicht erreichbar." }; + default: + return { status, message: "Anmeldung fehlgeschlagen. Bitte erneut versuchen." }; + } +} + function LoginPanel() { - const router = useRouter(); const auth = useAuth(); + const [loading, setLoading] = useState(false); + const [toast, setToast] = useState(""); + const [success, setSuccess] = useState(""); + const form = useForm({ resolver: zodResolver(schema), - defaultValues: { email: "admin@schubamed.de", password: "" } + defaultValues: { email: "admin@schubamed.de", password: "" }, + mode: "onSubmit" }); - async function onSubmit(values: LoginForm) { - const result = await login(values.email, values.password); - auth.setToken(result.access_token); - router.push("/dashboard"); + useEffect(() => { + if (!toast) return; + const timeout = window.setTimeout(() => setToast(""), 5000); + return () => window.clearTimeout(timeout); + }, [toast]); + + async function handleSubmit(values: LoginForm) { + setLoading(true); + setToast(""); + setSuccess(""); + try { + const result = await login(values.email, values.password); + const currentUser = (await auth.refreshUser()) ?? (result.user as User); + auth.setUser(currentUser); + auth.setToken("authenticated"); + const target = currentUser.must_change_password ? "/profile/security" : "/dashboard"; + setSuccess("Anmeldung erfolgreich"); + window.location.replace(target); + } catch (error) { + const mapped = mapLoginError(error); + if (process.env.NODE_ENV !== "production") { + console.error("Login failed", error); + console.error("Login failed mapped", mapped.status, mapped.message); + } + setToast(mapped.message); + } finally { + setLoading(false); + } } return ( @@ -38,7 +85,14 @@ function LoginPanel() {

Anmelden

Sicherer Zugriff auf Atlas Workspace.

-
+ { + event.preventDefault(); + void form.handleSubmit(handleSubmit)(event); + }} + className="space-y-5" + noValidate + > + {toast &&
{toast}
} + {success &&
{success}
}
diff --git a/validation-suite/frontend/atlas/app/api/login/route.ts b/validation-suite/frontend/atlas/app/api/login/route.ts new file mode 100644 index 00000000..2f516cd4 --- /dev/null +++ b/validation-suite/frontend/atlas/app/api/login/route.ts @@ -0,0 +1,32 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { AUTH_COOKIE_NAME, buildAuthCookieOptions } from "@/lib/auth"; + +const mercuryBase = `${process.env.MERCURY_INTERNAL_URL ?? "http://mercury-api:8000"}/api/v1`; + +export async function POST(request: Request) { + const body = await request.json(); + try { + const response = await fetch(`${mercuryBase}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + const data = await response.json(); + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + const cookieStore = await cookies(); + cookieStore.set({ + name: AUTH_COOKIE_NAME, + value: data.access_token, + ...buildAuthCookieOptions(data.expires_in) + }); + return NextResponse.json({ user: data.user, expires_in: data.expires_in }); + } catch (error) { + return NextResponse.json( + { detail: "Der Anmeldedienst ist momentan nicht erreichbar." }, + { status: 502 } + ); + } +} diff --git a/validation-suite/frontend/atlas/app/api/logout/route.ts b/validation-suite/frontend/atlas/app/api/logout/route.ts new file mode 100644 index 00000000..face1a43 --- /dev/null +++ b/validation-suite/frontend/atlas/app/api/logout/route.ts @@ -0,0 +1,13 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { AUTH_COOKIE_NAME, buildAuthCookieOptions } from "@/lib/auth"; + +export async function POST() { + const cookieStore = await cookies(); + cookieStore.set({ + name: AUTH_COOKIE_NAME, + value: "", + ...buildAuthCookieOptions(0) + }); + return NextResponse.json({ status: "ok" }); +} diff --git a/validation-suite/frontend/atlas/app/api/me/route.ts b/validation-suite/frontend/atlas/app/api/me/route.ts new file mode 100644 index 00000000..75c22744 --- /dev/null +++ b/validation-suite/frontend/atlas/app/api/me/route.ts @@ -0,0 +1,18 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { AUTH_COOKIE_NAME } from "@/lib/auth"; + +const mercuryBase = `${process.env.MERCURY_INTERNAL_URL ?? "http://mercury-api:8000"}/api/v1`; + +export async function GET() { + const cookieStore = await cookies(); + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; + if (!token) { + return NextResponse.json({ detail: "Not authenticated" }, { status: 401 }); + } + const response = await fetch(`${mercuryBase}/auth/me`, { + headers: { Authorization: `Bearer ${token}` } + }); + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); +} diff --git a/validation-suite/frontend/atlas/app/api/v1/[...path]/route.ts b/validation-suite/frontend/atlas/app/api/v1/[...path]/route.ts new file mode 100644 index 00000000..42d56897 --- /dev/null +++ b/validation-suite/frontend/atlas/app/api/v1/[...path]/route.ts @@ -0,0 +1,41 @@ +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; +import { AUTH_COOKIE_NAME } from "@/lib/auth"; + +const mercuryBase = `${process.env.MERCURY_INTERNAL_URL ?? "http://mercury-api:8000"}/api/v1`; + +async function forward(request: NextRequest, method: string, path: string[]) { + const cookieStore = await cookies(); + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; + if (!token) { + return NextResponse.json({ detail: "Not authenticated" }, { status: 401 }); + } + const url = new URL(`${mercuryBase}/${path.join("/")}${request.nextUrl.search}`); + const headers = new Headers(request.headers); + headers.set("Authorization", `Bearer ${token}`); + headers.delete("host"); + headers.delete("cookie"); + const init: RequestInit = { method, headers }; + if (method !== "GET" && method !== "HEAD") { + init.body = await request.text(); + } + const response = await fetch(url, init); + const contentType = response.headers.get("content-type") ?? ""; + if (contentType.includes("application/json")) { + return NextResponse.json(await response.json(), { status: response.status }); + } + return new NextResponse(response.body, { status: response.status, headers: { "content-type": contentType } }); +} + +export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return forward(request, "GET", (await params).path); +} +export async function POST(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return forward(request, "POST", (await params).path); +} +export async function PUT(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return forward(request, "PUT", (await params).path); +} +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return forward(request, "DELETE", (await params).path); +} diff --git a/validation-suite/frontend/atlas/components/auth.tsx b/validation-suite/frontend/atlas/components/auth.tsx index 14d6bf92..aa889109 100644 --- a/validation-suite/frontend/atlas/components/auth.tsx +++ b/validation-suite/frontend/atlas/components/auth.tsx @@ -1,15 +1,16 @@ "use client"; import { useRouter } from "next/navigation"; -import { createContext, useContext, useEffect, useMemo, useState } from "react"; -import { apiGet, User } from "@/lib/api"; +import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { User } from "@/lib/api"; type AuthContextValue = { token: string | null; setToken: (value: string | null) => void; + setUser: (value: User | null) => void; logout: () => void; user: User | null; - refreshUser: () => void; + refreshUser: () => Promise; }; const AuthContext = createContext(null); @@ -18,42 +19,67 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const router = useRouter(); const [token, setTokenState] = useState(null); const [user, setUser] = useState(null); + const tokenRef = useRef(null); + const userRef = useRef(null); useEffect(() => { - const stored = window.localStorage.getItem("atlas_token"); - setTokenState(stored); - if (!stored) return; - void apiGet("/auth/me", stored).then(setUser).catch(() => setUser(null)); - }, []); - - useEffect(() => { - if (!token) { - setUser(null); - return; - } - void apiGet("/auth/me", token).then(setUser).catch(() => setUser(null)); + tokenRef.current = token; }, [token]); + useEffect(() => { + userRef.current = user; + }, [user]); + + useEffect(() => { + void fetch("/api/me", { credentials: "include", cache: "no-store" }) + .then(async (response) => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return (await response.json()) as User; + }) + .then((current) => { + setUser(current); + setTokenState("authenticated"); + }) + .catch(() => { + if (!tokenRef.current && !userRef.current) { + setUser(null); + setTokenState(null); + } + }); + }, []); + const value = useMemo(() => { const setToken = (next: string | null) => { setTokenState(next); - if (next) { - window.localStorage.setItem("atlas_token", next); - } else { - window.localStorage.removeItem("atlas_token"); - } }; return { token, setToken, + setUser, logout: () => { setToken(null); + void fetch("/api/logout", { method: "POST", credentials: "include" }); router.push("/login"); }, user, - refreshUser: () => { - if (token) { - void apiGet("/auth/me", token).then(setUser).catch(() => setUser(null)); + refreshUser: async () => { + try { + const response = await fetch("/api/me", { credentials: "include", cache: "no-store" }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const current = (await response.json()) as User; + setUser(current); + setTokenState("authenticated"); + return current; + } catch { + if (!userRef.current) { + setUser(null); + setTokenState(null); + } + return null; } } }; diff --git a/validation-suite/frontend/atlas/lib/api.ts b/validation-suite/frontend/atlas/lib/api.ts index a5b309ee..822c50b0 100644 --- a/validation-suite/frontend/atlas/lib/api.ts +++ b/validation-suite/frontend/atlas/lib/api.ts @@ -1,4 +1,4 @@ -export const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "/api/v1"; +export const API_BASE = "/api/v1"; export type Entity = { id: string; @@ -121,21 +121,25 @@ export type Paginated = { }; export async function login(email: string, password: string) { - const response = await fetch(`${API_BASE}/auth/login`, { + const response = await fetch(`/api/login`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }) + body: JSON.stringify({ email, password }), + credentials: "include" }); if (!response.ok) { - throw new Error("Anmeldung fehlgeschlagen"); + const detail = await response.text(); + const error = new Error(detail || `HTTP ${response.status}`) as Error & { status?: number }; + error.status = response.status; + throw error; } - return response.json() as Promise<{ access_token: string; token_type: string }>; + return response.json() as Promise<{ user: User; expires_in: number }>; } export async function apiGet(path: string, token: string): Promise { const response = await fetch(`${API_BASE}${path}`, { - headers: { Authorization: `Bearer ${token}` }, - cache: "no-store" + cache: "no-store", + credentials: "include" }); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); @@ -146,8 +150,9 @@ export async function apiGet(path: string, token: string): Promise { export async function apiSend(path: string, token: string, method: "POST" | "PUT", body: unknown): Promise { const response = await fetch(`${API_BASE}${path}`, { method, - headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, - body: JSON.stringify(body) + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + credentials: "include" }); if (!response.ok) { const detail = await response.text(); @@ -159,7 +164,7 @@ export async function apiSend(path: string, token: string, method: "POST" | " export async function apiDelete(path: string, token: string): Promise { const response = await fetch(`${API_BASE}${path}`, { method: "DELETE", - headers: { Authorization: `Bearer ${token}` } + credentials: "include" }); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); @@ -169,7 +174,8 @@ export async function apiDelete(path: string, token: string): Promise { export async function apiFetch(path: string, token: string, init?: RequestInit): Promise { const response = await fetch(`${API_BASE}${path}`, { ...init, - headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...(init?.headers ?? {}) } + headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, + credentials: "include" }); if (!response.ok) { throw new Error(await response.text()); diff --git a/validation-suite/frontend/atlas/lib/auth.ts b/validation-suite/frontend/atlas/lib/auth.ts new file mode 100644 index 00000000..2d12762d --- /dev/null +++ b/validation-suite/frontend/atlas/lib/auth.ts @@ -0,0 +1,13 @@ +export const AUTH_COOKIE_NAME = process.env.AUTH_COOKIE_NAME ?? "atlas_access_token"; +export const AUTH_COOKIE_SECURE = process.env.AUTH_COOKIE_SECURE?.trim().toLowerCase() === "true"; +export const AUTH_COOKIE_SAMESITE = (process.env.AUTH_COOKIE_SAMESITE ?? "lax").trim().toLowerCase(); + +export function buildAuthCookieOptions(maxAge: number) { + return { + httpOnly: true as const, + secure: AUTH_COOKIE_SECURE, + sameSite: AUTH_COOKIE_SAMESITE as "lax" | "strict" | "none", + path: "/" as const, + maxAge + }; +} diff --git a/validation-suite/frontend/atlas/middleware.ts b/validation-suite/frontend/atlas/middleware.ts new file mode 100644 index 00000000..7c35686a --- /dev/null +++ b/validation-suite/frontend/atlas/middleware.ts @@ -0,0 +1,60 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { AUTH_COOKIE_NAME } from "@/lib/auth"; + +const protectedPrefixes = [ + "/dashboard", + "/customers", + "/locations", + "/contacts", + "/devices", + "/equipment", + "/validations", + "/users", + "/profile" +]; + +async function loadCurrentUser(request: NextRequest) { + const response = await fetch(new URL("/api/me", request.url), { + headers: { cookie: request.headers.get("cookie") ?? "" }, + cache: "no-store" + }); + if (!response.ok) { + return null; + } + return (await response.json()) as { must_change_password?: boolean }; +} + +export async function middleware(request: NextRequest) { + const token = request.cookies.get(AUTH_COOKIE_NAME)?.value; + const isProtected = protectedPrefixes.some( + (prefix) => request.nextUrl.pathname === prefix || request.nextUrl.pathname.startsWith(`${prefix}/`) + ); + + if (!token) { + if (isProtected) { + return NextResponse.redirect(new URL("/login", request.url)); + } + return NextResponse.next(); + } + + const currentUser = await loadCurrentUser(request); + const requiresPasswordChange = Boolean(currentUser?.must_change_password); + const isLogin = request.nextUrl.pathname === "/login"; + const isPasswordProfile = request.nextUrl.pathname === "/profile/security"; + + if (isLogin) { + const target = requiresPasswordChange ? "/profile/security" : "/dashboard"; + return NextResponse.redirect(new URL(target, request.url)); + } + + if (requiresPasswordChange && isProtected && !isPasswordProfile) { + return NextResponse.redirect(new URL("/profile/security", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/dashboard/:path*", "/customers/:path*", "/locations/:path*", "/contacts/:path*", "/devices/:path*", "/equipment/:path*", "/validations/:path*", "/users/:path*", "/profile/:path*", "/login"] +}; diff --git a/validation-suite/frontend/atlas/package.json b/validation-suite/frontend/atlas/package.json index d64c097e..3df8c45f 100644 --- a/validation-suite/frontend/atlas/package.json +++ b/validation-suite/frontend/atlas/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test:auth": "node --test tests/auth-cookie.test.mjs" }, "dependencies": { "@hookform/resolvers": "^3.10.0", @@ -31,4 +32,3 @@ "typescript": "^5.8.3" } } - diff --git a/validation-suite/frontend/atlas/tests/auth-cookie.test.mjs b/validation-suite/frontend/atlas/tests/auth-cookie.test.mjs new file mode 100644 index 00000000..7d013ab6 --- /dev/null +++ b/validation-suite/frontend/atlas/tests/auth-cookie.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +function parseCookieSecure(value) { + return value?.trim().toLowerCase() === "true"; +} + +test("AUTH_COOKIE_SECURE=false yields secure false", async () => { + assert.equal(parseCookieSecure("false"), false); + const options = { + httpOnly: true, + secure: parseCookieSecure("false"), + sameSite: "lax", + path: "/", + maxAge: 3600 + }; + assert.deepEqual(options, { + httpOnly: true, + secure: false, + sameSite: "lax", + path: "/", + maxAge: 3600 + }); +}); + +test("AUTH_COOKIE_SECURE=true yields secure true", async () => { + assert.equal(parseCookieSecure("true"), true); + const options = { + httpOnly: true, + secure: parseCookieSecure("true"), + sameSite: "lax", + path: "/", + maxAge: 0 + }; + assert.deepEqual(options, { + httpOnly: true, + secure: true, + sameSite: "lax", + path: "/", + maxAge: 0 + }); +});