From 834d7f59daaacaa6d73b0df95c7d81d23cc8c17f Mon Sep 17 00:00:00 2001 From: Zikil Date: Sat, 1 Mar 2025 00:29:58 +0700 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0,=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B4=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 6148 bytes backend/.DS_Store | Bin 6148 -> 6148 bytes backend/app/.DS_Store | Bin 6148 -> 6148 bytes backend/app/__pycache__/core.cpython-310.pyc | Bin 3432 -> 3426 bytes backend/app/core.py | 8 +- .../__pycache__/review_repo.cpython-310.pyc | Bin 5591 -> 5585 bytes .../__pycache__/user_repo.cpython-310.pyc | Bin 5669 -> 6370 bytes backend/app/repositories/review_repo.py | 2 +- backend/app/repositories/user_repo.py | 86 ++- .../__pycache__/auth_router.cpython-310.pyc | Bin 1158 -> 2230 bytes .../catalog_router.cpython-310.pyc | Bin 4895 -> 5091 bytes backend/app/routers/auth_router.py | 37 +- backend/app/routers/catalog_router.py | 4 +- .../catalog_schemas.cpython-310.pyc | Bin 5033 -> 5292 bytes .../review_schemas.cpython-310.pyc | Bin 1769 -> 1766 bytes .../__pycache__/user_schemas.cpython-310.pyc | Bin 3795 -> 4304 bytes backend/app/schemas/catalog_schemas.py | 6 + backend/app/schemas/review_schemas.py | 2 +- backend/app/schemas/user_schemas.py | 23 +- backend/app/services/__init__.py | 3 +- .../__pycache__/__init__.cpython-310.pyc | Bin 1449 -> 1533 bytes .../catalog_service.cpython-310.pyc | Bin 5333 -> 6279 bytes .../__pycache__/order_service.cpython-310.pyc | Bin 3569 -> 3976 bytes .../review_service.cpython-310.pyc | Bin 1627 -> 2051 bytes .../__pycache__/user_service.cpython-310.pyc | Bin 3028 -> 5858 bytes backend/app/services/catalog_service.py | 91 ++- backend/app/services/order_service.py | 34 +- backend/app/services/review_service.py | 25 +- backend/app/services/user_service.py | 118 ++- frontend/components/Header.tsx | 75 +- frontend/components/admin/AdminLayout.tsx | 277 +++++++ frontend/components/admin/AdminSidebar.tsx | 106 +++ frontend/package-lock.json | 258 +++++++ frontend/package.json | 1 + frontend/pages/account/addresses.tsx | 443 ++++++++++++ frontend/pages/account/change-password.tsx | 218 ++++++ frontend/pages/account/edit.tsx | 249 +++++++ frontend/pages/account/index.tsx | 145 ++++ frontend/pages/account/orders.tsx | 215 ++++++ frontend/pages/account/orders/[id].tsx | 257 +++++++ frontend/pages/admin/categories/[id].tsx | 309 ++++++++ frontend/pages/admin/categories/create.tsx | 236 ++++++ frontend/pages/admin/categories/index.tsx | 284 ++++++++ frontend/pages/admin/customers/[id].tsx | 330 +++++++++ frontend/pages/admin/customers/index.tsx | 236 ++++++ frontend/pages/admin/index.tsx | 320 +++++++++ frontend/pages/admin/orders/[id].tsx | 389 ++++++++++ frontend/pages/admin/orders/index.tsx | 361 ++++++++++ frontend/pages/admin/products/[id].tsx | 679 ++++++++++++++++++ frontend/pages/admin/products/create.tsx | 503 +++++++++++++ frontend/pages/admin/products/index.tsx | 328 +++++++++ frontend/pages/forgot-password.tsx | 130 ++++ frontend/pages/login.tsx | 200 ++++++ frontend/pages/register.tsx | 260 +++++++ frontend/pages/reset-password/[token].tsx | 210 ++++++ frontend/services/analytics.ts | 100 +++ frontend/services/api.ts | 45 ++ frontend/services/auth.ts | 94 +++ frontend/services/catalog.ts | 178 +++++ frontend/services/orders.ts | 78 ++ frontend/services/users.ts | 99 +++ 61 files changed, 7960 insertions(+), 92 deletions(-) create mode 100644 frontend/components/admin/AdminLayout.tsx create mode 100644 frontend/components/admin/AdminSidebar.tsx create mode 100644 frontend/pages/account/addresses.tsx create mode 100644 frontend/pages/account/change-password.tsx create mode 100644 frontend/pages/account/edit.tsx create mode 100644 frontend/pages/account/index.tsx create mode 100644 frontend/pages/account/orders.tsx create mode 100644 frontend/pages/account/orders/[id].tsx create mode 100644 frontend/pages/admin/categories/[id].tsx create mode 100644 frontend/pages/admin/categories/create.tsx create mode 100644 frontend/pages/admin/categories/index.tsx create mode 100644 frontend/pages/admin/customers/[id].tsx create mode 100644 frontend/pages/admin/customers/index.tsx create mode 100644 frontend/pages/admin/index.tsx create mode 100644 frontend/pages/admin/orders/[id].tsx create mode 100644 frontend/pages/admin/orders/index.tsx create mode 100644 frontend/pages/admin/products/[id].tsx create mode 100644 frontend/pages/admin/products/create.tsx create mode 100644 frontend/pages/admin/products/index.tsx create mode 100644 frontend/pages/forgot-password.tsx create mode 100644 frontend/pages/login.tsx create mode 100644 frontend/pages/register.tsx create mode 100644 frontend/pages/reset-password/[token].tsx create mode 100644 frontend/services/analytics.ts create mode 100644 frontend/services/api.ts create mode 100644 frontend/services/auth.ts create mode 100644 frontend/services/catalog.ts create mode 100644 frontend/services/orders.ts create mode 100644 frontend/services/users.ts diff --git a/.DS_Store b/.DS_Store index d2e2933fc704634269da1b65f7dac2566c76c5e0..ac86e422dbbf4bc69f5c2298f8679ae00867e3df 100644 GIT binary patch delta 65 zcmZoMXffEJ!^E^EWwIVq4r9RN*-UmU;=DVjO}@*dz-D*Y;V$dW$;`}3Fs3%M60=mX Q!sbBcZ7iGFIR5bi0Jdxt@c;k- delta 65 zcmZoMXffEJ!^9LTHCc}-hmm3OY$iJvp`z+_lkYMquz88(MZVuLnVDG$#?)q3VisLh PusM)<8_Q-kj(_|BjaL+; diff --git a/backend/.DS_Store b/backend/.DS_Store index 6a06397d0ab56730ba059dfb795d66db896d040d..a950caf9f3d8ecd5e768deb405214add7bef1667 100644 GIT binary patch delta 22 dcmZoMXffEJ!o)PWak3gyFcZ7N=4z%+Q27_S$G+lRxg=cz`BNM&augIY<*0XPbaTq i%VB!JFxi1!WU>J}AJfrHAhVE((O_a>$Yyqqzx)8d&=|!4 delta 78 zcmZoMXffDe%EBl(M#lQp}qZS$G+l7UxecU|qvB@%v;swmzoPz{%^_ ia+oeKOm<)wnQXw$$JDzB$Sh=H_%N|BWHUR*Uw#0E9vBt? diff --git a/backend/app/__pycache__/core.cpython-310.pyc b/backend/app/__pycache__/core.cpython-310.pyc index 03864797b32bb341b7f7475bd6ad9177350ad262..97aa23b7c85c7e5acacb42ddb5c324121f6f03fc 100644 GIT binary patch delta 76 zcmaDM^+<|0pO=@50SHdNI+%WOBky$%A;I+2lK9f%)S~#L%J|gW#LS$@KRL44Si#KA cIh>an8KWjUaA!06Z(hS4&&cRMnTIz902b>SF8}}l delta 82 zcmaDP^+Jj_pO=@50SH9b9!!6*k@q@>s7QKhNqlK>YEgVrC78}j%uSvAjU$Vb10)LK dZBFC7%*c3avK4nWW5DKR-0_Tz0h8HyV*sOp9Bu#r diff --git a/backend/app/core.py b/backend/app/core.py index 72c903b..3ec3a2f 100644 --- a/backend/app/core.py +++ b/backend/app/core.py @@ -72,14 +72,14 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De try: payload = verify_token(token) - username: str = payload.get("sub") - if username is None: + email: str = payload.get("sub") + if email is None: raise credentials_exception except JWTError: raise credentials_exception - from app.repositories.user_repo import get_user_by_username - user = get_user_by_username(db, username) + from app.repositories.user_repo import get_user_by_email + user = get_user_by_email(db, email) if user is None: raise credentials_exception diff --git a/backend/app/repositories/__pycache__/review_repo.cpython-310.pyc b/backend/app/repositories/__pycache__/review_repo.cpython-310.pyc index de70c3330c11658e2ab2b8150d72e303e777c2ad..3913f552ec3d36153ebe5dcf4465b0caec7021f6 100644 GIT binary patch delta 114 zcmcbveNme?pO=@50SKy}9!zK5$h$#+?-o~SacWU~YHng?j;8SB1-zn@9}Bp#v4RCA z8wnOL`fpw=c#M%P45+Ord~&|9IAiGKcHsub*vT9sE{wM(`-;qGjGFvbB$7J{q{JUY K#7+(rZ36%q6C%X` delta 147 zcmcbpeO;S3pO=@50SMl%I+)JAk#~cDU=(j@acWUKh{#LKP1O{hynt79@&f@kP7avZ zWF5f*M!(H-1&=Yxg#tAfg#n2uo|62M#GLq|#FEUs^dc>gc*x{N;ReQ-$xI?Hj8T)_ eMdmZ!n*3ZOQXmo}=?5ZWK!i9@negOf(L4ZeXDm1X diff --git a/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc b/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc index 721ee413ceba2cbcd74cbb02d148d244e5fcba77..362c92bcfa92e1ecce2b5183e734941dfec5aa6a 100644 GIT binary patch delta 2278 zcmZ`)TW=Fb6yDiguh;8qoH&V{V4OQnLP7}PCi2pTTU4c`RM3V?usoY4!MU)zCIoGi zaH%TPQbgyeQdJUFmEfULfTC7KwD1qsulrJ|LRAqD?F;Hf6!o03V~nbIKs{7}@&}=ZM3Gh68{OJ9^JFbTs$@A-7He)To_krHLbbq z8I5x^riTFWk1Mp6)?JoI6I|;9`L*;wIDEg{Xc1&|a+TJ;FUV`zE+j@(Au0ck3GYt^gD zs75>JCP16ZXmQ=KMp&4podC6zH|yf3M5LQ;2Bfu&XnGwSu7~!5wvB7Ilp>1pWB1Xm zfV9*8!$NxC?sGCIvWGIQYX;t3DU-{kB_jfJqX|VbYYJAg5s%hegP$8vw1IHtyr~;m z5vJ>p3=nv$tZ#jD~)GB2b(BhL=N{>}~O2j@Q9 zsl3;l_8LjhxawTSH0Ngx>U!oTW@mI)8=o{xYiuSnt-HQlrZiIOU^ikpWn(tqMcJ3GKMJ=vh=Ozb)AwTe&0=;mm>a0pK=J;&z?wb#6loXIjeS zhB>)K*S<2j+Q_p`)~htKFMZK%n;^b-ZV@-ApUdX*R3FRFn7YOJ8jY;eF_xX30h*_I z3XBf+r*D`<*z4MBTV2njrB9o#7ZznK%P{)EpRo;vgkk^$q>fi&6)M`a*I8U{!$9B zuN2`l}D=Ok;yD9md;MDoTx$QiFj=SUBFI;o(6)vWn zADy+rGB#o1V@3k;pn;h)iuoFVfO?OtwM#^aY_mo;q!avp7S6DIv^@l zxZvDIlHk2o$=Ef6!19qNDUfLiTO@1fmg%Y(lr;(WRnVF}t=RgyDi=+J{>+!nGgm{=z&5TnVm$H?H~Qy^Yp1n+%3BWDED zMhV0%*hRe4rGRlD@s7@y-Y$yltNbs*Dm%uwEm@tv%Q-r}Zo zivmtOR)ob|RzmuOWV__{Hg{xs-K+;0_px+Vi|J)2kts>0kH?uPgK}rVV*RX)OxJO? zKAvc?gU$kF18flK?qj+s>_p)Z8%Cywja*jBqx(bZq)HpsT9tb%^KWt>S{`e5-pfYPjP{K$gsH)hqa_uPy0JW9p8&Mf?!@t!=WQ`Ziib zL$mIM_4f`BaNaun5}gP2t+L@2MZqqNhSKn|d6gvKb91mzl1>3pgobr}zFNJx>M_rP zN9I#@zhFiXU@FvG=|H18f#9er(TPgTza8VqqNcEfSD^WqYEc5`Xc``bifV?y525Eb z%?TyY_e>UIN=+pSvjY8w;!OoQv+_!yErs6DyeZ!bFiLo^eN#U1)g_zR%}-}kDrS+V zQbAEM>xZQRJ*b#%DZ)(!?&s8$DP@+vqF&lCHcUSfXn_%!+Zs{cnt$8D9!EuwagGIA zji9AnRW2*K;>T~je|!!Jc@E8Doa1wdz!z4fa{ya%o-LTKEU*p&P*H>4(F}M^yOFVctJW5W(H{t1$Jn@*w zB+j+0w|)=dkOlXPdkzy$lDq@c&IjZ!{Ny}mo)mmuQcp+P$cOvW5l(Q=gXsD66N3A% z>*h^3ndQu{{y?PyAl|e-X9BOXO?#J@H#eHgK(iUCF{9GzpUl2hwe4!cx-8 zccpET`~vsd#$y-ppC)3HE`)L~+|H)S4BXF-lLpkY>!$p|K6oMbB;3t)4T(GKC=Q8> aq2>?^x)`pW(F?tqN- Dict[str, Any]: "is_approved": review.is_approved, "created_at": review.created_at, "updated_at": review.updated_at, - "user_username": user.username if user else "Неизвестный пользователь" + "user_email": user.email if user else "Неизвестный пользователь" } diff --git a/backend/app/repositories/user_repo.py b/backend/app/repositories/user_repo.py index df14ce7..eb8c092 100644 --- a/backend/app/repositories/user_repo.py +++ b/backend/app/repositories/user_repo.py @@ -17,34 +17,26 @@ def get_user_by_email(db: Session, email: str) -> Optional[User]: return db.query(User).filter(User.email == email).first() -def get_user_by_username(db: Session, username: str) -> Optional[User]: - return db.query(User).filter(User.username == username).first() - - def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[User]: return db.query(User).offset(skip).limit(limit).all() def create_user(db: Session, user: UserCreate) -> User: - # Проверяем, что пользователь с таким email или username не существует + # Проверяем, что пользователь с таким email не существует if get_user_by_email(db, user.email): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Пользователь с таким email уже существует" ) - if get_user_by_username(db, user.username): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Пользователь с таким username уже существует" - ) - # Создаем нового пользователя hashed_password = get_password_hash(user.password) db_user = User( email=user.email, - username=user.username, - hashed_password=hashed_password, + password=hashed_password, + phone=user.phone, + first_name=user.first_name, + last_name=user.last_name, is_active=user.is_active, is_admin=user.is_admin ) @@ -75,12 +67,12 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User: # Если предоставлен новый пароль, хешируем его if "password" in update_data and update_data["password"]: - update_data["hashed_password"] = get_password_hash(update_data.pop("password")) + update_data["password"] = get_password_hash(update_data.pop("password")) # Удаляем поле password_confirm, если оно есть update_data.pop("password_confirm", None) - # Проверяем уникальность email и username, если они изменяются + # Проверяем уникальность email, если он изменяется if "email" in update_data and update_data["email"] != db_user.email: if get_user_by_email(db, update_data["email"]): raise HTTPException( @@ -88,13 +80,6 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User: detail="Пользователь с таким email уже существует" ) - if "username" in update_data and update_data["username"] != db_user.username: - if get_user_by_username(db, update_data["username"]): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Пользователь с таким username уже существует" - ) - # Применяем обновления for key, value in update_data.items(): setattr(db_user, key, value) @@ -131,11 +116,11 @@ def delete_user(db: Session, user_id: int) -> bool: ) -def authenticate_user(db: Session, username: str, password: str) -> Optional[User]: - user = get_user_by_username(db, username) +def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: + user = get_user_by_email(db, email) if not user: return None - if not verify_password(password, user.hashed_password): + if not verify_password(password, user.password): return None return user @@ -241,4 +226,53 @@ def delete_address(db: Session, address_id: int, user_id: int) -> bool: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Ошибка при удалении адреса" - ) \ No newline at end of file + ) + + +# Функции для работы с паролями и токенами сброса +def update_password(db: Session, user_id: int, new_password: str) -> bool: + """Обновляет пароль пользователя""" + db_user = get_user(db, user_id) + if not db_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Пользователь не найден" + ) + + hashed_password = get_password_hash(new_password) + db_user.password = hashed_password + + try: + db.commit() + return True + except Exception: + db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Ошибка при обновлении пароля" + ) + + +def create_password_reset_token(db: Session, user_id: int) -> str: + """Создает токен для сброса пароля""" + import secrets + import datetime + + # В реальном приложении здесь должна быть модель для токенов сброса пароля + # Для примера просто генерируем случайный токен + token = secrets.token_urlsafe(32) + + # В реальном приложении сохраняем токен в базе данных с привязкой к пользователю + # и временем истечения срока действия + + return token + + +def verify_password_reset_token(db: Session, token: str) -> Optional[int]: + """Проверяет токен сброса пароля и возвращает ID пользователя""" + # В реальном приложении проверяем токен в базе данных + # и его срок действия + + # Для примера просто возвращаем фиктивный ID пользователя + # В реальном приложении это должна быть проверка в базе данных + return 1 # Фиктивный ID пользователя \ No newline at end of file diff --git a/backend/app/routers/__pycache__/auth_router.cpython-310.pyc b/backend/app/routers/__pycache__/auth_router.cpython-310.pyc index 9fd8df2547e794c99657b87f4260ca46cb148168..ec953bf88f5648bdcf9d48f7289d5fbf511cc80c 100644 GIT binary patch literal 2230 zcmZ`*O>7%Q6yBL#uN}vBoWyaGHl?O5KR%F^5J!ZlS`Y{+L~RjbFV@yGaoz5E-I+CQ ztRN+%;sRB``-KJZQ^1v zZ^85HA3wLg$ywHKsEj{gv)+UuZxG9379*h*5uZfVr;+X3k>fiA?KI3puJ4+;9cH7P zpEGkOoQU#%KAQ9=jh+b$QPD3(CBI~JH=K&fe%Z{k;dC_P&qTBSY*g_p(VRaQRsAZl zIAJ+9(IPB=llt>~{_+Bw zF|+5nr}mV#$>Ye5^Ep=I)zQx9*$K1rId<~ARX_DR)~D|1iPNj^o{M`47dm^EcX@|N zU3}-lh1IvNH+eT{#~tm;BuIL)PW0mYr+dkzS62fmuf>9$<5zoJCU3_gg2n84E+zQa zsdmn`n~Anhch*61TRds7i@J>OW={y-Ng6%L1?*IEl%X&uVKLF*7;6cK5=0bdh zcXaXKmn2VgDOJy03OX&W9o*5+cmV{}TLL`l%fa^GQ*~E80J%4~q4v~IYIpD%%x_^N zUBTDd*R_)bE!m&&1aGw^AcsIog3E5)k-QPbjECC605xS7n-mi@dT{Q*_yvY6f!HF6 z^)1;T8`cgzZ`~#I6Q*#|B%m==mb>dZm;QJWen?`# zbhALk5t#H{FN|C5j-G&nqXr9-;0c5__+(%hgc#=m!pn5Sx@_;*pU@4!D&}Fa?hMi9 z&6x(O&SB;{LBw^gJK{;*7DvI`z}GGS>xGF)!cZ*2nywCyK)3;-m5l@`FavAS1wl7w z9EXVGFzru4<$w>br046r(Vs^1M7_$ebsP;=tWJA%FsrZBu6j7wRQrRintGsiYlAKI zeX2hM3c-@PH@LL|P)vQRV#xhQ8biZ`pffxQ+99x@2FDt}hk+u~;3A~^uqb~HOH{JO z3DEk}9)j!eYX<-`CK=)wEH|-4>GMRn#sEU8Hpq@`0KNy#>bt=fT!~%4wy*Bsr9hZC z-B&-T{hAuXh{}Eqy!XKmuhRBdHq>rPaT6<~;}jfk0{dUIWyA_l9^X41OgVL7ykk6t9hq^Ms&s=4sS0#;b{vy~55Z4;t$tMZ5s?{t>h2-fyXp?2 zhFb4|Cng8;^f19kP-q3CGZ~m<j6&H1hL#f_tt2Ua4(ucXMO#Ycs!#rRnE#rp~Gd#H%IG&|e2>^e}3(6Az>*kdMwzQFv75juh;2 z>_jN%4#Nw)Os?q^tfZh-9ar7VOG+LIuOyq=$f_y0GBzS*OrEq|v#i+Dxs`lrt4sAi zyEHh8co6YQTA4{T+0oZ5ssmq8wSBLV++@ygrPdh^xuq(8LuD;-+Q*0PTwU$T&y-(TR^hDmLr8Rg{g(1hQo!S znK76_li4qs5hx7Xa1qu{{ ztP^9X(o9b+i7zP1Pbp0;kWpu{j-afFJ3|Un3UdoX3Uf146k7^QFoP!R zWPd@e$safs85Jf63QA5sz^g9U2Gm=m1|n2}gr6q+U;$jvc!NnlLSmZF-Q9y>N*lcpDkSSXV3s4v9h4{=>||!wvuuksIv* diff --git a/backend/app/schemas/__pycache__/review_schemas.cpython-310.pyc b/backend/app/schemas/__pycache__/review_schemas.cpython-310.pyc index 0bda2715f848d6a4dc49508a7c16f0d281f9b9f9..0b620ca65b030db560dc7a01eb7bf09e7f21f3f5 100644 GIT binary patch delta 38 scmaFK`;3=2pO=@50SJ4IKag diff --git a/backend/app/schemas/__pycache__/user_schemas.cpython-310.pyc b/backend/app/schemas/__pycache__/user_schemas.cpython-310.pyc index 15b9d0e2b4ed83c7f3d29c15b4192ffec778526c..302ee9bb22639be62d9864b8efa5aeb623753fa9 100644 GIT binary patch delta 1158 zcmZuw%}*0S6yIsP+uhlZ7HF~bLnNs{`7#I^6Fx*pfS6cNB$|x@8)kqS+LGBKM-Ff> zQ4eJP0l4@NaPnZHXD=q+*2F{)CVDemxaoTfl*G83_uKb-^XARW@6CSBed=&(woMgy zKK}Z!) zC)%d)2n&xYEad83A<&C1R zZm!p=+^4H$;nkO`r3&|rjnbL>&^OB7a%p9*UVhG}eH{s_l&b|>G{MP4GeQeO7$J$! zji4bU5DExA2)zic2z`=Rsn$+#G1+qfsG^fNzVbx-P6p+dXr0`U^ZI0R5N8CXg7PZT z5iHQw4=#>+C^JFvA);UY(Q@RCJPhaMfo<-%29IzZ6x0|hMnIu+j*1Z%^x{@9 zt_9p~a8Qvuj4+)UuS0IEF^>j$@dh^`uZfwvgjvkqQ+A2l%m^s%_;gY38SUxd@O`-V z0fcJ^!O5=6Bco^zAs`|*`6A$YaEym_|zz(o&z ztID?<1I0pAzO>R>(3!iqgUl=}{0yvd_!JQsD;8R1-kHne9TwOtfr5((gee46L*V|! qEEKvlbJ5t^DD#sHKkhqumEKhF37h~kRJ`_Mw+MV3ReRrvLekh=x33}2JCh$qGlM2 zfhxLTXKTP7;XyC#idzG%6K{m~cRG*7wE-iacRRgSReP27T@PQ5ss!T%r{!ggG|bXs3w1yp1h}N z@LcxCPRNFB+zm957W#xDGKnk*j8UxC6=8DV8J>Na3M@Ql36G&<2<%a;Ae*OHjfxl(7Ppq@4~+W-Y$f<`^84(}})B-R$zx d;`BH+$Kkj!Tf#hEdyy~AWuIJRoNP^Mi+?PEX|VtR delta 322 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 KqR!0DWc~+4(+58Q delta 2408 zcmZ`*&2Jk;6yKTs^0%G%BS~F{Gzrujnzm_a(-w)gP|*_FR49kiNR`=oOlsG$&3Kcl z)mWAY5^#t(%(1Oxi&G?ogt&1)LL9k)xa^_*6AdfQZW89fJ8`u@B{L!nQP1$Ug8)$4PritMA?w&X|P=to<*4=2XQt91dj|WdtzwMlsufs>EoIvFI)3% zb=C~34(*%;o)YFzh^rz2GI-FJAPn@UxeUb9D&P;(7}=&RvZl4DAeXdT6D?in6$%5t z1`HF1<=Nmn@QYiN(MMLx5VR8zMwbxnldzyG+K#^L5I@*(d8y_V%FMBRT4ugcbJt70 z(X4K)`C(RS7J0ehmFqP>SZ)^FqF3H5`O)Ua?S5X$rRGjhF58(ihrvXiN8%uvL-INh zf3&bx@(TO5_?FvflxoZ|d0IZU4`u!6AkPI>E_!=IC7;-tHpZ_B5!GVUpaEj&2I;*a zVm*rl49ZX7eXjv2QXqrJg2@YlD2NIog&^vJsvsJ|fFK$WL<53oKoE_75P{$B2N5C* zK{We8)B#Pe!UcRDh-3AjgC_7JNRA^x5b*^hL)kwHyhRv2uZZ%B{?9Aa^6iGj>ujUw z$+^UVHb!BGF7uP{9pE^Ig#$Ql^z$*xPlqa84s%T^k=1&P%RF-x1aP5RyeK84F$G)AD|3`dxJp zW_{S?Gr(IxzA_nC`x0&g1Cx$cHgC&Pdj3u50~YpyE^Qf}4f*TMP0y|ban5vRZ0jvc zKn)2)n8IS_BXb2Zx-5SUFBt&Rlk!ybFbT=c=of9r?ge+(Rem1s;unxyMDjM0{n_@D zSOSB{vXbYPtIZEVuQ>%I0$8SMSpP<>h#t`sI@Kxg@U{NtmqE$5H(g$KYebxa?)Tr@gO|@RX#>*sdXcoedE5!nq_x{NVT?)z34H7=c@Ui05L1 zOuvttuE;;)>B;@R@~a?ufKrv&t9vuMl$gt_jW5dVHQ3X}eJZop;mmO~Q{T-4HXG)d?f7 zOCD;Ep*!MEF9MwC7GZW)^J0Q*k*wCT+QO>DSpeSPHYJ*u=!|r=hZ@s`30w7+Ei3^W zS^*L0jIvOdd=+m6y<{f^hNMNX3wv%r$2Q>I(ur0JEC49)K?}zBCJZkF_zMd#ghhl6 zXF{9C{>`G>Ml0myw9XhnGz8v;SxkiS2h52t^RpPHbMo0phn$uVM<-^k1H(7V>+V{~ z8CO51TxCy3Ma(C^B%h9cy{IZK3G3C={+3bb?Tzfgw$jCe0scQHSC0>4Ij%MOJ@cB!_>4TA$&*X6keRhULfsqk%goWdIutMCGf3_r8G`aU z@V=K%GYe#0j*NX^6;bD7d1ve`xoAwvZ^rU>U1Z^Z5??RZJU)fXStRNQ=_t;yuX5~n z9NU$uX1;_otQ3y0_l?{2dR0Y=qemPs-eZh1>!3V8KA$xw;hj!Rj!vuDWDdW1b9!#t F{0}KK<9Gl7 diff --git a/backend/app/services/__pycache__/order_service.cpython-310.pyc b/backend/app/services/__pycache__/order_service.cpython-310.pyc index 4f63063d39d6660118d6e671d0c3997a8f46f58c..2d9ba8a5448d6366293dfd90f6cfcd9c00370234 100644 GIT binary patch delta 1991 zcmaJ?O>7%Q6!!e>pVzLFHvNIbZIdR%B5JABl$17=AZa5&1Vkzh;b1wQCD_<;HM zo-K9?1uqBB<6pkt{d(%F;uo}M!(R*DHF(3f!AK%$E7vAnMsgADvp5&im>jS!k9bTZ zi+NKtEy-irv>QZ<2fQmHr|&|XIG`jKv%VMe1Jbpm6I%4*cnf6!YbugisLejGyd%WT0q1&*51Z8mc`DYiQ@XjSjkS`Ar?f{Lw*IC~6D z2~1KPuDDfyKWNwVm+Z{Rq*RA(stwqu8k*rpIdxut!*=FRKu#Xs@FW zSbzGK#~h+g=MXO0jHAk%obq0N`Qz7h0 z>BGBNR5d%Tnt|c8#yf)|=P(*8Rb?>S6KRSZ%Q=UW=QA^l-UToiA39)sQlcJ(!Y=6F zc;lK`aRH+aCnY5nv^)2L>U*lwHtRo!D{wGHfJZIr&%{+Sub+syrKhB;3Ir|y-m&d@eNW^R(`1OzhgRC(ywX`;# z$D>zhLNC6MaJ=WEwYWESgT3J6yXxkO{>j?RTO;iCynSg)jPVG8O1$h6LUjoSjxOlz z29W{fGE9a@LZA3-l2MNAvHnt5bl*O8m^E)fR95u=$O$9`f_g|nObo4QVQ=uv9L{cy$`hGMI{&BJv{mdexcfnWB*{Y8uUm zi31{HQj#}Yrc&E>_kxeAW-E0TC;fEWRB=b}Q`aEk6?j8b;7E8rK<5jPol8u}ZMCAm zbABYt`g8Y<-U`I3RWQRMWf0XGt?wB-kpX{PU#?Ez$Ls|gM81E!`U5m`+eN&A3)LSg~;Wr6l>DPm}eV|2X= z>+1X7yHhv8Nnv-#VE@&d>mep7*v=RwP2q^Rsa{yrDM=w z@F_?;p*!e=-ax#FM&j0Md-b5z0-_SdbI=EGgR`oC&cD+ep)^KLm$3%O z1~bBM{L^6YL)5khq)f3>w=m@?$R$1;YZ1dr_(Z|OpDsUnbBn$Z&rU>m_K%W)-8*& Nn4MUhTq-V1`~%h%h2j7J delta 1603 zcmZux&2JM&6!(1WdhM(?ZUUiAOi~hxY88?oDpV*SRUmo*tWv5T(t~9@+hF6^FuTxJ zTFWZwfeWWmZyb_KC63&B?Wu>JdZ;Sxy?+4re*NAo{uE=)^X$xj7qQ&mys!q);i(AYyPsOR*M`2AblJFqX#ZGUx-sm%YroVmpc$ZU+dnUwY!F0dpn@p z>*j`go~@ZD?#AE^*YyUpPY!SRbc1%99AY@g6Ph;Ln^?RK3p?a}9&^PM{{gwbmfL6h ztS`U<5ku}H=g$T5D{D!+Cm#^hDo^?5b}A`Zvv3NRcEfJ??E`(~jQJ^FW77PW z-?}Ns8BdF&L`S*`YqJ)--7vO+pu@2-|NfwjVPJvk5(<;=O z7%+oNpT*vJ%!nvI2?9K1R_KsBPJI5*eLbN}M(t!F{2Z zViC)>LqvJTpMVw#8iFN;`E%~8IWmyN$u*z)8-tJVU3JG9*A@wd$ihiW!z#*RHBmvN zLdpCb(*h~8U=j$OH3h_QOwk}~WaSvE<*{nmsa;3-@SW)n~aY8SfPEa@ZM8*6bEV9)66`a0GozZq5y-q9Y s0-B25GAosLd%cb&Lgw&gIvKxiR~7Tc{Co5B>&_~E>!tUD)4{3I|0RViu>b%7 diff --git a/backend/app/services/__pycache__/review_service.cpython-310.pyc b/backend/app/services/__pycache__/review_service.cpython-310.pyc index ee044b88c4df4f82fd7d7bed96a7d7bd265ca75c..2a5ecb816c85733c5e2f1a2278948bab05c8446d 100644 GIT binary patch literal 2051 zcmaJ?OK;mo5a#akC54h@xp89W)fNd-7KRk`Y#1qG0?>*Q@|BU{Pz4p@lgQ7jOGfPsI-Ny<$+Q;lXzWrvIcRFng+F!r@GI`mutY2}m z+SXw561w^w1h+Uzt&EHa!7)u~#zri&N4A->)XChDYsPl!W&X&|TB8=s9YMG&+T0U9 z_XQJogndl6tYe?Ic>CNQ1^mHMCop;~-VvP}x&iO;+$&=_@IsLNv2CGmbOLkF+;eLXH!z-Rd0v7jrp{8TzT43;z!E zSJ2fC2u=WUfPcY=RnQAoS|u&XC0p1fyRb|9k}Mp~N+%(Q)|GuA37oki+O2|-c+sJw zx1-r?pyF>t7O6oUc0H9{P?6|UcVZJeB!qeCZkBVAj!&aB!6ZUg`lt$wv5eg&$3tUx z)SenF%+bybA6J(A2oC-6cK8M$P{a4h$#6f9=b4xmYWS7RCo;;^kW0W>@bPgj$7&wO zLXB0COEEl(;uA6DLr7u>22K;$8a5uP`CxV?d*B0uQy6AIy0lGwVpd=4R;>>3`-vJy zJWHlACfzhV<3TsTzktCKcxMH8RuX{RS-7PG;JX062jF`Mz6{_{=twM{RZQoG0X3#_ z9hLkQAU9DRZ%N)m(FZZ=R%du~Xajc;!Aji4(R~o8TqoPFlEu~72GKN35jkbj!rj8U zAQgv7c4;qc4hdoSAwAGug}Mr9%?G%CQHVMW$nE<6P1q*lEiigJxNTT_09|1yJp#9Y zD=`JpGz<)PO<YW#@(MCxG=9OcG&^bF_wMP5N3oI zX`?XniB(!}y#@KfxIx7ibdg zZ(8r4Ped_pfT+q#4R+H!j#9M;gXWCx6nPP)HH#FyMqaHAFJM$8(}{MBhI|Z$d)Hn#qYbX)~T|NX6_H|diOQRHD!m~jxGl?K+w>X5i) zR02<1B9y7L>d`>5NMwy>$rQdas*vXO()LlFr>5df9Z4jjiDRYi%=3ULXnz;p?RhHp YFg%5%51Ob{rf?fJiimLy=9J4k=NJ6sCx1EI}~#idbNuHG4@Uv{oeO z(`=gpmuZmr3Di{Sq6`0l9vWuW7t4xrry0#_cfXn4uhpw^oJP?i!|%(lpC2yOkNA|I z1n4T1EflkYDr4WcWmZ@|kz?88R`&TGI~OgWrHKHl&Z92qPNw%kANBQd62JmR;SMP( zYcCGuk>R1Z2;6~^ieYFI?@ zo~%Y>HTPQ{4E#2<11yuGlG!o__t2%dUsa`56zK|7*Pld#y7jG5wrGt{#8_ZcVy-E7 z5!7EMa@qx*+^%#+BS??*?c5CNSo8qJ1C$A%?lx6pn+8haPkn*L^{IH>auSRK^aWl% zQ79W&WpTi(^cZAMoT)vQ$d*gP=XYsySVfgc_dRUBEk=Bw!m}JaB=-@i8v|O$*kcq! z-Bfv53MqKV1^(ebplmDQJ9wQOO;~ZSN?Y&pHEg+8rhuMGF8jkQ mmnH+CSCt3v)AWGX8)$Bo9|{SEVN<^-Qg+4fP+7dY2>s diff --git a/backend/app/services/__pycache__/user_service.cpython-310.pyc b/backend/app/services/__pycache__/user_service.cpython-310.pyc index 5e20e81d3de3176de53ab5860b6eaf8d6585c115..b136a1e6e4638f6b7017a3f2628f75d90cc26b5e 100644 GIT binary patch literal 5858 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 UkRdWk(kwfWp2&`6^(pxM2H?9K8vp Dict[str, Any]: + from app.schemas.catalog_schemas import Category as CategorySchema + new_category = catalog_repo.create_category(db, category) - return {"category": new_category} + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(new_category) + return {"category": category_schema} def update_category(db: Session, category_id: int, category: CategoryUpdate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Category as CategorySchema + updated_category = catalog_repo.update_category(db, category_id, category) - return {"category": updated_category} + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(updated_category) + return {"category": category_schema} def delete_category(db: Session, category_id: int) -> Dict[str, Any]: @@ -33,51 +41,55 @@ def delete_category(db: Session, category_id: int) -> Dict[str, Any]: def get_category_tree(db: Session) -> List[Dict[str, Any]]: + from app.schemas.catalog_schemas import Category as CategorySchema + # Получаем все категории верхнего уровня root_categories = catalog_repo.get_categories(db, parent_id=None) result = [] for category in root_categories: + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(category) # Рекурсивно получаем подкатегории - category_dict = { - "id": category.id, - "name": category.name, - "slug": category.slug, - "description": category.description, - "is_active": category.is_active, - "subcategories": _get_subcategories(db, category.id) - } + category_dict = category_schema.model_dump() + category_dict["subcategories"] = _get_subcategories(db, category.id) result.append(category_dict) return result def _get_subcategories(db: Session, parent_id: int) -> List[Dict[str, Any]]: + from app.schemas.catalog_schemas import Category as CategorySchema + subcategories = catalog_repo.get_categories(db, parent_id=parent_id) result = [] for category in subcategories: - category_dict = { - "id": category.id, - "name": category.name, - "slug": category.slug, - "description": category.description, - "is_active": category.is_active, - "subcategories": _get_subcategories(db, category.id) - } + # Преобразуем объект SQLAlchemy в схему Pydantic + category_schema = CategorySchema.model_validate(category) + category_dict = category_schema.model_dump() + category_dict["subcategories"] = _get_subcategories(db, category.id) result.append(category_dict) return result def create_product(db: Session, product: ProductCreate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Product as ProductSchema + new_product = catalog_repo.create_product(db, product) - return {"product": new_product} + # Преобразуем объект SQLAlchemy в схему Pydantic + product_schema = ProductSchema.model_validate(new_product) + return {"product": product_schema} def update_product(db: Session, product_id: int, product: ProductUpdate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Product as ProductSchema + updated_product = catalog_repo.update_product(db, product_id, product) - return {"product": updated_product} + # Преобразуем объект SQLAlchemy в схему Pydantic + product_schema = ProductSchema.model_validate(updated_product) + return {"product": product_schema} def delete_product(db: Session, product_id: int) -> Dict[str, Any]: @@ -86,6 +98,10 @@ def delete_product(db: Session, product_id: int) -> Dict[str, Any]: def get_product_details(db: Session, product_id: int) -> Dict[str, Any]: + from app.schemas.catalog_schemas import Product as ProductSchema, Category as CategorySchema + from app.schemas.catalog_schemas import ProductVariant as ProductVariantSchema + from app.schemas.catalog_schemas import ProductImage as ProductImageSchema + product = catalog_repo.get_product(db, product_id) if not product: raise HTTPException( @@ -105,23 +121,36 @@ def get_product_details(db: Session, product_id: int) -> Dict[str, Any]: # Получаем отзывы продукта reviews = review_repo.get_product_reviews(db, product_id, limit=5) + # Преобразуем объекты SQLAlchemy в схемы Pydantic + product_schema = ProductSchema.model_validate(product) + variants_schema = [ProductVariantSchema.model_validate(variant) for variant in variants] + images_schema = [ProductImageSchema.model_validate(image) for image in images] + return { - "product": product, - "variants": variants, - "images": images, + "product": product_schema, + "variants": variants_schema, + "images": images_schema, "rating": rating, "reviews": reviews } def add_product_variant(db: Session, variant: ProductVariantCreate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductVariant as ProductVariantSchema + new_variant = catalog_repo.create_product_variant(db, variant) - return {"variant": new_variant} + # Преобразуем объект SQLAlchemy в схему Pydantic + variant_schema = ProductVariantSchema.model_validate(new_variant) + return {"variant": variant_schema} def update_product_variant(db: Session, variant_id: int, variant: ProductVariantUpdate) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductVariant as ProductVariantSchema + updated_variant = catalog_repo.update_product_variant(db, variant_id, variant) - return {"variant": updated_variant} + # Преобразуем объект SQLAlchemy в схему Pydantic + variant_schema = ProductVariantSchema.model_validate(updated_variant) + return {"variant": variant_schema} def delete_product_variant(db: Session, variant_id: int) -> Dict[str, Any]: @@ -130,6 +159,8 @@ def delete_product_variant(db: Session, variant_id: int) -> Dict[str, Any]: def upload_product_image(db: Session, product_id: int, file: UploadFile, is_primary: bool = False) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductImage as ProductImageSchema + # Проверяем, что продукт существует product = catalog_repo.get_product(db, product_id) if not product: @@ -168,12 +199,18 @@ def upload_product_image(db: Session, product_id: int, file: UploadFile, is_prim new_image = catalog_repo.create_product_image(db, image_data) - return {"image": new_image} + # Преобразуем объект SQLAlchemy в схему Pydantic + image_schema = ProductImageSchema.model_validate(new_image) + return {"image": image_schema} def update_product_image(db: Session, image_id: int, is_primary: bool) -> Dict[str, Any]: + from app.schemas.catalog_schemas import ProductImage as ProductImageSchema + updated_image = catalog_repo.update_product_image(db, image_id, is_primary) - return {"image": updated_image} + # Преобразуем объект SQLAlchemy в схему Pydantic + image_schema = ProductImageSchema.model_validate(updated_image) + return {"image": image_schema} def delete_product_image(db: Session, image_id: int) -> Dict[str, Any]: diff --git a/backend/app/services/order_service.py b/backend/app/services/order_service.py index 3267516..68a0d79 100644 --- a/backend/app/services/order_service.py +++ b/backend/app/services/order_service.py @@ -9,6 +9,8 @@ from app.schemas.content_schemas import AnalyticsLogCreate # Сервисы корзины и заказов def add_to_cart(db: Session, user_id: int, cart_item: CartItemCreate) -> Dict[str, Any]: + from app.schemas.order_schemas import CartItem as CartItemSchema + new_cart_item = order_repo.create_cart_item(db, cart_item, user_id) # Логируем событие добавления в корзину @@ -20,12 +22,18 @@ def add_to_cart(db: Session, user_id: int, cart_item: CartItemCreate) -> Dict[st ) content_repo.log_analytics_event(db, log_data) - return {"cart_item": new_cart_item} + # Преобразуем объект SQLAlchemy в схему Pydantic + cart_item_schema = CartItemSchema.model_validate(new_cart_item) + return {"cart_item": cart_item_schema} def update_cart_item(db: Session, user_id: int, cart_item_id: int, cart_item: CartItemUpdate) -> Dict[str, Any]: + from app.schemas.order_schemas import CartItem as CartItemSchema + updated_cart_item = order_repo.update_cart_item(db, cart_item_id, cart_item, user_id) - return {"cart_item": updated_cart_item} + # Преобразуем объект SQLAlchemy в схему Pydantic + cart_item_schema = CartItemSchema.model_validate(updated_cart_item) + return {"cart_item": cart_item_schema} def remove_from_cart(db: Session, user_id: int, cart_item_id: int) -> Dict[str, Any]: @@ -39,11 +47,15 @@ def clear_cart(db: Session, user_id: int) -> Dict[str, Any]: def get_cart(db: Session, user_id: int) -> Dict[str, Any]: + from app.schemas.order_schemas import CartItem as CartItemSchema + + # Получаем элементы корзины с деталями продуктов cart_items = order_repo.get_cart_with_product_details(db, user_id) # Рассчитываем общую сумму корзины total_amount = sum(item["total_price"] for item in cart_items) + # Примечание: cart_items уже содержит сериализованные данные из репозитория return { "items": cart_items, "total_amount": total_amount, @@ -52,6 +64,8 @@ def get_cart(db: Session, user_id: int) -> Dict[str, Any]: def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any]: + from app.schemas.order_schemas import Order as OrderSchema + new_order = order_repo.create_order(db, order, user_id) # Логируем событие создания заказа @@ -62,7 +76,9 @@ def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any ) content_repo.log_analytics_event(db, log_data) - return {"order": new_order} + # Преобразуем объект SQLAlchemy в схему Pydantic + order_schema = OrderSchema.model_validate(new_order) + return {"order": order_schema} def get_order(db: Session, user_id: int, order_id: int, is_admin: bool = False) -> Dict[str, Any]: @@ -80,6 +96,8 @@ def get_order(db: Session, user_id: int, order_id: int, is_admin: bool = False) def update_order(db: Session, user_id: int, order_id: int, order: OrderUpdate, is_admin: bool = False) -> Dict[str, Any]: + from app.schemas.order_schemas import Order as OrderSchema + updated_order = order_repo.update_order(db, order_id, order, is_admin) # Проверяем права доступа @@ -89,10 +107,14 @@ def update_order(db: Session, user_id: int, order_id: int, order: OrderUpdate, i detail="Недостаточно прав для обновления этого заказа" ) - return {"order": updated_order} + # Преобразуем объект SQLAlchemy в схему Pydantic + order_schema = OrderSchema.model_validate(updated_order) + return {"order": order_schema} def cancel_order(db: Session, user_id: int, order_id: int) -> Dict[str, Any]: + from app.schemas.order_schemas import Order as OrderSchema + # Отменяем заказ (обычный пользователь может только отменить заказ) order_update = OrderUpdate(status="cancelled") updated_order = order_repo.update_order(db, order_id, order_update, is_admin=False) @@ -104,4 +126,6 @@ def cancel_order(db: Session, user_id: int, order_id: int) -> Dict[str, Any]: detail="Недостаточно прав для отмены этого заказа" ) - return {"order": updated_order} \ No newline at end of file + # Преобразуем объект SQLAlchemy в схему Pydantic + order_schema = OrderSchema.model_validate(updated_order) + return {"order": order_schema} \ No newline at end of file diff --git a/backend/app/services/review_service.py b/backend/app/services/review_service.py index 128c7e8..e873b2d 100644 --- a/backend/app/services/review_service.py +++ b/backend/app/services/review_service.py @@ -7,13 +7,21 @@ from app.schemas.review_schemas import ReviewCreate, ReviewUpdate # Сервисы отзывов def create_review(db: Session, user_id: int, review: ReviewCreate) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + new_review = review_repo.create_review(db, review, user_id) - return {"review": new_review} + # Преобразуем объект SQLAlchemy в схему Pydantic + review_schema = ReviewSchema.model_validate(new_review) + return {"review": review_schema} def update_review(db: Session, user_id: int, review_id: int, review: ReviewUpdate, is_admin: bool = False) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + updated_review = review_repo.update_review(db, review_id, review, user_id, is_admin) - return {"review": updated_review} + # Преобразуем объект SQLAlchemy в схему Pydantic + review_schema = ReviewSchema.model_validate(updated_review) + return {"review": review_schema} def delete_review(db: Session, user_id: int, review_id: int, is_admin: bool = False) -> Dict[str, Any]: @@ -22,18 +30,27 @@ def delete_review(db: Session, user_id: int, review_id: int, is_admin: bool = Fa def approve_review(db: Session, review_id: int) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + approved_review = review_repo.approve_review(db, review_id) - return {"review": approved_review} + # Преобразуем объект SQLAlchemy в схему Pydantic + review_schema = ReviewSchema.model_validate(approved_review) + return {"review": review_schema} def get_product_reviews(db: Session, product_id: int, skip: int = 0, limit: int = 10) -> Dict[str, Any]: + from app.schemas.review_schemas import Review as ReviewSchema + reviews = review_repo.get_product_reviews(db, product_id, skip, limit) # Получаем рейтинг продукта rating = review_repo.get_product_rating(db, product_id) + # Преобразуем объекты SQLAlchemy в схемы Pydantic + reviews_schema = [ReviewSchema.model_validate(review) for review in reviews] + return { - "reviews": reviews, + "reviews": reviews_schema, "rating": rating, "total": rating["total_reviews"], "skip": skip, diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index 9aaf83a..10cf894 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -11,9 +11,14 @@ from app.schemas.user_schemas import UserCreate, UserUpdate, AddressCreate, Addr # Сервисы аутентификации и пользователей def register_user(db: Session, user: UserCreate) -> Dict[str, Any]: + from app.schemas.user_schemas import User as UserSchema + # Создаем пользователя db_user = user_repo.create_user(db, user) + # Преобразуем объект SQLAlchemy в схему Pydantic + user_schema = UserSchema.model_validate(db_user) + # Создаем токен доступа access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( @@ -21,7 +26,7 @@ def register_user(db: Session, user: UserCreate) -> Dict[str, Any]: ) return { - "user": db_user, + "user": user_schema, "access_token": access_token, "token_type": "bearer" } @@ -57,6 +62,9 @@ def login_user(db: Session, email: str, password: str) -> Dict[str, Any]: def get_user_profile(db: Session, user_id: int) -> Dict[str, Any]: + from app.schemas.user_schemas import User as UserSchema, Address as AddressSchema + from app.schemas.review_schemas import Review as ReviewSchema + user = user_repo.get_user(db, user_id) if not user: raise HTTPException( @@ -73,29 +81,121 @@ def get_user_profile(db: Session, user_id: int) -> Dict[str, Any]: # Получаем отзывы пользователя reviews = review_repo.get_user_reviews(db, user_id) + # Преобразуем объекты SQLAlchemy в схемы Pydantic + user_dict = { + "id": user.id, + "email": user.email, + "phone": user.phone, + "first_name": user.first_name, + "last_name": user.last_name, + "is_active": user.is_active, + "is_admin": user.is_admin, + "created_at": user.created_at, + "updated_at": user.updated_at, + "addresses": [] + } + user_schema = UserSchema.model_validate(user_dict) + addresses_schema = [AddressSchema.model_validate({ + "id": address.id, + "user_id": address.user_id, + "address_line1": address.address_line1, + "address_line2": address.address_line2, + "city": address.city, + "state": address.state, + "postal_code": address.postal_code, + "country": address.country, + "is_default": address.is_default, + "created_at": address.created_at, + "updated_at": address.updated_at + }) for address in addresses] + reviews_schema = [ReviewSchema.model_validate(review.__dict__) for review in reviews] + return { - "user": user, - "addresses": addresses, - "orders": orders, - "reviews": reviews + "user": user_schema, + "addresses": addresses_schema, + "orders": orders, # Заказы обрабатываются отдельно в сервисе заказов + "reviews": reviews_schema } def update_user_profile(db: Session, user_id: int, user_data: UserUpdate) -> Dict[str, Any]: + from app.schemas.user_schemas import User as UserSchema + updated_user = user_repo.update_user(db, user_id, user_data) - return {"user": updated_user} + # Преобразуем объект SQLAlchemy в схему Pydantic + user_schema = UserSchema.model_validate(updated_user) + return {"user": user_schema} def add_user_address(db: Session, user_id: int, address: AddressCreate) -> Dict[str, Any]: + from app.schemas.user_schemas import Address as AddressSchema + new_address = user_repo.create_address(db, address, user_id) - return {"address": new_address} + # Преобразуем объект SQLAlchemy в схему Pydantic + address_schema = AddressSchema.model_validate(new_address) + return {"address": address_schema} def update_user_address(db: Session, user_id: int, address_id: int, address: AddressUpdate) -> Dict[str, Any]: + from app.schemas.user_schemas import Address as AddressSchema + updated_address = user_repo.update_address(db, address_id, address, user_id) - return {"address": updated_address} + # Преобразуем объект SQLAlchemy в схему Pydantic + address_schema = AddressSchema.model_validate(updated_address) + return {"address": address_schema} def delete_user_address(db: Session, user_id: int, address_id: int) -> Dict[str, Any]: success = user_repo.delete_address(db, address_id, user_id) - return {"success": success} \ No newline at end of file + return {"success": success} + + +def request_password_reset(db: Session, email: str) -> Dict[str, Any]: + """Запрос на сброс пароля""" + # Проверяем, существует ли пользователь с таким email + user = user_repo.get_user_by_email(db, email) + if not user: + # Не сообщаем, что пользователь не существует (безопасность) + return {"message": "Если указанный email зарегистрирован, на него будет отправлена инструкция по сбросу пароля"} + + # Генерируем токен для сброса пароля + reset_token = user_repo.create_password_reset_token(db, user.id) + + # В реальном приложении здесь должна быть отправка email + # Для примера просто возвращаем токен + return { + "message": "Инструкция по сбросу пароля отправлена на указанный email", + "token": reset_token # В реальном приложении не возвращаем токен + } + + +def reset_password(db: Session, token: str, new_password: str) -> Dict[str, Any]: + """Сброс пароля по токену""" + # Проверяем токен и получаем пользователя + user_id = user_repo.verify_password_reset_token(db, token) + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Недействительный или устаревший токен сброса пароля" + ) + + # Обновляем пароль + user_repo.update_password(db, user_id, new_password) + + return {"message": "Пароль успешно изменен"} + + +def change_password(db: Session, user_id: int, current_password: str, new_password: str) -> Dict[str, Any]: + """Изменение пароля авторизованным пользователем""" + # Проверяем текущий пароль + user = user_repo.get_user(db, user_id) + if not user or not user_repo.verify_password(current_password, user.password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Неверный текущий пароль" + ) + + # Обновляем пароль + user_repo.update_password(db, user_id, new_password) + + return {"message": "Пароль успешно изменен"} \ No newline at end of file diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 7627ac3..4afe6d4 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -1,14 +1,24 @@ import Link from "next/link"; -import { Search, Heart, User, ShoppingCart, ChevronLeft } from "lucide-react"; +import { Search, Heart, User, ShoppingCart, ChevronLeft, LogOut } from "lucide-react"; import { useState, useEffect } from "react"; import { motion } from "framer-motion"; import Image from "next/image"; import { useRouter } from "next/router"; +import authService from "../services/auth"; export default function Header() { const router = useRouter(); // Состояние для отслеживания прокрутки страницы const [scrolled, setScrolled] = useState(false); + // Состояние для отслеживания аутентификации пользователя + const [isAuthenticated, setIsAuthenticated] = useState(false); + // Состояние для отображения выпадающего меню пользователя + const [showUserMenu, setShowUserMenu] = useState(false); + + // Эффект для проверки аутентификации при загрузке компонента + useEffect(() => { + setIsAuthenticated(authService.isAuthenticated()); + }, []); // Эффект для отслеживания прокрутки useEffect(() => { @@ -25,6 +35,14 @@ export default function Header() { }; }, [scrolled]); + // Функция для выхода из системы + const handleLogout = () => { + authService.logout(); + setIsAuthenticated(false); + setShowUserMenu(false); + router.push('/'); + }; + // Функция для возврата на предыдущую страницу const goBack = () => { router.back(); @@ -35,6 +53,11 @@ export default function Header() { // Проверяем, находимся ли мы на странице категорий или коллекций const isDetailPage = router.pathname.includes("[slug]"); + // Функция для переключения отображения меню пользователя + const toggleUserMenu = () => { + setShowUserMenu(!showUserMenu); + }; + return (