From 876af481459b01547e7b0a8f7d23e2c6c6dd76ec Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Sun, 7 Dec 2025 01:28:03 +0200 Subject: [PATCH] updates --- .../__pycache__/auth_routes.cpython-312.pyc | Bin 36471 -> 36471 bytes .../__pycache__/user_routes.cpython-312.pyc | Bin 20816 -> 24001 bytes Backend/src/auth/routes/user_routes.py | 104 +++++- .../schemas/__pycache__/user.cpython-312.pyc | Bin 3216 -> 3399 bytes Backend/src/auth/schemas/user.py | 2 + .../guest_request_routes.cpython-312.pyc | Bin 27300 -> 27574 bytes .../routes/guest_request_routes.py | 28 +- .../api_key_routes.cpython-312.pyc | Bin 8078 -> 9341 bytes .../webhook_routes.cpython-312.pyc | Bin 10061 -> 10861 bytes .../src/integrations/routes/api_key_routes.py | 23 ++ .../src/integrations/routes/webhook_routes.py | 34 +- Backend/src/models/__init__.py | 6 + .../__pycache__/__init__.cpython-312.pyc | Bin 8342 -> 8605 bytes .../team_chat_routes.cpython-312.pyc | Bin 44123 -> 45960 bytes .../notifications/routes/team_chat_routes.py | 51 ++- ...accountant_security_routes.cpython-312.pyc | Bin 12665 -> 13278 bytes .../routes/accountant_security_routes.py | 14 +- ...ccountant_security_service.cpython-312.pyc | Bin 10638 -> 11198 bytes .../services/accountant_security_service.py | 14 +- .../favorite_routes.cpython-312.pyc | Bin 10585 -> 10485 bytes Backend/src/reviews/routes/favorite_routes.py | 18 +- .../image/__pycache__/pil.cpython-312.pyc | Bin 0 -> 2541 bytes Frontend/src/App.tsx | 19 +- .../auth/components/StepUpAuthManager.tsx | 57 +++ .../auth/components/StepUpAuthModal.tsx | 335 ++++++++++++++++++ .../auth/contexts/StepUpAuthContext.tsx | 67 ++++ .../rooms/components/FavoriteButton.tsx | 13 +- .../team-chat/components/TeamChatPage.tsx | 72 +++- .../src/pages/admin/UserManagementPage.tsx | 93 ++++- Frontend/src/shared/services/apiClient.ts | 42 ++- Frontend/src/store/useFavoritesStore.ts | 32 +- 31 files changed, 914 insertions(+), 110 deletions(-) create mode 100644 Backend/venv/lib/python3.12/site-packages/qrcode/image/__pycache__/pil.cpython-312.pyc create mode 100644 Frontend/src/features/auth/components/StepUpAuthManager.tsx create mode 100644 Frontend/src/features/auth/components/StepUpAuthModal.tsx create mode 100644 Frontend/src/features/auth/contexts/StepUpAuthContext.tsx diff --git a/Backend/src/auth/routes/__pycache__/auth_routes.cpython-312.pyc b/Backend/src/auth/routes/__pycache__/auth_routes.cpython-312.pyc index 070930ae40a5cb3e6f28a68007c5dcaef90cc0e1..a31ea7b2b7e887c862927b79bf0e87884767d3f9 100644 GIT binary patch delta 22 ccmex9hw1wqChpU`yj%=G5VqB1BX>?809pnI8UO$Q delta 22 ccmex9hw1wqChpU`yj%=GkXC5Ckvpdk09e@u9nEgh0Fz=3&g!U}Ix!unPub8wZ?+EV$QLSV-)Zuwz!3I@{fa z#;xssZ4x)mb2^)3Otxfb8)wql+W(~P+KHt=imY*V-PzgMNB>xodUn#dyWMj>A;4ZY z?eq>Do$tK9^L_W;-#Oonz0O{HpVi;c>C_BdeLt!7f6;wGZwcX9XI_uEcrw63n~OTUnvx?X0PO{*3D53a|m=l zOUWj#qy)1`d6cYrsQHcSJZiwhPqSLLUYb>PI-B(GS7eo*C%vaie$%=+>Q2c#HS_t> zd{)OM)x$!>8wd>#3nFQgjj;vuKa!2f*?dx@71$c`tk%Vrk+-x(j&isqVg-<-IueTb z0{eW!Lf9h_UF6@iJyV-eAr1JC3t}T0HX$)nVbC`!BxOhaVmKmhLvaI=7SzdxLgT?m zQW+9?LG%uu61SsN$_wG4q&#%=Xjq7(rsRBqfVc(ywxWdslJ-R+qF9C_l|QTs9Sw=2 zzKGaL{y|qteyz(XtU~Kf)GNhMK=6hJN9d&D4pbaK;v%K`8QBqFE%QIo{|m=9%zxi> zShj07Nd7~H7Bxs}k!(Uzhol~eM@mmmp@MpbH7zydE-Vw;k%m@pdHpk>I@% z4C;31zKkc%y95-SiXB3&4R#4F^ z{cMjoP!1^MRc1?gDap$96ltQGur89DHsxi!d{&Wu?Vn+Jc2XN<$u+fEdPbLGXk}rN!iTIUNOts6kx6 zmr7pqKosSO2SG@uO?yuoBOAz+C7WE`q0(hUHl(AERAOd2V>BauCq}B#vu4fH#Oru{ zz%lBW%w(BJN!l~2h?=6Rs5+|U4ZJa$IZ{WiDYA^S8JF=vsUs{o-^&lu9-{0ZU@c`I`~y&ey3J#F!MIvp00uq#dA0=DJLxv ze5vULDOvn1pEc_wJM#;OzD_T*!fN)wvRt;cJ1dJIdeJLtC4Zl9%e0=ho@7rlPjdsz zN%n2-B# zaSd&@VKOb`TLoL$C&-P03TsM$=8Ew6(2x)gzoizdh`!Lt9wTLio}9m+Ivyb19|)%~ zaus*qQUZ=;QU}}N!`%OZS&%7qg67grB!S2lvj=gR4gOEE~>MPV+4x8 zYshOO=bEx|Qkbh5f*TbPDC~+agE3-%_&7&Kig!!8STbK+KD8`o9L`si7nMISzo=ib zSH$fVv-}VJZ~3n@#VdC$*xQ%vU2%KYg1viMciWJC$CUeKTdc8n(X>CN-4D*0wrNE| zV_wo^$2Hk8O=-fKyJRhiTT5m-5<2U-r)Hjt=}PW8U9U7=Z2ZB_7h9H`8{^K6KQh1T zc*k+Y|FfRpd%<{h*Mf8JlJh{^d0@fWH{EqRGxv_QV2+Em^v3o-zO?^HeE*SHuP?S` zaM3yxGY%!J`Ok+ggl0Nmep}&^wK#4qp2mUEMBZ^&3Kj99h6O|8lA$GTXjw39pO)S> zWUgesZ_(5n)Art-UoQTFJCKI;l`!}yhXu_?xh(Q|rk)(` zqnhF#2Y1UM1O8SH2ihTgP|}wcn7@Mm-!QuQW{+Cl4fV$YP=j~>p$p2$Eo#o6W z@|njf5zmurn{4FwJCwTh@v_NlRl}?WYMT_4jnvN4^>+V7-2YKa<; z!wV#;qZv_q)EKq!IhUkS1D}fzB53tq8?`4b)eiEn8y{ocWNY=06?vDqNe7=xep_9_ z*hec4TilalRm$4R~BJTS=a*Ei&X!IwJ^|^cv~JiokiIP7;Ts zg7dWV?DKG^;g|eZZH3Gkm5%fPZkIGvMS9adQD?ebt>3bg*F*TdkxbQXb&i221qVL? zgifm3L*k&H=Y^oSn=I8muAzO`F*JIeY^^_E|2N2mRpa-jg9 z#^`5qFAAupb4E|Qj}&2sFV#V5%(n~Ggtw9=%uEd}2n zfCSSBtK{Uvrrm7cytR3w1mQa?XIlRN0=tTrT$OQG zj+mxsv8?IJmg}a)vM%z(*3F&zct!hytz*fyH*VXzVC$LQeLK^cLi8<z{#RmA;*y(uw)Op1V@(YH9(?kAy>lj-%Kinc?*&Jdh zeXhmJYAn_-mfe)*0R8PfJ7acz&ak@O>>Z=^dHn@_EN{ov>Z=8@-1a47N8H$Pf0;w= z{d;QiPG{ZJw_zWjX|)Zs0QmR1`dgTf8(Yo&MrKjV_N%3fx>h|5K5?@BTIna*Ex`XW zi|yA-f9aGWpW9jnE(sM2{s}b;q6GK>)l+Iz^*3_~hrPd^OH|9yvR;OgW)Arl(?GL{ zyKO8UsON6i%Lbac+s)iS3(0CNmb|JY&7QsU=USiT^eQHTZ3?|cwqW;4PVd@bQZc*{ zzUt(U7EEer_p8I!lhu$f+p;B54RP%+*Ku`!_+pEsMI)*oORn6vK{FcIvf=vByw`IG9%_LU(s(uR4EhW4zgPkClieOkQ+ zl@Iw<{(rv#3(eEI4usX|xuG6P(>%)zFsbNg&;OBGUYgXynjXV7IoF_FpN=$X;B8UE z$N(N%)F+s*mA9vdb{I-|2Q6Bq>sI(Uu5fEsD$}Up!Mq}_^iJ?{-Z?8*^O)Tu~&_j_xd4=^AlNPZbp6S9Z{fC4mOar;p22AB`SVF*eS)~5-hdr*Lp;O2#ZfCXiuv@J9q z;OUvkm-c_OuLMp^8g%v#2i;T0g;PoO5LBdy=<~zZENOLZUG>C9DB@HPYWJGf4Ihah zhm*&I;L6Ow;gyUowE2Q~V512MRM_OE#q^jj?1qC@Xvj}%swfP>kt^&jr5}x^g2Ul* z_n7b0C>%9_TU%|pTZjx*dfbs?zKDBF6i$G_>T)aH`vQV5jO+IeMf@iqG8m?&b^Bp4 zaNw()Fz=%2ruHbE=|S9V;pri-KM0%Pz=A$ud4wVPw8K4xy~;#xUr=}&4wcvuq&DEe zh@PrH(hsbbfAL*>E*>iV4JcbX%B?**-3+u0sgI5bJR;(uc#MR1*G%n0_Q6&_yn?d# zfF!egL0{lh#6J|S91TIP;Yzx#_@Pw%1zO>EHu$I+7!MyyN^u{OTmbqt>==^r)E!K< z99Y4VvOwr0to!fK6Wa@qv1naav3g)!-%z0G6U4Zubc!|!IB8P7nnW0;3k@f=shPZV zPm?P8;~fnThmByK{u-D5T{$7HxZqnLehOwy&~k?V2fp9`JM+cYnY$&GZ=8Pp^mzpq z)g@!W$3}8)PlfCd3xD%J+tbN%%W^%*>)blEoUc(GVn5ZkB;Yh@zi5B8biuJ<$+0Q! z*fiI;RMQr(X}c=MYxXQSzP9A(jyt*+9Q&rfmayf0&TzVd^ALy0g`e+fmK8AmOvxCt z5=PVW$_vWxsIaA$$K`&WiXD zdq-}dq(PHNKM~4(|LC9 z9lP@dI5B&s_uMsPzp8q}@S0)4T^%=6&pmN<;OE|J-i2L<;&q2&hmXYg(fHxfJ5#Vi z?FDTtH@&RB*dcGM??}w$TQm+r=ag_4EK8W|kmR0>vze9|F2i(B&1Be8UNv(&=4$7} zE1oM(NI16d;Cbz$u`lJ8aBZ5~Jhvg{Y*}-?zwCf?@59z;aTjFzsf1E@M`MP20XH_G zRG-t#XwK^HDD~&GGuoJ~eoma@ujK#qz}1ZR4_(_>Uwmr26 z7xjHHC2WrZgZ(Z%3r`g^TfKT{w%Rj#ikP35mv@?(>vp!&B)#sCB0rV0L*6A}{;?>p zYYTHjt^unX87#QmFtMP&VODi+<8HX@olV@0IvHx4WGLCj0e@4X>e|fR)D(8rayP3` zd$U%ClFd}RMb(qfeJI;u?{RUrTrwEk%16bmB2%x1yVcNY=#^)1ak&geaSaEPdg{d= zlfJIQ?6>CUx^md5?C>$4DDahl+eiJwmGG(!<469~P|U)2Q5zmQCXD*RmGnh&f=J9p zxzg*!?=@a8MU$jl42{G0sMPs}q5}stLOg=@0koHng`jY}j4U@c9ucwY-ynX1dVD+I zf)5PHTaHR?|5z+NL|OWC8IyCgXv^N!}8 z6;lzwN(KK8DNo8jt72b?d8>6&foze>JYXOd&T_t4u1F3_e)unz-BO9H-)EHqk894R z!U5QsM=VG5FDRp{;HI&WEY9<5rrRXaN&Wo&-%I8I{fXKUWtsSm!k36GsNfqh2 z(qo_M+vfCF%33KT7tgLpFD;liZwThy!}qw9Z4^sOF~gKc$*hM_S~$;RA$Wv&rvGCP zW;0!s&M-axk4)2Zr}thjRn42X>aZrbc&tfXMA(ofN{Yt*pc&Q@AL}&B1&iSeBn|zs z?r?9s|3R5*@m)uw5xKixqCs}mT;iz!8LdQxF&>SFBb&l~axAE#xh%udnNf!db)cB$D4x_usU*|xp2KtnO4X7a>rHB-`}W1;cxnbQ9Es57 z=+}T2x;{A`j>jnt;v3`{-4~^U;W%w(w=8AqGLSNxwV8~M5Furhx)pQ zMh1Ii%D0KHA!y?U>^r47M=|46BwTaKhVKI=&#h@Qa7q14uo%4`6>E^R015giu7f&| zbRgjyxDh#?r3<*EZX^a<2p?w?%5fj*W+asuf^J2^zXN_3T90IQJL!G+%9ESRYXi!* zA!$Y;vsO=F*A8TN0txC#b{+jAK6 z-4DWwvG+ZH6;rqmb18QcoG?q>q2(k;IL(+ z(;x!Ks?bo&*=kf<=BeXL+0C|Gts+WR$;Jw+v(!q)Kpk4?)QU#YDjDoI2A7yIU`H8@ z&Y5u;W?f=GrIs>AGtcr{LJ%GkByt4c`x)_B@kxzFfH`NPk$dAb%*1KH&1L_oGiGE+ z4#}AYfAG6&CXefsK9L#6qsAY}reyXL2`&|OtJv>oo)HVh0#=Z2?xn*;z1JhFr{Q$u?0LoIRRJ1m(I0=;r>Wr zI6~#HbhuDH)E|q-C{{0P4&)W!aZl=p!?D;wz!y4q@cAKcWJ;is+|Pa$2xX-NXs(Nm z^!CcJ*cl^Tz`hQ6Ngwl+2K~caCjsd6M`Ec8o`r@psZxMacbT|wCj1knzWun$q;3!b z0yd_K*`L(*M*3wyJ@o_8SUjnF0Fsw!EGa@=@JdW-VgPP4uo;I4snnMt$7A1v1}Pw5 zg=HZUV;jq=kIv$2s?M=WC{`{+T<>kDvzEo`Q~V z$R(-gyABYTw5E5O`JgQV=B`-ndzU%^k2y=JAL4oZfZ@N0WYdc@Bnu*CSrz+! zIDpitWGGpR9Ytm-_mo<(NqKl7punJ5yWM;))>z|Ve_6VN#FtY@)g^Ia1pTPnk-{XB^3`5sU(rj{#{e3RsGG zhn=W9B9=)(X02c9DTgx7jpJp(AIYcWLK#<#R;eEvcpml~A5D$yz50;Gqo@bAayYOrvowU_c+v1j z@uWRRwdvjGZ~zhRfrAAP)o&Y?*mp~6b7>Tu`AO|(fDB1POO*EXOOiZ9TiNKc`%V0c z>O-R!S?2N+RgWX9An8HUi(~}Iee?jb6i85&6r&L-DGtd8X)T*tzRnNV{!dZ;DiG*T zA`;wRNiFDkQ-qpEwzr|x#LxXh_n{u@;Ap0VFy~(fTyK{zp*tD8F!xpcocVMGHe*q+^#XO*o+RYBE z2n6<`$TTu6%>u=HAUOj6v3H=7ZwX&KB1~tplff0FZS3n6i&R-tCg-FncfyoAZYsML zYJ59#Vb!%zCwq5Qeb_Kr*ERuvp|-2;_DT1q3HPR}?k&gGeHdE$+`5-GJip=XCC4^Q z*|H~Xg%h^I(;ZiA3#ZC!&soNIN#nzZ|0xIu$ST#Rvx9%N>V)n&?Md5|w|LT9IpM8j z&8vq=?bsWuHG~wgx7IvEdfCRc*>$329r?(fKjkT$^psC{%17(2dX}8fPT8}bwVt$& z`&ON+I_DoRXqmLPPS{)TOzY6$&TT6@U%8Z>TdO;|Gamr2vusDcFd?pVZubgT++@2; zeIz{`|Z+?mfdw**wLXxOIEhOzfso)*<` zt1o8<);Zb79Tw8ayB{JLgt?|*pqahwbh23cs%3FJq5P5TqqF_aR-slnZublERcS5s z3$+O&bX-ED2%0_9Kr08Gb!mMXAQI5)0U)hhAJVl+Lc;j4kuBd?;Fuq$B4LCKJ29^! zXx#GBA*9SXyi<(nfb7mjU!G&*9GJ`l2ka{70ao9+F^9v*T#fNsoVoK~?rgrR`2W^f z#zr^iALVevd->r1Dfqd&eGF&BCXWBe`Nqb7E(u=zsU*pPi|nn+4@`u#)u*t)Hb zrrAoTzXhY11xWhG0Uu|2gOqQoH~2l+H~`aQgDQ4@Ym2y4(=cYfuYaG{>G8Pyk- zsgZ9of&8L@fYC)G0m((Pq1`NAbcNb=;w6;^w3l=mkX$m0$lEeIfcFJDbmC>52KdWn z5qyJ-=^HQ`6_KcsXax4e&UW(j*f^B$QFE;K0eLVSTY$ftXbu{9fh4swIs#uab!+TI ziP%3Br}&eW|E@}^hoQ&h{Z&#N9*I)~P^)K}egHdTDA~(@LfedzB zh^Zi-X^{~2Qr80UkHU(vkGtL_OIDDEDT{Mjgz<*0a9RW7v{rDqr*+8b1%v%F18|>< zf+|j?RVZA=uI~1&-KGLhy=huR<_3h&AOpU3-?WaidJJRWtP$gwki|f*X%&|asagG= m^^2B~hUfg#0t`RX@!{<_AIA67&rpgk50Ot*z|xzrlK%o{;Dc2F diff --git a/Backend/src/auth/routes/user_routes.py b/Backend/src/auth/routes/user_routes.py index d48a5922..6140ffba 100644 --- a/Backend/src/auth/routes/user_routes.py +++ b/Backend/src/auth/routes/user_routes.py @@ -12,7 +12,10 @@ from ...bookings.models.booking import Booking, BookingStatus from ...shared.utils.role_helpers import can_manage_users from ...shared.utils.response_helpers import success_response from ...analytics.services.audit_service import audit_service +from ...shared.config.logging_config import get_logger from ..schemas.user import CreateUserRequest, UpdateUserRequest + +logger = get_logger(__name__) router = APIRouter(prefix='/users', tags=['users']) @router.get('/') @@ -71,7 +74,31 @@ async def create_user( password = user_data.password full_name = user_data.full_name phone_number = user_data.phone_number - role_id = user_data.role_id or 3 # Default to customer role + + # Get customer role for default + customer_role = db.query(Role).filter(Role.name == 'customer').first() + if not customer_role: + raise HTTPException(status_code=500, detail='Customer role not found') + + # Handle role - accept either role_id or role name + role_id = None + if user_data.role_id is not None: + role_id = user_data.role_id + elif user_data.role is not None: + # Convert role name to role_id + role_by_name = db.query(Role).filter(Role.name == user_data.role).first() + if not role_by_name: + raise HTTPException(status_code=400, detail=f'Invalid role name: {user_data.role}') + role_id = role_by_name.id + else: + # Default to customer role + role_id = customer_role.id + + # Validate that the role exists + role = db.query(Role).filter(Role.id == role_id).first() + if not role: + raise HTTPException(status_code=400, detail='Invalid role specified') + existing = db.query(User).filter(User.email == email).first() if existing: raise HTTPException(status_code=400, detail='Email already exists') @@ -186,18 +213,32 @@ async def update_user( user.email = user_data.email if user_data.phone_number is not None: user.phone = user_data.phone_number - if user_data.role_id is not None and can_manage_users(current_user, db): + + # Handle role update - accept either role_id or role name + role_id_to_set = None + if user_data.role_id is not None: + role_id_to_set = user_data.role_id + elif user_data.role is not None: + # Convert role name to role_id + role_by_name = db.query(Role).filter(Role.name == user_data.role).first() + if not role_by_name: + raise HTTPException(status_code=400, detail=f'Invalid role name: {user_data.role}') + role_id_to_set = role_by_name.id + + if role_id_to_set is not None and can_manage_users(current_user, db): # SECURITY: Prevent admin from changing their own role if current_user.id == id: raise HTTPException( status_code=400, detail='You cannot change your own role. Please ask another admin to do it.' ) - new_role = db.query(Role).filter(Role.id == user_data.role_id).first() - new_role_name = new_role.name if new_role else None - if user_data.role_id != old_role_id: - changes['role'] = {'old': old_role_name, 'new': new_role_name, 'old_id': old_role_id, 'new_id': user_data.role_id} - user.role_id = user_data.role_id + new_role = db.query(Role).filter(Role.id == role_id_to_set).first() + if not new_role: + raise HTTPException(status_code=400, detail='Invalid role ID specified') + new_role_name = new_role.name + if role_id_to_set != old_role_id: + changes['role'] = {'old': old_role_name, 'new': new_role_name, 'old_id': old_role_id, 'new_id': role_id_to_set} + user.role_id = role_id_to_set if user_data.is_active is not None and can_manage_users(current_user, db): if user_data.is_active != old_is_active: changes['is_active'] = {'old': old_is_active, 'new': user_data.is_active} @@ -260,8 +301,6 @@ async def update_user( status='success' ) except Exception as e: - import logging - logger = logging.getLogger(__name__) logger.warning(f'Failed to log user update audit: {e}') user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'currency': getattr(user, 'currency', 'VND'), 'role_id': user.role_id, 'is_active': user.is_active} @@ -304,10 +343,7 @@ async def delete_user(id: int, request: Request, current_user: User=Depends(auth if active_bookings > 0: raise HTTPException(status_code=400, detail='Cannot delete user with active bookings') - db.delete(user) - db.commit() - - # SECURITY: Log user deletion for audit trail + # Log user deletion BEFORE deletion (so we can reference the user) try: await audit_service.log_action( db=db, @@ -322,13 +358,49 @@ async def delete_user(id: int, request: Request, current_user: User=Depends(auth status='success' ) except Exception as e: - import logging - logger = logging.getLogger(__name__) logger.warning(f'Failed to log user deletion audit: {e}') + # Handle foreign key constraints: Anonymize audit logs before deletion + # This prevents foreign key constraint errors while preserving audit trail + try: + from ...analytics.models.audit_log import AuditLog + audit_logs = db.query(AuditLog).filter(AuditLog.user_id == id).all() + for log in audit_logs: + # Set user_id to None to break foreign key constraint + # This anonymizes the logs while keeping them for security monitoring + log.user_id = None + if audit_logs: + db.flush() # Flush changes before deleting user + logger.info(f'Anonymized {len(audit_logs)} audit logs for user {id} before deletion') + except Exception as e: + logger.warning(f'Could not anonymize audit logs for user {id}: {str(e)}') + # Continue with deletion attempt - if it fails, we'll catch the constraint error + + # Delete the user + try: + db.delete(user) + db.commit() + except Exception as delete_error: + db.rollback() + error_msg = str(delete_error) + # Check for foreign key constraint errors + if 'foreign key' in error_msg.lower() or 'constraint' in error_msg.lower() or '1451' in error_msg: + logger.error(f'Foreign key constraint error when deleting user {id}: {error_msg}') + raise HTTPException( + status_code=400, + detail='Cannot delete user: User has associated records (bookings, payments, audit logs, etc.) that prevent deletion. Please deactivate the user instead.' + ) + else: + logger.error(f'Error deleting user {id}: {error_msg}', exc_info=True) + raise HTTPException( + status_code=500, + detail=f'Error deleting user: {error_msg}' + ) + return success_response(message='User deleted successfully') except HTTPException: raise except Exception as e: db.rollback() - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + logger.error(f'Unexpected error deleting user {id}: {str(e)}', exc_info=True) + raise HTTPException(status_code=500, detail=f'Error deleting user: {str(e)}') \ No newline at end of file diff --git a/Backend/src/auth/schemas/__pycache__/user.cpython-312.pyc b/Backend/src/auth/schemas/__pycache__/user.cpython-312.pyc index 2402639d800e6dbda3ac5dc21dde0ee5e6e141ee..f542718744e943dcef870e51b2e68becf8e1a7dd 100644 GIT binary patch delta 856 zcmZ`$&ubGw6yDixcC$&hn~kPzl4{nP)~-@*n?#Fgg@6|?1%+D4rNquu7m}>Z#vc*U zDuN9NEet|V(u;?Zf=Imh7kE%XX%D)8gWl?;2XWp;8wFjM@4flnytnUtZ$17tp}o_z zkifs$*IE11D=mb?C}Q8#A(lfg<|VA-!7a5V!V5tt>ZF&*aqbZX$7i?|1rou1Ed)`{ z8aP2@oFwwHgohqWtqv{xw#8^&w+^we_ylb-CO$g8H+;z{6SG>X5Ob_lt`S--)$Ciu ztT`s-kwqI@o&;Vy)^h@(XuBY#5gK62f%b5USGSO-*lyXmO(;FW?=Ey|nl#M~9btJn zABJ}6D8MoHP+t2Z4cpi3+JDj_$!;3=?pkauaFlH*qZ$PEf5WzwJTjObyoV03r@^_R z0cin%g8=;i3cw%-ug9*|XacS>T*FJ{lIvDT?Ye{M1ZOq{Itfv~eHA=_AV2O)c1m^n zza&x{(#NUGJBfvr^Q&X)na23Gw7{OLC($JPpdLo=*;n;hfv-ivBJSJL_gB|PWTN=1 z3*rc@4vT!9GKu`+4B+ab74?ky8<<7JBzqGYf8fs(;~@)CjhoIypfV>0fPlfMFLDGt zn9fsecd@hto3so%1#|`=3y=el0CWIfoYUZ)0qE-IOE>LTpN5Dp+bs7y>8j(D=~*DV zYC-!Hz~slRvqtzz=HF_4Hq$E?R_`>W#{C^B-6;Wyb5@Av*pF~F<=6VV{$6mV;^3QQ ea*h_*#olx>hK#1VD{!+rp`ekb`d#40ul5sYIkaK` delta 671 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 delta 3590 zcmai0dr(x@8NcV=y?5F7vhU@&fCwZ!7En>*0|RIjSe|Oc$7bbTU=dioyA&m$iJCSx zCPKbW+ZoMYHEpJv8SVC!cG~E)rpeffua0(`@iCcpI&Ej%r2n+jN!#SbiPcD!PAy1RPQ70EE z7RB$Snevh}E2gZP`Ie?xNjh4pRLf=b?Ee)}$j841=0is$6H>rmgf)1lh z_Lg$olws1Kc7xyGCfSg=%}#P)eZCzOgBSj8Fisxv?##W$=)Cz+@w35c1i{Q}8oWF~#KC9Jp&T!1G0JxUbKU zrgW3Kp=$UvKTDc2Qd-Nhm*YO@40dB|MXJgiH?$H8a`X6DI>yIVz^mpml5%`vwu2=< zLw{CC4H#O7r*Mn1FhSd8QkoDsZvM)K)?-YN&Jvgn6#Ax2vS!lM#GN$v@dI2RKTURB zp=B|LIa!=E581$KbBYpWO~?f8q#2SyhX#n80Y0k>@~mFyDzFR^j4PoNp_hRp>hY;__}BfHtyKiXCZ z)Pn0JPAIdxhzlC*`Of)*@$}`mPith)Xu&^wAmwUK5(v*V%EM2XrA_0D@_ zGi154V3ljIu!A}iQl!5>s>IZ0G=$Lbu(}j;TM$~|m#%tWJ4STjXrw=)j)Y=rCx*HZ zx)FN7<8EA$S^(`02pbVX6cQRWGOQ#l;b?I2lTo)|!Zvu#UGF?ZDSaYbv{%D%f8qZ5 z5|_-{0txp)n3Y{{(zAw`{R(x>4bz#!a5|=*Ue$M4qAR zk%XI>{vIOSA|mCtB%wzae={dwA-mzh(>J76T(=YgU7q;$^6wDxbr`GMnY9_cB?#=? zCy_}A)9|;-ol-B-Mrf;gmh6D(sv~3**!`t0991T;#DsQ4iAF;MirNdE{(7I5+kssa*++gpZq^G97FmF!dKx;bv-!=_o{c)eFGhAe=%g! z2wy??7KMZ{s*K9PSR{z`R>#qQ0O26)uUTn5jC2D6yO}shiL?XhF1XmZw2T$UgZ4sk7V6@{zyLxREKT0SXOVs%!GdrO zfi)sFv065GiYQP!LZUH{l{e|UIUky9Ouy{z_Nwiu2nBhiGG_I@Z9 zQ`Perxq$Ekgj$4!DpnpuJ5Hg@tX@L?SVez=z6pdEDZqoK8j0P~6nL9!#Ou^o7w>4! z709dc(dV}D^WPB~xKQjc%Nq6_Ax?^F&7^>mrBRor2na3NaH7QwNQYoopqW%n29TyrPTRtI>w@ zG--c>9p&2t|7~k#GkWQa4K@b^0~?7E-VeCo7lAs_NC!2s?Hb!xP{kIYv%mq{+v{k~ zOnZek>$LPXk+b1)`*Qll_wRP^n%r}F^Xf;q7`7+-$oaDGoG>4ux@eq|4oQ#cVgYn^ ztW-myvwU{X7*?##(&-PeisB0)rRJB5in z*ze5I*ZX?ZxxRm^v-t2r-|xXh9YP<1jG!QlQAk+Qts7*&IlkE{mT-Eyn^}^M95&1fyosXZ{`nq6! z>~@dB`rZ=hGM;%8jP@3ld_d`hw^>yqsxK6ch6hIJZ#duF0Tx}>PkRd%UDlICO2SFA z5I#a+)n^*T)ar|bO8pR%yAhb^JfXlTrIFOEOPG}|vw9oNS%fK2cPtSTl>R7wYR9bf z1wDOuPc1nL@jZp+uhG;5(W`TX_+(E3`2fVef_X_U?<@S0(=L)^+yOi7V-;ZSv~X_r zCJHdQ9)WdNH8NHKlwp8@^_aL~}b=V#vWGlI2bWC?2Z%Ycyx^K(+cI`|> zQe;+A;>@n;zT>-++LWEa(Rk5JZc>-B^_;~$9X&phG^A`JXD}yCDQ@PBmZT-ct(@JR z%uI0`r?Y-W*UqGi|3l#T%p{)*Y=A$9uYzW;yQ+?CA$P3yq=4kU&6O09(B06HTBJBG OH_0=vO5n4hU;htTt8VB3 diff --git a/Backend/src/hotel_services/routes/guest_request_routes.py b/Backend/src/hotel_services/routes/guest_request_routes.py index 7fe60ed0..91d75cd7 100644 --- a/Backend/src/hotel_services/routes/guest_request_routes.py +++ b/Backend/src/hotel_services/routes/guest_request_routes.py @@ -46,7 +46,7 @@ async def get_guest_requests( priority: Optional[str] = Query(None), page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100), - current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get guest requests with filtering""" @@ -57,17 +57,22 @@ async def get_guest_requests( joinedload(GuestRequest.guest) ) - # Check if user is housekeeping - they can only see requests assigned to them or unassigned + # Check user role to determine access level role = db.query(Role).filter(Role.id == current_user.role_id).first() - is_housekeeping = role and role.name == 'housekeeping' + role_name = role.name if role else 'customer' - if is_housekeeping: + # Customers can only see their own requests + if role_name == 'customer': + query = query.filter(GuestRequest.user_id == current_user.id) + # Housekeeping can only see requests assigned to them or unassigned + elif role_name == 'housekeeping': query = query.filter( or_( GuestRequest.assigned_to == current_user.id, GuestRequest.assigned_to.is_(None) ) ) + # Admin and staff can see all requests (no additional filter needed) if status: query = query.filter(GuestRequest.status == status) @@ -379,7 +384,7 @@ async def update_guest_request( @router.get('/{request_id}') async def get_guest_request( request_id: int, - current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get a single guest request""" @@ -397,10 +402,17 @@ async def get_guest_request( # Check permissions role = db.query(Role).filter(Role.id == current_user.role_id).first() - is_housekeeping = role and role.name == 'housekeeping' + role_name = role.name if role else 'customer' - if is_housekeeping and request.assigned_to and request.assigned_to != current_user.id: - raise HTTPException(status_code=403, detail='You can only view requests assigned to you') + # Customers can only view their own requests + if role_name == 'customer': + if request.user_id != current_user.id: + raise HTTPException(status_code=403, detail='You can only view your own requests') + # Housekeeping can only view requests assigned to them or unassigned + elif role_name == 'housekeeping': + if request.assigned_to and request.assigned_to != current_user.id: + raise HTTPException(status_code=403, detail='You can only view requests assigned to you') + # Admin and staff can view all requests (no additional check needed) return { 'status': 'success', diff --git a/Backend/src/integrations/routes/__pycache__/api_key_routes.cpython-312.pyc b/Backend/src/integrations/routes/__pycache__/api_key_routes.cpython-312.pyc index 09a389d238019e2d9463bf3a0dddd36861c0dc6b..b6c9abec4c7c82041f1062cb14bfbd8740c1d654 100644 GIT binary patch delta 3332 zcmb_eU2Gf25#HtT$UFXt5`QF1mUNK>+l+0=jV%Y1R41xsOQ{vbjoc=7o5(cp#E_y$ zW$zSODrp8z9#SAk5bT59#zFef0!ghR4*U`zLD2%L+@cSq&=e7_ZtM1;C{Pq-m7*!! z-)rQ!w)iss1ClbwY}4+idP-g`BhrwMbT)e znGpW46cHQ+!sh}JJ|jg1nu+pnx+Am&DgR9%bhsccyRN#c4qt&#MOV>nwA%p+u1wUH z>%8E?fcssUj=Ij4z$0wSbq*f40%0dCx_D5IGy|_M+EV5|J|wr71l4`TRdB1)6^AM> zd+hI>RfHuZj;x^8xDIzN{nvXUJa^^%T4x& zUZfXG1nyqCAT3a%*EWLOk3jDAZ9ikL)VUhE*S2bSIbv^c$JWv9JRaYgjh)H@IWL?h z3-r6f0*y5lkpHpvl)&lE_wWQqXFrcJAfwEtE z^e(GS|6b)}R!iub!sc^|dL#isa5kHU+>CTS2VwI{vic#$Gg@_%-nSj5S2q&Iygrr8 ztD5OjH6xME^1piaDX|@9Kz}xy$R?lBW*2+4^GSvmVY=rQXBT0C4|+dW>IV&LLxL}f zg^(NuV#;YfKb>c@34@6^7c|Bg7PTp6^9vd?UDC)mU^zHDQ$BV+sm)=orU=G) z7W1%u7!RwCVRcyhvoVzLJ+cSMB4-Uz;em4}CEC{l@pd^xjyx?bwID<2Rc+*LQZVH&6dX6heUt5&VIQL}W4W_nY1@ z|F>_Tgr|9tj)v)rBd!rIS@EE};_Vr26ju6%TaLShRksf&tD*R)Pq^U|LB7!_fKhTQ zG6W&a3-J5+AKPIuAWRL5g#yjgKoYD8v7j8dArbzGzgq!b8yTN1)p@1?fUTAP%P-SL zlzNIz18c$>s<94~**3HcPoG=&CH>n)R@p_X<5 z(75C%37K8>U2N%|*sB&@#wN}dHlt?t+MdYym>eh--Kx9bw(wW7xTMO+SzPs4yx+h_ zV2wY_yEwVE>b*h@4GzIvoDuQmw9Nk-ySor!R zN~e*GBe78MQIxt!3Frihk0CjUleR_DCt%5>Gwc-FeG!O`FWiPr3pGt& zN;9VHlVxXN9TU;t+=NeGn<#DK=W3%_s&~&mK~@P|LI~M%`?c-~x>LN|9PwzljO44=|J3|zN6_*~qEbaN%zxF| zvuhB5F(@-xu1?MFDEoMP3)`JT32<5uC_m5Er*WV4x@ZhbzP)O6jdJRK|4 zD!OW)PT{^tzH|K>ZGWI67|e9B{Ja7Ey#*H$UQOp*9y&cXbyI*EwMx?z=H?C7gEsg{ z_@5@Lfm@3IxV_!81S%+7qF>^b_LpM^>Ck%MzKQ_+R#UVh0eU5WzMU?l**B{gh_z|p3$;sB{KLiz7H-ue#~waqpF delta 2002 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 delta 1233 zcmZuwUrbw77(eIU-u`cCxuySCT1&A^s2OY%Sz!>mpn^yoF>GqU1avaRy%vU^I;{CTbKOOcoz1Cau*6AAIn|Sj%GO%i{Tpl^0KPf8Y20zQ6Z7 z_x?Wp-K6!N#bQM4REPVPKK6ZT-8H!^%zTqQhf^Fv;|QY-WIzQ>{cST;V|LJY&30^} z4;{V`r$TJwNVvcy&^vsJTRTee6KxAlD zqXy_uyMIj*)Ns>L_%HQ+^Z_SncXZqAec@oFMvgRuB>n$o>=>G{H?hR6YF^jmaUQ+O zUqX4j%ja>I@iqIHtOk>c63okUi_RDa;ULC5PnE@qk~nc7PTto4y4KQOF*|Eks zr%z-+0t%mE6mER=PF*`R&+D^uh@!h#wr8nzUuzG`@Q6rvZ<5e)Syw*B`dc2|2cpn*TrW!IBOn=uoMn1UTip*_w#0hbl44*j<`@D%0o%pR0_oyFz z?H}nKbs6`pdS>i7!LjEOK=&IGVSX?2WFo-t2Lz_?hxtS&Er@-1y7-BBQiCtjpIn2! z#^%kEtSr4E6Iq%~s$Cj#iFF~DX`j0nkJEYgz~TfDB7>@Seo0YfLS}$@762-c$%Zd8 zKcAIVav7NCfDuxe+%>gvo#ZUAF9JLPFbZ%5;3a@5fLZ!w$i?nj-_a<4GzK`1FJ1<7 z4&W*s_w);kOr0*?_Vn_`G_U{#y6?~9bTNM7I*(`QZg7+%Pg6^1Ticj*igoca{>RWc zV+L{nZWRNM=y_A4uL9&UJr#MIMST%@%t(L$_>9&gKijI->7Y{nm z;78#@BPRi1rp6#u{(4Srl&@;pie*?FeLQ-?c#B2Aubs8h7ozvt9XMWTb=G($f6=$p U1tt%9#Mx^c(`wztk*;q151HU8MF0Q* diff --git a/Backend/src/integrations/routes/api_key_routes.py b/Backend/src/integrations/routes/api_key_routes.py index 4905803a..37343a5a 100644 --- a/Backend/src/integrations/routes/api_key_routes.py +++ b/Backend/src/integrations/routes/api_key_routes.py @@ -39,6 +39,9 @@ async def create_api_key( ): """Create a new API key.""" try: + from sqlalchemy.exc import ProgrammingError + import pymysql + expires_at = None if key_data.expires_at: expires_at = datetime.fromisoformat(key_data.expires_at.replace('Z', '+00:00')) @@ -67,6 +70,17 @@ async def create_api_key( }, message='API key created successfully. Save this key securely - it will not be shown again.' ) + except HTTPException: + raise + except (ProgrammingError, pymysql.err.ProgrammingError) as e: + error_str = str(e).lower() + if "doesn't exist" in error_str or "does not exist" in error_str or "table" in error_str and "not found" in error_str: + logger.warning(f'API keys table does not exist: {str(e)}') + raise HTTPException( + status_code=503, + detail='API keys table not found. Please run database migrations to create the table.' + ) + raise except Exception as e: logger.error(f'Error creating API key: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @@ -78,6 +92,9 @@ async def get_api_keys( ): """Get all API keys.""" try: + from sqlalchemy.exc import ProgrammingError + import pymysql + api_keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all() return success_response(data={ @@ -93,6 +110,12 @@ async def get_api_keys( 'created_at': k.created_at.isoformat() if k.created_at else None } for k in api_keys] }) + except (ProgrammingError, pymysql.err.ProgrammingError) as e: + error_str = str(e).lower() + if "doesn't exist" in error_str or "does not exist" in error_str or "table" in error_str and "not found" in error_str: + logger.warning(f'API keys table does not exist: {str(e)}') + return success_response(data={'api_keys': []}, message='API keys table not found. Please run database migrations.') + raise except Exception as e: logger.error(f'Error getting API keys: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) diff --git a/Backend/src/integrations/routes/webhook_routes.py b/Backend/src/integrations/routes/webhook_routes.py index f87830fa..25b754b7 100644 --- a/Backend/src/integrations/routes/webhook_routes.py +++ b/Backend/src/integrations/routes/webhook_routes.py @@ -65,18 +65,30 @@ async def get_webhooks( ): """Get all webhooks.""" try: - webhooks = db.query(Webhook).order_by(Webhook.created_at.desc()).all() + from sqlalchemy.orm import noload + # Explicitly prevent relationship loading which might be causing the SQL error + webhooks = db.query(Webhook).options(noload(Webhook.creator)).order_by(Webhook.created_at.desc()).all() - return success_response(data={ - 'webhooks': [{ - 'id': w.id, - 'name': w.name, - 'url': w.url, - 'events': w.events, - 'status': w.status.value, - 'created_at': w.created_at.isoformat() if w.created_at else None - } for w in webhooks] - }) + result = [] + for w in webhooks: + try: + result.append({ + 'id': w.id, + 'name': w.name, + 'url': w.url, + 'events': w.events if w.events else [], + 'status': w.status.value if w.status else 'inactive', + 'created_at': w.created_at.isoformat() if w.created_at else None, + 'updated_at': w.updated_at.isoformat() if w.updated_at else None, + 'description': w.description, + 'retry_count': w.retry_count, + 'timeout_seconds': w.timeout_seconds + }) + except Exception as e: + logger.error(f'Error serializing webhook {w.id}: {str(e)}', exc_info=True) + continue + + return success_response(data={'webhooks': result}) except Exception as e: logger.error(f'Error getting webhooks: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) diff --git a/Backend/src/models/__init__.py b/Backend/src/models/__init__.py index 06873e08..5a805f6a 100644 --- a/Backend/src/models/__init__.py +++ b/Backend/src/models/__init__.py @@ -105,6 +105,10 @@ from ..notifications.models.email_campaign import Campaign, CampaignStatus, Camp from ..security.models.security_event import SecurityEvent, SecurityEventType, SecurityEventSeverity, IPWhitelist, IPBlacklist, OAuthProvider, OAuthToken from ..security.models.gdpr_compliance import DataSubjectRequest, DataSubjectRequestType, DataSubjectRequestStatus, DataRetentionPolicy, ConsentRecord +# Integration models +from ..integrations.models.api_key import APIKey +from ..integrations.models.webhook import Webhook, WebhookDelivery, WebhookEventType, WebhookStatus, WebhookDeliveryStatus + __all__ = [ # Auth 'Role', 'User', 'RefreshToken', 'PasswordResetToken', @@ -155,4 +159,6 @@ __all__ = [ # Security 'SecurityEvent', 'SecurityEventType', 'SecurityEventSeverity', 'IPWhitelist', 'IPBlacklist', 'OAuthProvider', 'OAuthToken', 'DataSubjectRequest', 'DataSubjectRequestType', 'DataSubjectRequestStatus', 'DataRetentionPolicy', 'ConsentRecord', + # Integrations + 'APIKey', 'Webhook', 'WebhookDelivery', 'WebhookEventType', 'WebhookStatus', 'WebhookDeliveryStatus', ] diff --git a/Backend/src/models/__pycache__/__init__.cpython-312.pyc b/Backend/src/models/__pycache__/__init__.cpython-312.pyc index a15242bc5376fc8c920e6a8871e9f52eef94a5f9..ef8cd9736238300b9a4af96a855abbc9b5c04322 100644 GIT binary patch delta 356 zcmbQ{IM~}MZIEVNM}fKTErOjy3$$GWwQvYJ$F3gEjGshPw&)9P1alN z;i*X(`T5zm_@NA!)SS$+)S}8;0#E_hvedkikjjG8Tf9(_;F83W(&Ag9XgVOmnyWX5 z2v{;J7A*%lx@ZN6SP3Flfr!;0Vhxb+(|ogeg77b{5b4aklGOB~#FEVXykfoF{FKz3 zV!gzI%=qloO0W^Pu&OABxEj0pliTFtxIO`GVFcph)tleR&0!S(%ErKGc%Px_Dnr#L O787o!M)sl)KxqJCYkm3v delta 96 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 Hks|Vc&;3a_ delta 2578 zcmZuy4N%kP8UMfkuY{0AnqyCm*Sou zRJzEg+CM8dN|xWFq!VEr;SbOkm=9a4Q`ov))m2u$q7wI;U3R)Uk4$QYrEt80Ta!($YFTTirIfk3pz&vrQcKBkI8b}g zI)y=#<^=V?hILLBZO6JQ^11f@I-RQfHM6Uw6Y$3uimT6IkVFJtQhosMAZte8CHyN= z=MgR-EM^c@+gg3zke|Mb5+lNU@Z9D+57yX~5I+y@_czpq5q2URM&JeDg+C5&Z_XoM zY15kztClZN*NHHD)E)ID|# z5ys$sUxxa3B*XB9Z=re|NjF<~ev&ch5BTb~`h!7lgP(q@dHi=}b$0P(O(nWp%ybPq zsMe;e=Xu*d?DGnY!rR=66kqu?QV&<2L6#!$Q{_ja(0`*~Hl3SIIf?feZxRI|cuxTL z+WC<4v>UFpcL21m;Jve2*ZW84Dv6y)|O3VP25&W zziBFzR#Z&W~KVY1pF_{b=Mzhv6r?MiI_!7sjRS#HEAo-Q@})Y#_jy&Z6CvmoDYe(y^rC z0|J{rs&A!!c5CYWQMsw5F+kH%y&r)ctU&ny8%C-Jflm@%Q$9bApo~8lDx;#tW^XW5 z7xeoB*pK*9taGXHx8&i(z+n+p+Jb%>jFPq>Iy(7BOHgBO@Q3O`e(%;gZbD6LD_wY< zDjD&maHmsG1s!Zx- zy`jpg749z<$rNnuE>`*VGQg?sbc>m7{*kGxhpXL-m3d5fQoG;1S0ZO%&+8Z1qgm41 z#hicJ>soY)g^$;)7b!0SA7u&5i@&@HeJ?8u7;{kz^vy`|VLWfnFt(NDpkp=yZ-qrj z@z}2+r64?vjjtzPlnoy47=}v&MQS=*<)%1sI5mlU4GTt!7pqJ_Uu4ER?N0@^d7B%3 zPH$W26@MVqSnmxrwgf1F&`2?}9v^WtoF1uCaV*goxJDbv48Uk^{BbZ~B)+fNxpZZ8 zg~ftGJ=zBlK7_QfY@?nDqcS!XJqv5cRw*7PAlgsI+I3`!W z9QE{dgo_NKa%)R4L_bFUQ{<)Awh)~}_A0V!d??e;QSueSw+QzT?jy*U-*Tj|yD8R@ zvPzf)iYG-kqs9Z5Pdf~&7|;HMV4rsD^h(8@N!05G>!E_4yqLu38&-QvVZW>;6D+rN TjGB=I(H;{~R!y``6K?Vy*{REY diff --git a/Backend/src/notifications/routes/team_chat_routes.py b/Backend/src/notifications/routes/team_chat_routes.py index 97ef9763..2655c9cb 100644 --- a/Backend/src/notifications/routes/team_chat_routes.py +++ b/Backend/src/notifications/routes/team_chat_routes.py @@ -184,7 +184,7 @@ def serialize_channel(channel: TeamChannel, current_user_id: int, unread_count: "id": m.id, "full_name": m.full_name, "email": m.email, - "avatar_url": m.avatar_url, + "avatar_url": m.avatar, # User model uses 'avatar' field "role": m.role.name if m.role else None } for m in channel.members @@ -202,7 +202,7 @@ def serialize_message(message: TeamMessage) -> dict: "sender": { "id": message.sender.id, "full_name": message.sender.full_name, - "avatar_url": message.sender.avatar_url, + "avatar_url": message.sender.avatar, # User model uses 'avatar' field "role": message.sender.role.name if message.sender.role else None } if message.sender else None, "content": message.content if not message.is_deleted else "[Message deleted]", @@ -629,7 +629,7 @@ async def send_direct_message( "sender": { "id": current_user.id, "full_name": current_user.full_name, - "avatar_url": current_user.avatar_url + "avatar_url": current_user.avatar # User model uses 'avatar' field }, "message": serialized } @@ -647,20 +647,55 @@ async def get_team_users( db: Session = Depends(get_db) ): """Get all team users (admin, staff, housekeeping) for messaging.""" + # Get role IDs for team roles to ensure proper filtering + team_roles = db.query(Role).filter( + Role.name.in_(['admin', 'staff', 'housekeeping']) + ).all() + team_role_ids = [r.id for r in team_roles] + + if not team_role_ids: + logger.error('No team roles found in database') + return {"success": True, "data": []} + + # Build query with proper role filtering query = db.query(User).options( joinedload(User.role), joinedload(User.presence) - ).join(Role).filter( - Role.name.in_(['admin', 'staff', 'housekeeping']), + ).filter( + User.role_id.in_(team_role_ids), User.is_active == True, User.id != current_user.id ) if role: - query = query.filter(Role.name == role) + role_obj = db.query(Role).filter(Role.name == role).first() + if role_obj: + query = query.filter(User.role_id == role_obj.id) users = query.order_by(User.full_name).all() + # Debug: Check total count of team users (including inactive) for troubleshooting + if not users: + # Check if there are any team users at all (including inactive) + all_team_users = db.query(User).filter( + User.role_id.in_(team_role_ids), + User.id != current_user.id + ).count() + + active_team_users = db.query(User).filter( + User.role_id.in_(team_role_ids), + User.is_active == True, + User.id != current_user.id + ).count() + + logger.warning( + f'No active team users found for user {current_user.id} ({current_user.email}). ' + f'Total team users (including inactive): {all_team_users}, ' + f'Active team users: {active_team_users}. ' + f'Query filters: role={role}, is_active=True, excluded_user_id={current_user.id}, ' + f'team_role_ids={team_role_ids}' + ) + return { "success": True, "data": [ @@ -668,10 +703,10 @@ async def get_team_users( "id": u.id, "full_name": u.full_name, "email": u.email, - "avatar_url": u.avatar_url, + "avatar_url": u.avatar, # User model uses 'avatar' field "role": u.role.name if u.role else None, "status": u.presence.status if u.presence else 'offline', - "last_seen": u.presence.last_seen_at.isoformat() if u.presence else None + "last_seen": u.presence.last_seen_at.isoformat() if u.presence and u.presence.last_seen_at else None } for u in users ] diff --git a/Backend/src/payments/routes/__pycache__/accountant_security_routes.cpython-312.pyc b/Backend/src/payments/routes/__pycache__/accountant_security_routes.cpython-312.pyc index e27141bdc928a6010d38b8c4676c93219d953060..d6e4afe857f86b081d80d7250a978640ee5a1532 100644 GIT binary patch delta 3240 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 x{Z6oMkw@ak+J;3DiA6UTX datetime.utcnow() + ).order_by(AccountantSession.last_activity.desc()).first() + + if active_session: + session_token = active_session.session_token + else: + raise HTTPException(status_code=400, detail='No active session found. Please log in again.') # Verify MFA if token provided if mfa_token: diff --git a/Backend/src/payments/services/__pycache__/accountant_security_service.cpython-312.pyc b/Backend/src/payments/services/__pycache__/accountant_security_service.cpython-312.pyc index 4fc18af628a6f9b5c67a747cb8df1430bfea916d..45554a4c158ddcf8b8b10e1ffeea9f7907899d59 100644 GIT binary patch delta 1085 zcmZ{jO-vI(6vt<_-R<(xvb5AKOSdc^)>c0JMvVwWB}P%euRx+nRa#@Kf|(YyrWJ2$ zOf+N?KQ1aa3`C>#VoWsNOuVouT4IdBcrqSbAO_<_=PjUeaFYGa|Gjzd?R&HB^#d=u z9B=IQe8g7t$@*w};IZRvk;}$R3wvf0974BHiims-nQ{ghESK%uR<2mHbgW?~ci|qNJ?Onz9mm${O=54F)UC_2ML#P!>;O zDlN`)m9j3vmNDxn^u<=1Pw{b_WTc{>>1bu?2C|w(5)rVD z;l0>L!VwZWo+R~PTEmfe4{3(uT7V0%jn+EV4Gl~N1p;?LssLudcBbAtj`bv>1CdZX z5|2mwdI=j6#RUe9o^!ShitJV|p=l42)Je7cUiY2u3IE2dx+$kN>uU1{sUjx@btyQ~ zrAzBgt45bT3QvWy6tn>eDD6yY@ z_Z-5{GHs>(7W|98^4|4@m_LZ;-*uGfIQw}cO(R~mksg{>cX2$XuhmK%reD=L{3Y|* z*UE7PIJ2_C&0#NXsd|Uos9fC<+yyCn06YLb32^`p19|~x0SS7e+J}#19#$X1b$)&y zo-A}377$a#B?b?|5RgZ^Zy8D(Hq#W5su_8Hy4vsA4USn05}XESo^@hV%Y2DhhwW3c zV(@HdRh(|a21-Qvj+s2B7Ba!NHZ$bEf^m{60bLk?&bsMzz^_~c34a*#5`Kz}XZphZ grz1_|GF=ngR@P<3?rCIlKt$!6vcAnaDl-#)1KrjA4gdfE delta 739 zcmXYt%}*0S7{+I&yW1_LOX;>!+tLq#LPg|2{AfZ9!L*SUB}5u)RN5x(fhwVHTO(K! z4<4GBn2Exl|6@} zi|}zcHL>(<=!^2B$0PF6!e>jNguEjSR;8;H;!@;1E3$3NC(QU*7a9UjILiX;20XTF zglKeCYzXjMb_-g9i@cMIcw4ZV9X8|uID2IXyR52IWDOzLIgF47Dfhs~?f@%VA?tcA z#@NEzIONqqaH#Wi0T$KatgNa0!`d`r05Q&S=&IIAm0Z1&D?P6)X{BneP}5{gClFUT zVxs0nZT4J3brZoaOse==h`Q5ClZ%(+Ni0^wQQm>1uVgpfPW{z4)vnW+&*D~lJHmPn2K^cOSzq(lEwlwj z@O|(h_hVEW#`qCOm2rf_S^$kaY=>spUjnD03$y^f@F_jgU6BN1XGFh0;9=AWD?>l& zEl5V^Vo7Y7LEtMj385geh{uR!#8cRb2I&L+D4L;T0dbxlbgPC1#c{9O5K$Ns@rFNG zHr5TRS<0k8Y}f*j8}ZJd(XtT9WiT_FG^Iy$o5%3=xX)*ZeC3C@JcbpOOuCdmHm*92 z=v~%#N1BvAgVe=+sfriNKx8!Pdx0{Dz<=;ho?Koj)}NGaXs@6!I_*yj)N?{iF3Kcy M{d+LJPyA-Xf3fVVod5s; diff --git a/Backend/src/payments/services/accountant_security_service.py b/Backend/src/payments/services/accountant_security_service.py index 1f50764b..aa9dab89 100644 --- a/Backend/src/payments/services/accountant_security_service.py +++ b/Backend/src/payments/services/accountant_security_service.py @@ -126,8 +126,18 @@ class AccountantSecurityService: Check if step-up authentication is required. Returns (requires_step_up: bool, reason: str | None) """ + # If no session token provided, try to find the most recent active session for this user if not session_token: - return True, "Step-up authentication required for this action" + active_session = db.query(AccountantSession).filter( + AccountantSession.user_id == user_id, + AccountantSession.is_active == True, + AccountantSession.expires_at > datetime.utcnow() + ).order_by(AccountantSession.last_activity.desc()).first() + + if active_session: + session_token = active_session.session_token + else: + return True, "Step-up authentication required for this action" session = AccountantSecurityService.validate_session(db, session_token, update_activity=False) if not session: @@ -167,6 +177,8 @@ class AccountantSecurityService: minutes=AccountantSecurityService.STEP_UP_VALIDITY_MINUTES ) + # Use flush to ensure changes are visible in the same transaction + # The route handler will commit db.flush() return True diff --git a/Backend/src/reviews/routes/__pycache__/favorite_routes.cpython-312.pyc b/Backend/src/reviews/routes/__pycache__/favorite_routes.cpython-312.pyc index e9151ae0c25dc10a4abd1b10f9665a5ff846ce83..6443b471b66fe16f8574beced4fa9e636ec2b037 100644 GIT binary patch delta 2192 zcmbVNYitx%6rQ`YGdr`}-M(nIrH|dRee6;eDqAW-L5kawXe?Hg(#BAxd#A9l-Klq` zl-9-iNc>}>=rtiR`1)r9O=|YH#9#pv{w#+0n3$+P#YESle+(v`Go==!{=l2ecOLiL zyJyb%&g{2O|1sdd=J$IASeg0G^u|y8+ePwjzE5l>LDz|g1%dgkO5DXoMe4pF|ZgqnfFYOt5qcuhS;9RN9(mn{Z2IhA}>3 zO;6}CiM8;@{M!1>=$D?K)Y&xiqb`r7GnUR^`=Y-wycJ!F!KltgMyJ^}G~HA;Q?{sO zGOUx|_Q%o*@G=XfyjuMHuJ5J89m*wR?W&@8qQ9hkI}9*d)fuVzck zn9Ne=4cSBJ-Gb1;-w2G5PX0^aecvEB0`Qn|iT@DJ))ZwyDL?c0m!8J&0(BpYh0ehn zfkOr5P`qbBhv0VtrOST)~Q3MI03}BX(qWVu3z$8j-Ic1EGr!CvV z^f9KJ$Jmn)WGgTfnNclug7t$LQ-~(34KX2TG9HL(S zd*gwi&%yz7R!+W)Er&y)T_yLiC9(!vm_813yUGLB2)D5z{#B@zbn(AJ(b-{iIPI_a zAMO8F<7KL6bokahhnHf%*X2;si z9J&!0E-1rXZ^}nQyVR0J)W3j8EHz8hDT6;H(mp4nOC&b<^zGp+sHN4!p z^5vQL;NJ|DU{YoNUgWYLteixJN#b~n<|g9dZ#7rJUk+4%R=vCWICQozZT8(6OWMb_ zgUhL^Q}9^ngO&E)sxBAOnKmpWO9?_;Fz+c8CJ;y+(^Dr_hu|nUb$CpIb@Q%%PWi7b zZ%1pB?c`E@Pg~M0EQ$n-MYo!aii`g0WJFwyNT^4}WP5(DH9~gcx7l`27^Y>5>8#}p z7#jsA!|pKrsX5^mpn&rz{Hb)08+0Fbb(gf z!C#ApTk0V~ED9hNIvr6MJiLS^M%EQwpcT9MHHZvCWTGg5Sb!@*6o$1Wlo;7ibb(gf i%FAL+(K3jP6a^3q&p4tmbYuxlj12$g0*#%5bpHmr=LKp2 delta 2163 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 PW^iOZIPxa~v9r;?OYtvv literal 0 HcmV?d00001 diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 8ea7e8a8..d4b11d73 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -24,6 +24,7 @@ import Loading from './shared/components/Loading'; import Preloader from './shared/components/Preloader'; import ScrollToTop from './shared/components/ScrollToTop'; import AuthModalManager from './features/auth/components/AuthModalManager'; +import StepUpAuthManager from './features/auth/components/StepUpAuthManager'; import ResetPasswordRouteHandler from './features/auth/components/ResetPasswordRouteHandler'; import ErrorBoundaryRoute from './shared/components/ErrorBoundaryRoute'; @@ -41,6 +42,7 @@ import { CustomerRoute, HousekeepingRoute } from './features/auth/components'; +import { StepUpAuthProvider } from './features/auth/contexts/StepUpAuthContext'; const HomePage = lazy(() => import('./features/content/pages/HomePage')); const DashboardPage = lazy(() => import('./pages/customer/DashboardPage')); @@ -250,7 +252,8 @@ function App() { - + + } /> + + + + + + } + /> + - + + diff --git a/Frontend/src/features/auth/components/StepUpAuthManager.tsx b/Frontend/src/features/auth/components/StepUpAuthManager.tsx new file mode 100644 index 00000000..1e8e3ffd --- /dev/null +++ b/Frontend/src/features/auth/components/StepUpAuthManager.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useRef } from 'react'; +import { useStepUpAuth } from '../contexts/StepUpAuthContext'; +import StepUpAuthModal from './StepUpAuthModal'; + +// Store reference to context functions for event listener +let stepUpContextRef: { openStepUp: (action: string, request: () => Promise) => void } | null = null; + +const StepUpAuthManager: React.FC = () => { + const { isOpen, actionDescription, closeStepUp, onStepUpSuccess, openStepUp } = useStepUpAuth(); + const contextRef = useRef({ openStepUp }); + + // Update ref when context changes + useEffect(() => { + contextRef.current = { openStepUp }; + stepUpContextRef = { openStepUp }; + }, [openStepUp]); + + // Listen for step-up required events from API client + useEffect(() => { + const handleStepUpRequired = (event: CustomEvent) => { + console.log('Step-up required event received', event.detail); + const { action, originalRequest } = event.detail; + + // Store the original request config for retry + const retryRequest = async () => { + if (originalRequest && typeof originalRequest === 'function') { + return await originalRequest(); + } + }; + + // Open step-up modal with the pending request + if (stepUpContextRef && stepUpContextRef.openStepUp) { + console.log('Opening step-up modal', { action: action || 'this action' }); + stepUpContextRef.openStepUp(action || 'this action', retryRequest); + } else { + console.warn('StepUpAuthContext ref not available'); + } + }; + + window.addEventListener('auth:step-up-required', handleStepUpRequired as EventListener); + return () => { + window.removeEventListener('auth:step-up-required', handleStepUpRequired as EventListener); + }; + }, []); + + return ( + + ); +}; + +export default StepUpAuthManager; + diff --git a/Frontend/src/features/auth/components/StepUpAuthModal.tsx b/Frontend/src/features/auth/components/StepUpAuthModal.tsx new file mode 100644 index 00000000..f80de1cd --- /dev/null +++ b/Frontend/src/features/auth/components/StepUpAuthModal.tsx @@ -0,0 +1,335 @@ +import React, { useState, useEffect } from 'react'; +import { X, Shield, Lock, KeyRound } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import { toast } from 'react-toastify'; +import accountantSecurityService from '../../security/services/accountantSecurityService'; +import useAuthStore from '../../../store/useAuthStore'; + +const mfaTokenSchema = yup.object({ + mfaToken: yup + .string() + .required('MFA token is required') + .matches(/^\d{6}$/, 'MFA token must be 6 digits'), +}); + +const passwordSchema = yup.object({ + password: yup + .string() + .required('Password is required') + .min(6, 'Password must be at least 6 characters'), +}); + +type MFATokenFormData = yup.InferType; +type PasswordFormData = yup.InferType; + +interface StepUpAuthModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + actionDescription?: string; +} + +const StepUpAuthModal: React.FC = ({ + isOpen, + onClose, + onSuccess, + actionDescription = 'this action', +}) => { + const { userInfo } = useAuthStore(); + const [verificationMethod, setVerificationMethod] = useState<'mfa' | 'password'>('mfa'); + const [isVerifying, setIsVerifying] = useState(false); + const [error, setError] = useState(null); + + const { + register: registerMFA, + handleSubmit: handleSubmitMFA, + formState: { errors: mfaErrors }, + reset: resetMFA, + } = useForm({ + resolver: yupResolver(mfaTokenSchema), + defaultValues: { + mfaToken: '', + }, + }); + + const { + register: registerPassword, + handleSubmit: handleSubmitPassword, + formState: { errors: passwordErrors }, + reset: resetPassword, + } = useForm({ + resolver: yupResolver(passwordSchema), + defaultValues: { + password: '', + }, + }); + + useEffect(() => { + if (isOpen) { + setError(null); + resetMFA(); + resetPassword(); + // Default to MFA if user has it enabled, otherwise password + // You can check userInfo.mfa_enabled if available + setVerificationMethod('mfa'); + } + }, [isOpen, resetMFA, resetPassword]); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen && !isVerifying) { + onClose(); + } + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, isVerifying, onClose]); + + const onSubmitMFA = async (data: MFATokenFormData) => { + try { + setIsVerifying(true); + setError(null); + + const response = await accountantSecurityService.verifyStepUp({ + mfa_token: data.mfaToken, + }); + + if (response.status === 'success' && response.data.step_up_completed) { + toast.success('Identity verified successfully'); + // Small delay to ensure backend commit is complete before retrying + await new Promise(resolve => setTimeout(resolve, 100)); + onSuccess(); + onClose(); + } else { + throw new Error('Step-up verification failed'); + } + } catch (error: any) { + const errorMessage = + error.response?.data?.detail || error.response?.data?.message || 'Failed to verify identity. Please try again.'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsVerifying(false); + } + }; + + const onSubmitPassword = async (data: PasswordFormData) => { + try { + setIsVerifying(true); + setError(null); + + const response = await accountantSecurityService.verifyStepUp({ + password: data.password, + }); + + if (response.status === 'success' && response.data.step_up_completed) { + toast.success('Identity verified successfully'); + // Small delay to ensure backend commit is complete before retrying + await new Promise(resolve => setTimeout(resolve, 100)); + onSuccess(); + onClose(); + } else { + throw new Error('Step-up verification failed'); + } + } catch (error: any) { + const errorMessage = + error.response?.data?.detail || error.response?.data?.message || 'Invalid password. Please try again.'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsVerifying(false); + } + }; + + if (!isOpen) return null; + + return ( +

{ + if (e.target === e.currentTarget && !isVerifying) { + onClose(); + } + }} + > + {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Close button */} + {!isVerifying && ( + + )} + +
+ {/* Header */} +
+
+ +
+

Verify Your Identity

+

+ Step-up authentication is required for {actionDescription} +

+
+ + {/* Error message */} + {error && ( +
{error}
+ )} + + {/* Verification method selector */} +
+ + +
+ + {/* MFA Form */} + {verificationMethod === 'mfa' && ( +
+
+ + + {mfaErrors.mfaToken && ( +

{mfaErrors.mfaToken.message}

+ )} +

Enter the 6-digit code from your authenticator app

+
+ + +
+ )} + + {/* Password Form */} + {verificationMethod === 'password' && ( +
+
+ + + {passwordErrors.password && ( +

{passwordErrors.password.message}

+ )} +

Enter your password to verify your identity

+
+ + +
+ )} + + {/* Info */} +
+

+ Note: Step-up authentication is valid for 15 minutes. You won't need to verify again for + similar actions during this time. +

+
+
+
+
+ ); +}; + +export default StepUpAuthModal; + diff --git a/Frontend/src/features/auth/contexts/StepUpAuthContext.tsx b/Frontend/src/features/auth/contexts/StepUpAuthContext.tsx new file mode 100644 index 00000000..2ef3a008 --- /dev/null +++ b/Frontend/src/features/auth/contexts/StepUpAuthContext.tsx @@ -0,0 +1,67 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; + +interface StepUpAuthContextType { + isOpen: boolean; + actionDescription: string; + pendingRequest: (() => Promise) | null; + openStepUp: (actionDescription: string, pendingRequest: () => Promise) => void; + closeStepUp: () => void; + onStepUpSuccess: () => void; +} + +const StepUpAuthContext = createContext(undefined); + +export const StepUpAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const [actionDescription, setActionDescription] = useState(''); + const [pendingRequest, setPendingRequest] = useState<(() => Promise) | null>(null); + + const openStepUp = useCallback((action: string, request: () => Promise) => { + console.log('openStepUp called', { action, hasRequest: !!request }); + setActionDescription(action); + setPendingRequest(() => request); + setIsOpen(true); + }, []); + + const closeStepUp = useCallback(() => { + setIsOpen(false); + setActionDescription(''); + setPendingRequest(null); + }, []); + + const onStepUpSuccess = useCallback(async () => { + if (pendingRequest) { + try { + await pendingRequest(); + } catch (error) { + // Error will be handled by the original request handler + console.error('Error retrying request after step-up:', error); + } + } + closeStepUp(); + }, [pendingRequest, closeStepUp]); + + return ( + + {children} + + ); +}; + +export const useStepUpAuth = () => { + const context = useContext(StepUpAuthContext); + if (context === undefined) { + throw new Error('useStepUpAuth must be used within a StepUpAuthProvider'); + } + return context; +}; + diff --git a/Frontend/src/features/rooms/components/FavoriteButton.tsx b/Frontend/src/features/rooms/components/FavoriteButton.tsx index a96d5fe6..dc88582c 100644 --- a/Frontend/src/features/rooms/components/FavoriteButton.tsx +++ b/Frontend/src/features/rooms/components/FavoriteButton.tsx @@ -2,6 +2,8 @@ import React, { useState } from 'react'; import { Heart } from 'lucide-react'; import useFavoritesStore from '../../../store/useFavoritesStore'; import useAuthStore from '../../../store/useAuthStore'; +import { useAuthModal } from '../../../features/auth/contexts/AuthModalContext'; +import { toast } from 'react-toastify'; interface FavoriteButtonProps { roomId: number; @@ -16,7 +18,8 @@ const FavoriteButton: React.FC = ({ showTooltip = true, className = '', }) => { - const { userInfo } = useAuthStore(); + const { userInfo, isAuthenticated } = useAuthStore(); + const { openModal } = useAuthModal(); const { isFavorited, addToFavorites, @@ -26,6 +29,7 @@ const FavoriteButton: React.FC = ({ const [showTooltipText, setShowTooltipText] = useState(false); + // Hide button for admin, staff, accountant if (userInfo?.role === 'admin' || userInfo?.role === 'staff' || userInfo?.role === 'accountant') { return null; } @@ -53,6 +57,13 @@ const FavoriteButton: React.FC = ({ if (isProcessing) return; + // Require authentication for adding/removing favorites + if (!isAuthenticated || userInfo?.role !== 'customer') { + toast.info('Please login as a customer to add favorites'); + openModal('login'); + return; + } + setIsProcessing(true); try { if (favorited) { diff --git a/Frontend/src/features/team-chat/components/TeamChatPage.tsx b/Frontend/src/features/team-chat/components/TeamChatPage.tsx index e3c1295b..87cdfcb2 100644 --- a/Frontend/src/features/team-chat/components/TeamChatPage.tsx +++ b/Frontend/src/features/team-chat/components/TeamChatPage.tsx @@ -51,6 +51,12 @@ const TeamChatPage: React.FC = ({ role }) => { const [editContent, setEditContent] = useState(''); const messagesEndRef = useRef(null); const [ws, setWs] = useState(null); + const selectedChannelRef = useRef(null); + + // Keep ref in sync with state + useEffect(() => { + selectedChannelRef.current = selectedChannel; + }, [selectedChannel]); // Fetch channels const fetchChannels = useCallback(async () => { @@ -96,28 +102,43 @@ const TeamChatPage: React.FC = ({ role }) => { // Initialize WebSocket useEffect(() => { + if (!userInfo?.id) return; + const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/api/v1/team-chat/ws`; const socket = new WebSocket(wsUrl); socket.onopen = () => { - // Send authentication - socket.send(JSON.stringify({ type: 'auth', user_id: userInfo?.id })); + // Send authentication only when socket is open + if (socket.readyState === WebSocket.OPEN) { + try { + socket.send(JSON.stringify({ type: 'auth', user_id: userInfo.id })); + } catch (error) { + console.error('Error sending WebSocket auth:', error); + } + } }; socket.onmessage = (event) => { - const data = JSON.parse(event.data); - - if (data.type === 'new_message' && data.data.channel_id === selectedChannel?.id) { - setMessages(prev => [...prev, data.data]); - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - } else if (data.type === 'new_message_notification') { - // Show notification for messages in other channels - toast.info(`New message in ${data.data.channel_name || 'Team Chat'}`); - fetchChannels(); // Refresh unread counts - } else if (data.type === 'message_edited') { - setMessages(prev => prev.map(m => m.id === data.data.id ? data.data : m)); - } else if (data.type === 'message_deleted') { - setMessages(prev => prev.filter(m => m.id !== data.data.id)); + try { + const data = JSON.parse(event.data); + + // Use ref to get current selectedChannel without causing re-renders + const currentChannel = selectedChannelRef.current; + + if (data.type === 'new_message' && data.data.channel_id === currentChannel?.id) { + setMessages(prev => [...prev, data.data]); + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } else if (data.type === 'new_message_notification') { + // Show notification for messages in other channels + toast.info(`New message in ${data.data.channel_name || 'Team Chat'}`); + fetchChannels(); // Refresh unread counts + } else if (data.type === 'message_edited') { + setMessages(prev => prev.map(m => m.id === data.data.id ? data.data : m)); + } else if (data.type === 'message_deleted') { + setMessages(prev => prev.filter(m => m.id !== data.data.id)); + } + } catch (error) { + console.error('Error parsing WebSocket message:', error); } }; @@ -125,12 +146,19 @@ const TeamChatPage: React.FC = ({ role }) => { console.error('WebSocket error:', error); }; + socket.onclose = () => { + console.log('WebSocket closed'); + }; + setWs(socket); return () => { - socket.close(); + if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) { + socket.close(); + } + setWs(null); }; - }, [userInfo?.id, selectedChannel?.id, fetchChannels]); + }, [userInfo?.id]); // Removed selectedChannel?.id and fetchChannels from dependencies // Initial data load useEffect(() => { @@ -146,8 +174,14 @@ const TeamChatPage: React.FC = ({ role }) => { useEffect(() => { if (selectedChannel) { fetchMessages(selectedChannel.id); - // Join channel in WebSocket - ws?.send(JSON.stringify({ type: 'join_channel', channel_id: selectedChannel.id })); + // Join channel in WebSocket - only if socket is open + if (ws && ws.readyState === WebSocket.OPEN) { + try { + ws.send(JSON.stringify({ type: 'join_channel', channel_id: selectedChannel.id })); + } catch (error) { + console.error('Error joining channel via WebSocket:', error); + } + } } }, [selectedChannel, fetchMessages, ws]); diff --git a/Frontend/src/pages/admin/UserManagementPage.tsx b/Frontend/src/pages/admin/UserManagementPage.tsx index 35d8eb11..799a31dc 100644 --- a/Frontend/src/pages/admin/UserManagementPage.tsx +++ b/Frontend/src/pages/admin/UserManagementPage.tsx @@ -9,15 +9,18 @@ import Pagination from '../../shared/components/Pagination'; import useAuthStore from '../../store/useAuthStore'; import { logger } from '../../shared/utils/logger'; import { useApiCall } from '../../shared/hooks/useApiCall'; +import { useStepUpAuth } from '../../features/auth/contexts/StepUpAuthContext'; const UserManagementPage: React.FC = () => { const { userInfo } = useAuthStore(); + const { openStepUp } = useStepUpAuth(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showModal, setShowModal] = useState(false); const [editingUser, setEditingUser] = useState(null); const [deletingUserId, setDeletingUserId] = useState(null); + const pendingSubmitDataRef = useRef<{ data: any; isEdit: boolean } | null>(null); const { execute: executeSubmit, isLoading: isSubmitting } = useApiCall( async (data: any, isEdit: boolean) => { @@ -113,28 +116,84 @@ const UserManagementPage: React.FC = () => { return; } - try { - const submitData: any = { - full_name: formData.full_name, - email: formData.email, - phone_number: formData.phone_number, - role: formData.role, - status: formData.status, - }; - - if (editingUser) { - if (formData.password && formData.password.trim() !== '') { - submitData.password = formData.password; - } - logger.debug('Updating user', { userId: editingUser.id, updateData: submitData }); - } else { + const submitData: any = { + full_name: formData.full_name, + email: formData.email, + phone_number: formData.phone_number, + role: formData.role, + status: formData.status, + }; + + if (editingUser) { + if (formData.password && formData.password.trim() !== '') { submitData.password = formData.password; - logger.debug('Creating user', { formData: submitData }); } - + logger.debug('Updating user', { userId: editingUser.id, updateData: submitData }); + } else { + submitData.password = formData.password; + logger.debug('Creating user', { formData: submitData }); + } + + // Store data for retry after step-up + pendingSubmitDataRef.current = { data: submitData, isEdit: !!editingUser }; + + try { await executeSubmit(submitData, !!editingUser); } catch (error: any) { logger.error('Error submitting user', error); + + // Check if step-up authentication is required + // Check both the original response structure and the modified error from API client + const errorData = error.response?.data; + const errorDetail = errorData?.detail; + + // Check for step-up required in multiple ways + const isStepUpRequired = + error.requiresStepUp === true || + error.stepUpAction !== undefined || + (error.response?.status === 403 && + (errorDetail?.error === 'step_up_required' || + errorData?.error === 'step_up_required' || + (typeof errorDetail === 'object' && errorDetail?.error === 'step_up_required') || + (typeof errorDetail === 'string' && errorDetail.includes('Step-up authentication required')))); + + if (isStepUpRequired) { + const actionDescription = + error.stepUpAction || + (typeof errorDetail === 'object' ? errorDetail?.action : null) || + errorDetail?.action || + (typeof errorDetail === 'string' ? errorDetail : null) || + errorDetail?.message || + (editingUser ? 'user update' : 'user creation'); + + logger.debug('Step-up required, opening modal', { + actionDescription, + error: { + requiresStepUp: error.requiresStepUp, + stepUpAction: error.stepUpAction, + status: error.response?.status, + detail: errorDetail + } + }); + + // Open step-up modal and retry after verification + try { + openStepUp(actionDescription, async () => { + if (pendingSubmitDataRef.current) { + logger.debug('Retrying request after step-up', { data: pendingSubmitDataRef.current }); + await executeSubmit( + pendingSubmitDataRef.current.data, + pendingSubmitDataRef.current.isEdit + ); + } + }); + } catch (err) { + logger.error('Error opening step-up modal', err); + // Fallback: show error message + toast.error('Step-up authentication required. Please verify your identity.'); + } + return; // Don't show error toast, step-up modal will handle it + } } }; diff --git a/Frontend/src/shared/services/apiClient.ts b/Frontend/src/shared/services/apiClient.ts index 7371bca2..50711ecc 100644 --- a/Frontend/src/shared/services/apiClient.ts +++ b/Frontend/src/shared/services/apiClient.ts @@ -258,6 +258,12 @@ apiClient.interceptors.response.use( let errorMessage = 'You do not have permission to access this resource.'; let shouldRetry = false; + // Check for step-up authentication requirement + const isStepUpRequired = + errorData?.error === 'step_up_required' || + (typeof errorData?.detail === 'object' && errorData?.detail?.error === 'step_up_required') || + (typeof errorData?.detail === 'string' && errorData?.detail?.includes('Step-up authentication required')); + // Check for MFA requirement error const isMfaRequired = errorData?.error === 'mfa_required' || @@ -266,7 +272,41 @@ apiClient.interceptors.response.use( (typeof errorData.detail === 'string' && errorData.detail.includes('Multi-factor authentication is required')) || (typeof errorData.detail === 'object' && errorData.detail?.error === 'mfa_required')); - if (isMfaRequired) { + if (isStepUpRequired) { + // Step-up authentication required - dispatch event for UI to handle + const actionDescription = (typeof errorData?.detail === 'object' && errorData?.detail?.action) || + (typeof errorData?.detail === 'string' ? errorData?.detail : 'this action'); + + errorMessage = typeof errorData?.detail === 'object' && errorData?.detail?.message + ? errorData.detail.message + : `Step-up authentication required for ${actionDescription}. Please verify your identity.`; + + // Create retry function for the original request + const retryRequest = async () => { + if (originalRequest && !originalRequest._retry) { + // Mark as retry to prevent infinite loops + originalRequest._retry = true; + // Retry the original request + return apiClient.request(originalRequest); + } + }; + + // Dispatch custom event for step-up authentication + window.dispatchEvent(new CustomEvent('auth:step-up-required', { + detail: { + action: actionDescription, + message: errorMessage, + originalRequest: retryRequest, + } + })); + + return Promise.reject({ + ...error, + message: errorMessage, + requiresStepUp: true, + stepUpAction: actionDescription, + }); + } else if (isMfaRequired) { // Get user info to determine redirect path try { const userInfoStr = localStorage.getItem('userInfo'); diff --git a/Frontend/src/store/useFavoritesStore.ts b/Frontend/src/store/useFavoritesStore.ts index 9cdb9b57..b774a7fe 100644 --- a/Frontend/src/store/useFavoritesStore.ts +++ b/Frontend/src/store/useFavoritesStore.ts @@ -122,12 +122,10 @@ const useFavoritesStore = create( addToFavorites: async (roomId: number) => { - // Don't add favorites if user is not authenticated or not a customer + // Require authentication - only logged-in customers can add favorites if (!isAuthenticatedCustomer()) { - // Save as guest favorite instead - get().saveGuestFavorite(roomId); - toast.success('Added to favorites'); - return; + toast.error('Please login as a customer to add favorites'); + throw new Error('Authentication required'); } try { @@ -157,27 +155,25 @@ const useFavoritesStore = create( } catch (error: any) { console.error('Error adding favorite:', error); - - if (error.response?.status === 401) { - get().saveGuestFavorite(roomId); - toast.success('Added to favorites'); + // Don't fallback to guest favorites - require authentication + if (error.response?.status === 401 || error.response?.status === 403) { + toast.error('Please login as a customer to add favorites'); } else { const message = error.response?.data?.message || 'Unable to add to favorites'; toast.error(message); } + throw error; // Re-throw to let caller handle } }, removeFromFavorites: async (roomId: number) => { - // Don't remove favorites if user is not authenticated or not a customer + // Require authentication - only logged-in customers can remove favorites if (!isAuthenticatedCustomer()) { - // Remove from guest favorites instead - get().removeGuestFavorite(roomId); - toast.success('Removed from favorites'); - return; + toast.error('Please login as a customer to manage favorites'); + throw new Error('Authentication required'); } try { @@ -209,16 +205,16 @@ const useFavoritesStore = create( } catch (error: any) { console.error('Error removing favorite:', error); - - if (error.response?.status === 401) { - get().removeGuestFavorite(roomId); - toast.success('Removed from favorites'); + // Don't fallback to guest favorites - require authentication + if (error.response?.status === 401 || error.response?.status === 403) { + toast.error('Please login as a customer to manage favorites'); } else { const message = error.response?.data?.message || 'Unable to remove from favorites'; toast.error(message); } + throw error; // Re-throw to let caller handle } },