From 336f9ea5591ddcb955b6d3e48135805b26bef6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Sun, 15 Mar 2026 23:30:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=D0=B8=D1=89=D0=B5=20minio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 --- apps/server-net/build_log.txt | Bin 0 -> 15000 bytes apps/server-net/build_log_err.txt | Bin 0 -> 3222 bytes apps/server-net/out.txt | 27 ++++ apps/server-net/out8.txt | 23 +++ .../src/Host/Controllers/ChatsController.cs | 43 ++--- .../src/Host/Controllers/FilesController.cs | 33 ++++ .../Host/Controllers/MessagesController.cs | 16 +- .../src/Host/Controllers/UsersController.cs | 57 ++----- apps/server-net/src/Host/Program.cs | 21 +-- .../Chats/Create/CreateChatCommandHandler.cs | 2 +- apps/server-net/src/Modules/Chats/Class1.cs | 2 +- .../Persistence/ChatsDbContext.cs | 12 +- .../20260311180825_InitialChats.Designer.cs | 2 +- .../Migrations/20260311180825_InitialChats.cs | 2 +- ...20260311215929_UpdateChatModel.Designer.cs | 2 +- .../20260311215929_UpdateChatModel.cs | 2 +- ...130_AddForwardedFromToMessages.Designer.cs | 2 +- ...260312094130_AddForwardedFromToMessages.cs | 2 +- ...2100324_AddReactionsToMessages.Designer.cs | 2 +- .../20260312100324_AddReactionsToMessages.cs | 2 +- ...260312174910_UpdateChatsSchema.Designer.cs | 2 +- .../20260312174910_UpdateChatsSchema.cs | 2 +- ...204842_SupportPinningAndMuting.Designer.cs | 2 +- .../20260312204842_SupportPinningAndMuting.cs | 2 +- ...313183634_AddStoryIdToMessages.Designer.cs | 2 +- .../20260313183634_AddStoryIdToMessages.cs | 2 +- ...32_AddStoryMediaInfoToMessages.Designer.cs | 2 +- ...60313201532_AddStoryMediaInfoToMessages.cs | 2 +- ...60313204219_AddChatDescription.Designer.cs | 2 +- .../20260313204219_AddChatDescription.cs | 2 +- .../Migrations/ChatsDbContextModelSnapshot.cs | 2 +- .../server-net/src/Modules/Identity/Class1.cs | 2 +- .../Persistence/IdentityDbContext.cs | 20 ++- ...20260311180816_InitialIdentity.Designer.cs | 2 +- .../20260311180816_InitialIdentity.cs | 2 +- ...IdentityWithNewFieldsAndTables.Designer.cs | 2 +- ...39_UpdateIdentityWithNewFieldsAndTables.cs | 2 +- ...215347_AddHideStoryViewsToUser.Designer.cs | 2 +- .../20260311215347_AddHideStoryViewsToUser.cs | 2 +- ...ideStoryViewsAndFixStoryViewer.Designer.cs | 2 +- ...4453_AddHideStoryViewsAndFixStoryViewer.cs | 2 +- ...15319_AddStoryReactionsReplies.Designer.cs | 2 +- ...20260313115319_AddStoryReactionsReplies.cs | 2 +- .../IdentityDbContextModelSnapshot.cs | 2 +- .../Knot.Shared.Infrastructure/Class1.cs | 2 +- .../DependencyInjection.cs | 32 +++- .../Knot.Shared.Infrastructure.csproj | 1 + .../Security/AesEncryptionService.cs | 106 ++++++++++++ .../Storage/S3FileStorageService.cs | 152 ++++++++++++++++++ .../src/Shared/Knot.Shared.Kernel/Class1.cs | 2 +- .../Security/IEncryptionService.cs | 16 ++ .../Storage/IFileStorageService.cs | 13 ++ apps/web/src/components/UserProfile.tsx | 6 +- apps/web/src/lib/i18n.ts | 24 +-- apps/web/src/lib/socket.ts | 5 - diff.txt | Bin 0 -> 12952 bytes docker-compose.yml | 21 ++- logs.txt | Bin 0 -> 75588 bytes logs2.txt | Bin 0 -> 78094 bytes 60 files changed, 551 insertions(+), 168 deletions(-) delete mode 100644 LICENSE create mode 100644 apps/server-net/build_log.txt create mode 100644 apps/server-net/build_log_err.txt create mode 100644 apps/server-net/out.txt create mode 100644 apps/server-net/out8.txt create mode 100644 apps/server-net/src/Host/Controllers/FilesController.cs create mode 100644 apps/server-net/src/Shared/Knot.Shared.Infrastructure/Security/AesEncryptionService.cs create mode 100644 apps/server-net/src/Shared/Knot.Shared.Infrastructure/Storage/S3FileStorageService.cs create mode 100644 apps/server-net/src/Shared/Knot.Shared.Kernel/Security/IEncryptionService.cs create mode 100644 apps/server-net/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs create mode 100644 diff.txt create mode 100644 logs.txt create mode 100644 logs2.txt diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 2222de4..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Knot Messenger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/apps/server-net/build_log.txt b/apps/server-net/build_log.txt new file mode 100644 index 0000000000000000000000000000000000000000..b8f969b54bc78d7966cea307cb3b4a138b5a90e8 GIT binary patch literal 15000 zcmeHOU2oeq6eWs06&Tt=fxu%_!!WGk8_Suw3kz zx69AS@_)CT!z-GUEqit+W5GrkiX`fiyreGgJv>+W^N&CUsvSm4?XVQK!V=DnupO@A z;Q38@uH$^DuPJRLkJ6`msJV=qyuS@8!TN7d`sEVuZgF>nyN{RTpzICG*s9bS?TIxb z#otHjJ~U)2WnG(kP1d0mKF~d~)!!%fV5ExCk{a%)uT@>WRz1~KXZYJtm(clzI#(SP zs|3$lc+yu_xa;FC!Tkl!n`#4NAwWi*pcC6l^W~|<$yoO^-JyBI0+o@a?HTQK3 zZ23pXHg(Sl`Zz`Vk$R=RQwKPUR2^jvNSx{3|E|tYY5+a^X!R2EFVRn&-k)l%dMLX> zKe5*I3Vp}uDceH@`cmUZI38kd0^L@EaW}LUL8{@tdaic$*q^AUTJ9H&VE}pR8{<4c zopbberptnn`3W#n)PpizNCkQZ0%ryS%t4GFb!bD&K3Yg{w~k)}@1OzMOO&i<-q0FK z29SJ<_Ijwp_tM3?i9~4kC!gvyBNW1-29qZNRZQ0v$4=aPK z->%qMY&n4)HDSkD>#b*eNFVEGnx(t`hO*v5y|*pS+Zqeq*Z!0drj@Zqw>(PY9HTis zbPxWxiSqzg@dwuO*6gjhcxxW@w-zl-^R!mVTcx+k;;eEuL^~em=fkzL)c1Dn?YcO- zo)0_h0weVFF0V5ME4Wg5)=K8X?h3Q$#H|}(hx!d)>Y?AUhA)ct@OOISTF>7Bw5$e)IM<^Nl^*-uO7&87TVAX8W!? z3;Zm11D5fk5g8NYlPfG z5ASRTOw9(kEcy5v|NY5mE;E~gwUwKvz~V{h@y)m-!>WBUM4Iq!QoWbVk@@`^IT zv$AD!$7F(6mLB4L7s-(sZ#2zj?d)WwvfSA?+x1^rSKC-u^sk$KWob*yTls!vjk3>+ zTv=J{GP){9V|E^|U345Ta%WO>q#xh?EHe7L5o9tNn~nqa5!V^HC8NWPRCSo~%06QT zrl)gSk}IMYt{~qdSfDn+W6_({;LnH!%4nSoT#Vi>aW@)`C66dsONm5`eTO>UQI^rr zV1&u}P03f5F^UdwYXXjt8N4g>*-_7Mbb)j()VFwYtx-=IK9{WvB4Jb%big=DUFd@t zIZf{KbBzcjdbY!+*3Q(XypdyEW*a4x)gi?Q+|;N=5P{9MuIebv$a@1hM(a;;&2gJd zE_>rWGqLlJ%*+|lh_ocAguQ)%-iUwrjM|A-ydz%Adt;It797zEKSA9_s(QzeQYbYtvctyghEw2+Y;y-|}sn@LNye9k%dS^55^_MaT!xpD>rr z-Oe`r8?*kh+gZnYGOJd;Uyyrx5u$pQiM83aD2PTZO>EMv8{>O!QtU7OMZ6%boXFj* zM6;6V8SOe_pW>^`TFv(R3Yq91@DmT{?#;}a&EJd6)`xg3w-0&`yY5QqxTvAb!Ym^+ zzDj0vmeDO{CsTxt`t`@}S@N;PCOCvD*5Bh9)}LV;%2*LxWBFDqk$!sYSxcFL@m;gj zUW|p>w(S>JL=Hw*dIF0Zj89>NPEsVrl~o33bFKNBBYX!!|3v;q_6y{1Uch^GfxVu= zkMT>E8s=6onRv7cN{P^ne@o!^$_wHF?$YWZJIU|o4Du2_L_BLgLqc>Q-?g!>kA6P- zait0xHWAiGzv(#g!*JdI2YveJ=cC`W9L`Y*Ux~%%dyCkM zav$w8DL#{8*h!b=^qCaPEc;A~{r`i!y(}~C{Ix?jm#~`V$xN<`EI+fOWFzJ4BFR@Z zLozX2!|&~+rBP;!4C$VhtZGT&zf6D3U@`+KSx;)h?}|)T!^SuE7^Jxm$$;>0q`tym Gw)huReKrpO literal 0 HcmV?d00001 diff --git a/apps/server-net/build_log_err.txt b/apps/server-net/build_log_err.txt new file mode 100644 index 0000000000000000000000000000000000000000..1a519eefecaa254f5f324af9b01bad38a40516e4 GIT binary patch literal 3222 zcmeHJTWb?h5FSdO1RPxt zZziXk)Fh~t(ucC_p3BV6nVB;)-~9aXN}ftv-pHnWz_=ql>C3)+k!{KFM6!!nACyQA z@no1~x^t}G`*MJBpu2sEF}{O@M2@73eR98$6#ou%z6tIfNQ! zET^vNQde+XarOys^JLX=iMzW@S^={HxLX*nEhnysm+K+;+b4Ue8(}@1_&;@#as*O> zXXjg8QhnOsjqtafv4S&h;*4lYe&SZfdqvf3q^JFjemudf?dW;5fcm|Kem(VNL+^^W zkzhZ7U(K=>_&?(1f1GdOrOqw4uKLqE^b9>j?ftw6jYptOYHnQQyag)#UdL;`qfE}o zle+JEg893yQr2+afwbg2V>b8BbYzDUk!?|Uh&b-aXX(gQxpJ;LE{d?T=G9~5RT=qH zzRaw-nSYsC|AGb=C9@{0UVBb83>Q}MHV|f@Xk&QEbyZ#i$%ul}w1#5SF=#|Lk6(|* z_*I*eWz4`Da*+5{Qi-h65peo<1hE>uit0%OAzHCsH?>9+0a(F3ZV;u6(`vDymT?mS zOR%B~gwB8rwC4b4cI7$V9`xUrXP|t;ZVXPt0z=Up(OCO=0uhtd-XrO4MKP=Rsf$%R zkHO^ke8dvUC}59>OVo%J$qdDaHr`?)hoQ5f%neXzH}}qNnfDp{1?tU_It+2b9cW3+ z<{q^(t+-;3G3&+*zYY5mcU?6!tqnD;BQU%DJ+jN-?JeoZyX4ulZV4m3lq0)oKs5bR z6!-S|_{jSZv)3@1cQffzA~Q9nt-cFlyf9XFwL2ii_g5DY{r^Y)=e2B7+TyA)t7~BA zkh&1jWU*^qknfOY^+oi2s2xWDZ}Umr1N`q}esvrf{&txMtOq{peI&<@wWV@O(rHZ? p8+GNZDyQk|&tuL~Z_9IP!tT#&lE)@>4p7lHP|xyjC{M7W#9v0Bc}M^N literal 0 HcmV?d00001 diff --git a/apps/server-net/out.txt b/apps/server-net/out.txt new file mode 100644 index 0000000..25bad3a --- /dev/null +++ b/apps/server-net/out.txt @@ -0,0 +1,27 @@ + Determining projects to restore... + All projects are up-to-date for restore. +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Chats.UnitTests\Knot.Modules.Chats.UnitTests.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\Knot.Modules.Identity.UnitTests.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj] + Knot.Shared.Kernel -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\bin\Debug\net10.0\Knot.Shared.Kernel.dll +E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Domain\Story.cs(23,15): warning CS8618: Non-nullable property 'Type' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj] +E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(116,27): warning CS8600: Converting null literal or possible null value to non-nullable type. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj] +E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(142,47): warning CS8604: Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj] + Knot.Modules.Identity -> E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\bin\Debug\net10.0\Knot.Modules.Identity.dll + Knot.Shared.Infrastructure -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\bin\Debug\net10.0\Knot.Shared.Infrastructure.dll + Knot.Modules.Chats -> E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\bin\Debug\net10.0\Knot.Modules.Chats.dll + Knot.Modules.Identity.UnitTests -> E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\bin\Debug\net10.0\Knot.Modules.Identity.UnitTests.dll + Knot.Modules.Chats.UnitTests -> E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Chats.UnitTests\bin\Debug\net10.0\Knot.Modules.Chats.UnitTests.dll + +Build succeeded. + +E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Domain\Story.cs(23,15): warning CS8618: Non-nullable property 'Type' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj] +E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(116,27): warning CS8600: Converting null literal or possible null value to non-nullable type. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj] +E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Storage\S3FileStorageService.cs(142,47): warning CS8604: Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'. [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj] + 3 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:01.32 diff --git a/apps/server-net/out8.txt b/apps/server-net/out8.txt new file mode 100644 index 0000000..216d0bd --- /dev/null +++ b/apps/server-net/out8.txt @@ -0,0 +1,23 @@ + Determining projects to restore... + All projects are up-to-date for restore. +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Chats.UnitTests\Knot.Modules.Chats.UnitTests.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\Knot.Modules.Identity.UnitTests.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\Knot.Modules.Identity.csproj] +C:\Program Files\dotnet\sdk\10.0.100-preview.6.25358.103\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(335,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj] + Knot.Shared.Kernel -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Kernel\bin\Debug\net10.0\Knot.Shared.Kernel.dll + Knot.Modules.Identity -> E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\bin\Debug\net10.0\Knot.Modules.Identity.dll + Knot.Shared.Infrastructure -> E:\GIT\forkmessager\apps\server-net\src\Shared\Knot.Shared.Infrastructure\bin\Debug\net10.0\Knot.Shared.Infrastructure.dll + Knot.Modules.Identity.UnitTests -> E:\GIT\forkmessager\apps\server-net\tests\Knot.Modules.Identity.UnitTests\bin\Debug\net10.0\Knot.Modules.Identity.UnitTests.dll +E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(16,34): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj] +E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(18,101): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj] + +Build FAILED. + +E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(16,34): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj] +E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Infrastructure\Persistence\ChatsDbContext.cs(18,101): error CS0234: The type or namespace name 'Infrastructure' does not exist in the namespace 'Knot.Shared' (are you missing an assembly reference?) [E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\Knot.Modules.Chats.csproj] + 0 Warning(s) + 2 Error(s) + +Time Elapsed 00:00:00.89 diff --git a/apps/server-net/src/Host/Controllers/ChatsController.cs b/apps/server-net/src/Host/Controllers/ChatsController.cs index ce8fb15..03c3fad 100644 --- a/apps/server-net/src/Host/Controllers/ChatsController.cs +++ b/apps/server-net/src/Host/Controllers/ChatsController.cs @@ -120,6 +120,15 @@ public sealed class ChatsController : ControllerBase }); } + if (c.Type == ChatType.Personal && messagesList.Count == 0) + { + var currentMember = c.Members.FirstOrDefault(m => m.UserId == _userContext.UserId); + if (currentMember == null || currentMember.Role != ChatRole.Owner) + { + continue; // Skip returning this empty personal chat to the non-initiator + } + } + result.Add(new { id = c.Id, @@ -209,7 +218,8 @@ public sealed class ChatsController : ControllerBase public async Task LeaveOrDeleteChat(Guid id, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct) { var chat = await chatRepository.GetByIdAsync(id, ct); - if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound(); + if (chat == null) return Ok(new { success = true }); // Already physically deleted, idempotent + if (!chat.Members.Any(m => m.UserId == _userContext.UserId)) return Forbid(); if (chat.Type == ChatType.Group) { @@ -280,21 +290,16 @@ public sealed class ChatsController : ControllerBase } [HttpPost("{id:guid}/avatar")] - public async Task UploadGroupAvatar(Guid id, IFormFile avatar, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct) + public async Task UploadGroupAvatar(Guid id, IFormFile avatar, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct) { var chat = await chatRepository.GetByIdAsync(id, ct); if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound(); if (avatar == null || avatar.Length == 0) return BadRequest("No file"); - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars"); - if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); + using var stream = avatar.OpenReadStream(); + var fileId = await fileStorage.UploadFileAsync(stream, avatar.FileName, avatar.ContentType); + var url = $"/api/files/{fileId}"; - var fileName = $"{Guid.NewGuid()}{Path.GetExtension(avatar.FileName)}"; - var filePath = Path.Combine(uploadsPath, fileName); - using (var stream = new FileStream(filePath, FileMode.Create)) - await avatar.CopyToAsync(stream, ct); - - var url = $"/uploads/avatars/{fileName}"; chat.UpdateAvatar(url); chatRepository.Update(chat); await uow.SaveChangesAsync(ct); @@ -303,17 +308,13 @@ public sealed class ChatsController : ControllerBase } [HttpPost("{id:guid}/avatar/crop")] - public async Task CropGroupAvatar(Guid id, [FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct) + public async Task CropGroupAvatar(Guid id, [FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct) { var chat = await chatRepository.GetByIdAsync(id, ct); if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound(); if (avatar == null || avatar.Length == 0) return BadRequest("No file"); - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars"); - if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); - - var fileName = $"{Guid.NewGuid()}.jpg"; - var filePath = Path.Combine(uploadsPath, fileName); + string url; try { @@ -327,7 +328,14 @@ public sealed class ChatsController : ControllerBase image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight))); image.Mutate(ctx => ctx.Resize(400, 400)); - await image.SaveAsJpegAsync(filePath, ct); + + using var outStream = new MemoryStream(); + await image.SaveAsJpegAsync(outStream, ct); + outStream.Position = 0; + + var fileName = avatar.FileName ?? "avatar.jpg"; + var fileId = await fileStorage.UploadFileAsync(outStream, fileName, "image/jpeg"); + url = $"/api/files/{fileId}"; } } catch (Exception ex) @@ -335,7 +343,6 @@ public sealed class ChatsController : ControllerBase return StatusCode(500, "Error processing image: " + ex.Message); } - var url = $"/uploads/avatars/{fileName}"; chat.UpdateAvatar(url); chatRepository.Update(chat); await uow.SaveChangesAsync(ct); diff --git a/apps/server-net/src/Host/Controllers/FilesController.cs b/apps/server-net/src/Host/Controllers/FilesController.cs new file mode 100644 index 0000000..46960ea --- /dev/null +++ b/apps/server-net/src/Host/Controllers/FilesController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Knot.Shared.Kernel.Storage; + +namespace Host.Controllers; + +[ApiController] +[Route("api/files")] +public sealed class FilesController : ControllerBase +{ + private readonly IFileStorageService _fileStorage; + + public FilesController(IFileStorageService fileStorage) + { + _fileStorage = fileStorage; + } + + [HttpGet("{id}")] + [AllowAnonymous] + public async Task DownloadFile(string id) + { + try + { + var result = await _fileStorage.DownloadFileAsync(id); + // Обратите внимание, что мы возвращаем поток с автоматическим освобождением памяти. + return File(result.Stream, result.ContentType, result.FileName); + } + catch (Exception ex) + { + return NotFound(new { error = "Файл не найден или ошибка доступа.", message = ex.Message }); + } + } +} diff --git a/apps/server-net/src/Host/Controllers/MessagesController.cs b/apps/server-net/src/Host/Controllers/MessagesController.cs index 829ec40..3a5a02e 100644 --- a/apps/server-net/src/Host/Controllers/MessagesController.cs +++ b/apps/server-net/src/Host/Controllers/MessagesController.cs @@ -187,22 +187,14 @@ public sealed class MessagesController : ControllerBase } [HttpPost("upload")] - public async Task UploadFile(IFormFile file) + public async Task UploadFile(IFormFile file, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage) { if (file == null || file.Length == 0) return BadRequest("No file uploaded"); - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads"); - if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); - - var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; - var filePath = Path.Combine(uploadsPath, fileName); - - using (var stream = new FileStream(filePath, FileMode.Create)) - { - await file.CopyToAsync(stream); - } + using var stream = file.OpenReadStream(); + var fileId = await fileStorage.UploadFileAsync(stream, file.FileName, file.ContentType); - return Ok(new { url = $"/uploads/{fileName}", filename = file.FileName, size = file.Length }); + return Ok(new { url = $"/api/files/{fileId}", filename = file.FileName, size = file.Length }); } [HttpGet("chat/{chatId:guid}/shared")] diff --git a/apps/server-net/src/Host/Controllers/UsersController.cs b/apps/server-net/src/Host/Controllers/UsersController.cs index 69b7cf7..c1589e5 100644 --- a/apps/server-net/src/Host/Controllers/UsersController.cs +++ b/apps/server-net/src/Host/Controllers/UsersController.cs @@ -64,45 +64,19 @@ public sealed class UsersController : ControllerBase } [HttpPost("avatar")] - public async Task UploadAvatar(IFormFile avatar, CancellationToken ct) + public async Task UploadAvatar(IFormFile avatar, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct) { var user = await _userRepository.GetByIdAsync(_userContext.UserId, ct); if (user == null) return NotFound(); string? avatarUrl = null; + var fileToUpload = avatar != null && avatar.Length > 0 ? avatar : Request.Form.Files.Count > 0 ? Request.Form.Files[0] : null; - if (avatar != null && avatar.Length > 0) + if (fileToUpload != null) { - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars"); - if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); - - var ext = Path.GetExtension(avatar.FileName); - var fileName = $"{Guid.NewGuid()}{ext}"; - var filePath = Path.Combine(uploadsPath, fileName); - - using (var stream = new FileStream(filePath, FileMode.Create)) - { - await avatar.CopyToAsync(stream, ct); - } - - avatarUrl = $"/uploads/avatars/{fileName}"; - } - else if (Request.Form.Files.Count > 0) - { - var file = Request.Form.Files[0]; - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars"); - if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); - - var ext = Path.GetExtension(file.FileName); - var fileName = $"{Guid.NewGuid()}{ext}"; - var filePath = Path.Combine(uploadsPath, fileName); - - using (var stream = new FileStream(filePath, FileMode.Create)) - { - await file.CopyToAsync(stream, ct); - } - - avatarUrl = $"/uploads/avatars/{fileName}"; + using var stream = fileToUpload.OpenReadStream(); + var fileId = await fileStorage.UploadFileAsync(stream, fileToUpload.FileName, fileToUpload.ContentType); + avatarUrl = $"/api/files/{fileId}"; } else { @@ -124,26 +98,20 @@ public sealed class UsersController : ControllerBase } [HttpPost("avatar/crop")] - public async Task CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct) + public async Task CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, [FromServices] Knot.Shared.Kernel.Storage.IFileStorageService fileStorage, CancellationToken ct) { var user = await _userRepository.GetByIdAsync(_userContext.UserId, ct); if (user == null) return NotFound(); if (avatar == null || avatar.Length == 0) return BadRequest("No file"); - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars"); - if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); - - var ext = ".jpg"; - var fileName = $"{Guid.NewGuid()}{ext}"; - var filePath = Path.Combine(uploadsPath, fileName); + string avatarUrl; try { using (var inputStream = avatar.OpenReadStream()) using (var image = await SixLabors.ImageSharp.Image.LoadAsync(inputStream)) { - // Clamp coordinates to image bounds int startX = Math.Max(0, Math.Min(x, image.Width - 1)); int startY = Math.Max(0, Math.Min(y, image.Height - 1)); int rectWidth = Math.Max(1, Math.Min(width, image.Width - startX)); @@ -151,8 +119,14 @@ public sealed class UsersController : ControllerBase image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight))); image.Mutate(ctx => ctx.Resize(400, 400)); + + using var outStream = new MemoryStream(); + await image.SaveAsJpegAsync(outStream, ct); + outStream.Position = 0; - await image.SaveAsJpegAsync(filePath, ct); + var fileName = avatar.FileName ?? "avatar.jpg"; + var id = await fileStorage.UploadFileAsync(outStream, fileName, "image/jpeg"); + avatarUrl = $"/api/files/{id}"; } } catch (Exception ex) @@ -160,7 +134,6 @@ public sealed class UsersController : ControllerBase return StatusCode(500, "Error processing image: " + ex.Message); } - var avatarUrl = $"/uploads/avatars/{fileName}"; user.UpdateAvatar(avatarUrl); await _unitOfWork.SaveChangesAsync(ct); diff --git a/apps/server-net/src/Host/Program.cs b/apps/server-net/src/Host/Program.cs index f7e2b2f..78db633 100644 --- a/apps/server-net/src/Host/Program.cs +++ b/apps/server-net/src/Host/Program.cs @@ -31,7 +31,12 @@ var envMappings = new Dictionary ["Jwt:Audience"] = builder.Configuration["JWT_AUDIENCE"], ["WebRtc:TurnUrl"] = builder.Configuration["TURN_URL"], ["WebRtc:TurnUsername"] = builder.Configuration["TURN_USERNAME"], - ["WebRtc:TurnPassword"] = builder.Configuration["TURN_PASSWORD"] + ["WebRtc:TurnPassword"] = builder.Configuration["TURN_PASSWORD"], + ["KNOT_MASTER_ENCRYPTION_KEY"] = builder.Configuration["KNOT_MASTER_ENCRYPTION_KEY"], + ["S3_ENDPOINT"] = builder.Configuration["S3_ENDPOINT"], + ["S3_ACCESS_KEY"] = builder.Configuration["S3_ACCESS_KEY"], + ["S3_SECRET_KEY"] = builder.Configuration["S3_SECRET_KEY"], + ["S3_BUCKET"] = builder.Configuration["S3_BUCKET"] }; // Добавляем только те, что реально заданы в ENV @@ -41,7 +46,7 @@ builder.Configuration.AddInMemoryCollection( builder.Services.AddIdentityModule(builder.Configuration); builder.Services.AddChatsModule(builder.Configuration); -builder.Services.AddSharedInfrastructure(); +builder.Services.AddSharedInfrastructure(builder.Configuration); // Настройка CORS builder.Services.AddCors(options => @@ -147,17 +152,7 @@ if (app.Environment.IsDevelopment()) // app.UseHttpsRedirection(); -var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads"); -if (!Directory.Exists(uploadsPath)) -{ - Directory.CreateDirectory(uploadsPath); -} - -app.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new PhysicalFileProvider(uploadsPath), - RequestPath = "/uploads" -}); +// Не раздаем статические файлы, так как теперь используем MinIO app.UseAuthentication(); app.UseAuthorization(); diff --git a/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs b/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs index 6e0430e..e4526f8 100644 --- a/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs +++ b/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs @@ -31,7 +31,7 @@ public sealed class CreateChatCommandHandler : ICommandHandler options, IMediator mediator) - + public ChatsDbContext(DbContextOptions options, IMediator mediator, Knot.Shared.Kernel.Security.IEncryptionService encryptionService) : base(options) - { _mediator = mediator; + _encryptionService = encryptionService; } public DbSet Chats => Set(); @@ -59,6 +59,12 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork builder.ToTable("Messages"); builder.HasKey(m => m.Id); + builder.Property(m => m.Content) + .HasConversion( + v => v == null ? null : _encryptionService.EncryptMessage(v), + v => v == null ? null : _encryptionService.DecryptMessage(v) + ); + builder.OwnsMany(m => m.Media, mb => { diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.Designer.cs index f84f4a4..23743a0 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.cs index 5537f8c..a4f70f4 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.Designer.cs index 9afec5d..1a65891 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.cs index 8bbf309..0897202 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.Designer.cs index 7c6af95..fc681e9 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.cs index 90616d5..24c7894 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.Designer.cs index d08d3ff..880b952 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.cs index 50e7319..409f229 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.Designer.cs index d02310a..67f4e52 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.cs index faeb196..59c4da4 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.Designer.cs index b7486bf..92d4049 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.cs index 3d89355..df87a35 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.Designer.cs index e141b14..f521fb8 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.cs index 91f7a32..8c657f5 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.Designer.cs index 17bbe5d..9e9d74f 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.cs index 99a8e40..7aec783 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs index cb6793f..06969ac 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs index 673299c..3d32323 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs index 9fdbddf..1c2ee7b 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Identity/Class1.cs b/apps/server-net/src/Modules/Identity/Class1.cs index 7362c82..a8baf85 100644 --- a/apps/server-net/src/Modules/Identity/Class1.cs +++ b/apps/server-net/src/Modules/Identity/Class1.cs @@ -1,4 +1,4 @@ -namespace Knot.Modules.Identity; +namespace Knot.Modules.Identity; public class Class1 { diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/IdentityDbContext.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/IdentityDbContext.cs index abbac8d..b7ade0f 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/IdentityDbContext.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/IdentityDbContext.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Knot.Modules.Identity.Application.Abstractions; using Knot.Modules.Identity.Domain; using Knot.Shared.Kernel; +using Knot.Shared.Kernel.Security; namespace Knot.Modules.Identity.Infrastructure.Persistence; @@ -14,11 +15,13 @@ namespace Knot.Modules.Identity.Infrastructure.Persistence; public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork { private readonly IMediator _mediator; + private readonly IEncryptionService _encryptionService; - public IdentityDbContext(DbContextOptions options, IMediator mediator) + public IdentityDbContext(DbContextOptions options, IMediator mediator, IEncryptionService encryptionService) : base(options) { _mediator = mediator; + _encryptionService = encryptionService; } public DbSet Users => Set(); @@ -52,6 +55,12 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork builder.HasKey(s => s.Id); builder.Property(s => s.Type).IsRequired(); + builder.Property(s => s.Content) + .HasConversion( + v => v == null ? null : _encryptionService.EncryptMessage(v), + v => v == null ? null : _encryptionService.DecryptMessage(v) + ); + // Configure backing fields for collections builder.Metadata.FindNavigation(nameof(Story.Viewers))?.SetPropertyAccessMode(PropertyAccessMode.Field); @@ -92,7 +101,14 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork { builder.ToTable("StoryReplies"); builder.HasKey(r => r.Id); - builder.Property(r => r.Content).IsRequired().HasMaxLength(500); + + builder.Property(r => r.Content) + .IsRequired() + .HasMaxLength(500) + .HasConversion( + v => v == null ? null : _encryptionService.EncryptMessage(v), + v => v == null ? null : _encryptionService.DecryptMessage(v) + ); }); modelBuilder.Entity(builder => diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.Designer.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.Designer.cs index a44dcbd..ac05526 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.Designer.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.cs index 6ad3748..7086701 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311180816_InitialIdentity.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.Designer.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.Designer.cs index 3730c3b..dcb0491 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.Designer.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.cs index f7a178c..8c448a7 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311200839_UpdateIdentityWithNewFieldsAndTables.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.Designer.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.Designer.cs index dc4e7f5..1022027 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.Designer.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.cs index 3df6ef5..64f0280 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260311215347_AddHideStoryViewsToUser.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.Designer.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.Designer.cs index 2d03a73..99d5445 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.Designer.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.cs index b719aef..32d71ef 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260312174453_AddHideStoryViewsAndFixStoryViewer.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.Designer.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.Designer.cs index afbf1d7..da3c592 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.Designer.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.cs index 169028c..8069b35 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/20260313115319_AddStoryReactionsReplies.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs index 486ef99..f1e727c 100644 --- a/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs +++ b/apps/server-net/src/Modules/Identity/Infrastructure/Persistence/Migrations/IdentityDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Class1.cs b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Class1.cs index 5d6448e..8f00c44 100644 --- a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Class1.cs +++ b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Class1.cs @@ -1,4 +1,4 @@ -namespace Knot.Shared.Infrastructure; +namespace Knot.Shared.Infrastructure; public class Class1 { diff --git a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs index 96cd790..30110d0 100644 --- a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs +++ b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/DependencyInjection.cs @@ -1,15 +1,45 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; using Knot.Shared.Kernel; +using Knot.Shared.Kernel.Security; +using Knot.Shared.Kernel.Storage; +using Minio; +using System; namespace Knot.Shared.Infrastructure; public static class DependencyInjection { - public static IServiceCollection AddSharedInfrastructure(this IServiceCollection services) + public static IServiceCollection AddSharedInfrastructure(this IServiceCollection services, IConfiguration configuration) { services.AddHttpContextAccessor(); services.AddScoped(); + + // Настройка шифрования + var masterKey = configuration["KNOT_MASTER_ENCRYPTION_KEY"] + ?? throw new ArgumentNullException("KNOT_MASTER_ENCRYPTION_KEY is missing in env."); + services.AddSingleton(new AesEncryptionService(masterKey)); + + // Настройка MinIO (S3) + var s3Endpoint = configuration["S3_ENDPOINT"] ?? "minio:9000"; + var s3AccessKey = configuration["S3_ACCESS_KEY"] ?? "admin"; + var s3SecretKey = configuration["S3_SECRET_KEY"] ?? "KnotSuperSecretMinioPassword"; + var s3Bucket = configuration["S3_BUCKET"] ?? "knot-uploads"; + + services.AddMinio(configureClient => configureClient + .WithEndpoint(s3Endpoint) + .WithCredentials(s3AccessKey, s3SecretKey) + .WithSSL(false) + .Build()); + + services.AddScoped(provider => + { + var minioClient = provider.GetRequiredService(); + var encService = provider.GetRequiredService(); + return new S3FileStorageService(minioClient, encService, s3Bucket); + }); + return services; } } diff --git a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Knot.Shared.Infrastructure.csproj b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Knot.Shared.Infrastructure.csproj index 8d40ccb..6dd0df2 100644 --- a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Knot.Shared.Infrastructure.csproj +++ b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Knot.Shared.Infrastructure.csproj @@ -12,6 +12,7 @@ + diff --git a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Security/AesEncryptionService.cs b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Security/AesEncryptionService.cs new file mode 100644 index 0000000..9545213 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Security/AesEncryptionService.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Knot.Shared.Kernel.Security; + +public class AesEncryptionService : IEncryptionService +{ + private readonly byte[] _key; + + public AesEncryptionService(string masterKeyBase64) + { + // Try decoding as base64 or just using UTF8 string + if (masterKeyBase64.Length == 32) + { + _key = Encoding.UTF8.GetBytes(masterKeyBase64); + } + else + { + try + { + _key = Convert.FromBase64String(masterKeyBase64); + } + catch + { + _key = Encoding.UTF8.GetBytes(masterKeyBase64.PadRight(32, '0')[..32]); + } + } + + if (_key.Length != 32) + throw new ArgumentException("Мастер-ключ должен быть ровно 32 байта для AES-256."); + } + + // Сообщения: AES-256-GCM + public string EncryptMessage(string plainText) + { + if (string.IsNullOrEmpty(plainText)) return plainText; + + var nonce = new byte[12]; + RandomNumberGenerator.Fill(nonce); + + var plainBytes = Encoding.UTF8.GetBytes(plainText); + var cipherBytes = new byte[plainBytes.Length]; + var tag = new byte[16]; + + using var aesGcm = new AesGcm(_key, 16); + aesGcm.Encrypt(nonce, plainBytes, cipherBytes, tag); + + // формат: nonce:tag:cipherText + return $"{Convert.ToBase64String(nonce)}:{Convert.ToBase64String(tag)}:{Convert.ToBase64String(cipherBytes)}"; + } + + public string DecryptMessage(string cipherText) + { + if (string.IsNullOrEmpty(cipherText)) return cipherText; + + var parts = cipherText.Split(':'); + if (parts.Length != 3) return cipherText; + + try + { + var nonce = Convert.FromBase64String(parts[0]); + var tag = Convert.FromBase64String(parts[1]); + var cipherBytes = Convert.FromBase64String(parts[2]); + var plainBytes = new byte[cipherBytes.Length]; + + using var aesGcm = new AesGcm(_key, 16); + aesGcm.Decrypt(nonce, cipherBytes, tag, plainBytes); + + return Encoding.UTF8.GetString(plainBytes); + } + catch + { + return cipherText; // Возвращаем оригинал при ошибке расшифровки (например, старые не зашифрованные сообщения) + } + } + + // Потоковые файлы: AES CBC через CryptoStream + // (Примечание: Для GCM через CryptoStream нет поддержки "из коробки" в .NET при использовании Aes.Create(). + // Требуется CBC+HMAC/CTR или механизм AEAD блоками. Здесь предоставляется стандартный AES-CBC для соответствия требованиям CryptoStream). + public Stream CreateEncryptionStream(Stream unencryptedOutputStream, out byte[] iv) + { + var aes = Aes.Create(); + aes.KeySize = 256; + aes.Key = _key; + aes.Mode = CipherMode.CBC; + aes.GenerateIV(); + iv = aes.IV; + + var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); + return new CryptoStream(unencryptedOutputStream, encryptor, CryptoStreamMode.Write); + } + + public Stream CreateDecryptionStream(Stream encryptedInputStream, byte[] iv) + { + var aes = Aes.Create(); + aes.KeySize = 256; + aes.Key = _key; + aes.Mode = CipherMode.CBC; + aes.IV = iv; + + var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); + return new CryptoStream(encryptedInputStream, decryptor, CryptoStreamMode.Read); + } +} diff --git a/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Storage/S3FileStorageService.cs b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Storage/S3FileStorageService.cs new file mode 100644 index 0000000..6a07548 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Infrastructure/Storage/S3FileStorageService.cs @@ -0,0 +1,152 @@ +using Minio; +using Minio.DataModel.Args; +using Minio.Exceptions; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using Knot.Shared.Kernel.Security; + +namespace Knot.Shared.Kernel.Storage; + +public class S3FileStorageService : IFileStorageService +{ + private readonly IMinioClient _minioClient; + private readonly IEncryptionService _encryptionService; + private readonly string _bucketName; + + public S3FileStorageService(IMinioClient minioClient, IEncryptionService encryptionService, string bucketName) + { + _minioClient = minioClient; + _encryptionService = encryptionService; + _bucketName = bucketName; + } + + private async Task EnsureBucketExistsAsync() + { + try + { + var bktExistArgs = new BucketExistsArgs().WithBucket(_bucketName); + bool found = await _minioClient.BucketExistsAsync(bktExistArgs).ConfigureAwait(false); + if (!found) + { + var mkBktArgs = new MakeBucketArgs().WithBucket(_bucketName); + await _minioClient.MakeBucketAsync(mkBktArgs).ConfigureAwait(false); + } + } + catch (MinioException e) + { + Console.WriteLine($"[Bucket] Minio Error: {e.Message}"); + } + } + + public async Task UploadFileAsync(Stream fileStream, string fileName, string contentType) + { + await EnsureBucketExistsAsync(); + + // 1. Вычисляем SHA-256 для Content-Addressable Storage (CAS) - дедупликация + string fileHash; + using (var sha256 = SHA256.Create()) + using (var msHash = new MemoryStream()) + { + var startPos = fileStream.Position; + await fileStream.CopyToAsync(msHash); + var hashBytes = sha256.ComputeHash(msHash.ToArray()); + fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + // Возвращаем указатель потока в начало для последующего чтения + fileStream.Position = startPos; + } + + var ext = Path.GetExtension(fileName); + var fileId = $"{fileHash}{ext}"; + + // 2. Шифруем файл "на лету" во временный файл + // Для больших файлов мы используем временный файл, чтобы не перегружать оперативную память (RAM) + var tempFilePath = Path.GetTempFileName(); + byte[] ivParams; + + try + { + using (var tempFs = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write)) + using (var cryptoStream = _encryptionService.CreateEncryptionStream(tempFs, out ivParams)) + { + await fileStream.CopyToAsync(cryptoStream); + } + + // 3. Загружаем зашифрованный файл в MinIO + using var fileToUpload = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read); + var metadata = new System.Collections.Generic.Dictionary + { + { "ContentType", contentType }, + { "OriginalFileName", fileName }, + { "IV", Convert.ToBase64String(ivParams) }, + { "KeyVersion", "1" }, + { "EncryptionAlgorithm", "AES-256-CBC" } // Как реализовано в потоке AES CBC + }; + + var putObjectArgs = new PutObjectArgs() + .WithBucket(_bucketName) + .WithObject(fileId) + .WithStreamData(fileToUpload) + .WithObjectSize(fileToUpload.Length) + .WithContentType("application/octet-stream") + .WithHeaders(metadata); + + await _minioClient.PutObjectAsync(putObjectArgs).ConfigureAwait(false); + + return fileId; + } + finally + { + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + + public async Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId) + { + // Для скачивания, мы транслируем из Minio во временный файл, расшифровываем и возвращаем. + var tempFilePath = Path.GetTempFileName(); + string contentType = "application/octet-stream"; + string fileName = fileId; + string ivBase64 = null; + + var statArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(fileId); + var stat = await _minioClient.StatObjectAsync(statArgs).ConfigureAwait(false); + + if (stat.MetaData.ContainsKey("Contenttype")) contentType = stat.MetaData["Contenttype"]; + if (stat.MetaData.ContainsKey("Originalfilename")) fileName = stat.MetaData["Originalfilename"]; + if (stat.MetaData.ContainsKey("Iv")) ivBase64 = stat.MetaData["Iv"]; + + // Загружаем во временный файл (так как CryptoStream требует правильного чтения/записи) + var getObjArgs = new GetObjectArgs() + .WithBucket(_bucketName) + .WithObject(fileId) + .WithCallbackStream((stream) => + { + using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write); + stream.CopyTo(fs); + }); + + await _minioClient.GetObjectAsync(getObjArgs).ConfigureAwait(false); + + // Создаем поток дешифрования над байтами временного файла, оборачиваем в MemoryStream, чтобы он автоматически закрывался + // В реальном масштабируемом продакшене лучше возвращать FileStream напрямую, обернутый в crypto stream. + var msResult = new MemoryStream(); + using (var fsRead = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read)) + { + var iv = Convert.FromBase64String(ivBase64); + using var cryptoStream = _encryptionService.CreateDecryptionStream(fsRead, iv); + await cryptoStream.CopyToAsync(msResult); + } + + File.Delete(tempFilePath); + msResult.Position = 0; + + return (msResult, contentType, fileName); + } +} diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Class1.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Class1.cs index 4524915..bb20771 100644 --- a/apps/server-net/src/Shared/Knot.Shared.Kernel/Class1.cs +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Class1.cs @@ -1,4 +1,4 @@ -namespace Knot.Shared.Kernel; +namespace Knot.Shared.Kernel; public class Class1 { diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Security/IEncryptionService.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Security/IEncryptionService.cs new file mode 100644 index 0000000..b705164 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Security/IEncryptionService.cs @@ -0,0 +1,16 @@ +using System.IO; + +namespace Knot.Shared.Kernel.Security; + +public interface IEncryptionService +{ + // Шифрует текст (например, сообщения) для хранения в БД + string EncryptMessage(string plainText); + string DecryptMessage(string cipherText); + + // Создает поток шифрования для потоковой передачи файлов "на лету" + Stream CreateEncryptionStream(Stream unencryptedOutputStream, out byte[] iv); + + // Создает поток дешифрования для чтения файлов "на лету" + Stream CreateDecryptionStream(Stream encryptedInputStream, byte[] iv); +} diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs new file mode 100644 index 0000000..5b7afd6 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Storage/IFileStorageService.cs @@ -0,0 +1,13 @@ +using System.IO; +using System.Threading.Tasks; + +namespace Knot.Shared.Kernel.Storage; + +public interface IFileStorageService +{ + // Загружает поток файла и возвращает его уникальный идентификатор (SHA256 хеш или GUID). + Task UploadFileAsync(Stream stream, string fileName, string contentType); + + // Скачивает файл и возвращает его расшифрованный поток и тип содержимого. + Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId); +} diff --git a/apps/web/src/components/UserProfile.tsx b/apps/web/src/components/UserProfile.tsx index 6bf94de..273ce81 100644 --- a/apps/web/src/components/UserProfile.tsx +++ b/apps/web/src/components/UserProfile.tsx @@ -367,9 +367,9 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is onClick={onClose} /> diff --git a/apps/web/src/lib/i18n.ts b/apps/web/src/lib/i18n.ts index 29e4b91..7f50cae 100644 --- a/apps/web/src/lib/i18n.ts +++ b/apps/web/src/lib/i18n.ts @@ -244,12 +244,12 @@ const translations = { // Misc error: 'Ошибка', // Friends - friends: 'Друзья', - friendRequests: 'Заявки в друзья', - friendsList: 'Список друзей', - noFriends: 'Пока нет друзей', - addFriend: 'Добавить в друзья', - removeFriend: 'Удалить из друзей', + friends: 'Контакты', + friendRequests: 'Заявки в контакты', + friendsList: 'Список контактов', + noFriends: 'Пока нет контактов', + addFriend: 'Добавить в контакты', + removeFriend: 'Удалить из контактов', requestSent: 'Заявка отправлена', searchFriends: 'Поиск по @username (мин. 3 символа)', noSearchResults: 'Пользователи не найдены', @@ -502,12 +502,12 @@ const translations = { searchGifs: 'Search GIFs...', trending: 'Trending', error: 'Error', - friends: 'Friends', - friendRequests: 'Friend requests', - friendsList: 'Friends list', - noFriends: 'No friends yet', - addFriend: 'Add friend', - removeFriend: 'Remove friend', + friends: 'Contacts', + friendRequests: 'Contact requests', + friendsList: 'Contacts list', + noFriends: 'No contacts yet', + addFriend: 'Add to contacts', + removeFriend: 'Remove from contacts', requestSent: 'Request sent', searchFriends: 'Search by @username (min. 3 chars)', noSearchResults: 'No users found', diff --git a/apps/web/src/lib/socket.ts b/apps/web/src/lib/socket.ts index 41c2bf9..287f832 100644 --- a/apps/web/src/lib/socket.ts +++ b/apps/web/src/lib/socket.ts @@ -60,21 +60,16 @@ export function connectSocket(token: string): SocketCompat { connection.start() .then(() => { - console.log('[SignalR] Connected successfully'); - console.log('[SignalR] Connection ID:', connection?.connectionId); }) .catch(err => console.error('[SignalR] Connection error:', err.toString())); connection.onclose((error) => { - console.log('[SignalR] Connection closed', error); }); connection.onreconnecting((error) => { - console.log('[SignalR] Reconnecting...', error); }); connection.onreconnected((connectionId) => { - console.log('[SignalR] Reconnected, new connection ID:', connectionId); }); return socketWrapper; diff --git a/diff.txt b/diff.txt new file mode 100644 index 0000000000000000000000000000000000000000..bb0a8b4fdb06128bc07c62f2a8bbafbf7d199104 GIT binary patch literal 12952 zcmdU#ZEqXL5yuyXUll0ercGNQMME2ALpL^+8bQ*!sbg7EYNxR+OLF|eF#?IC9uS#Q zNlLM5_(P=rzHR@%9gc4IczlwT0s(P6@@{u#cAjT;|NEbI*bCeG)WQvYcJyzc-eIs;rAH=7<(9->0IM4{5tZLMOVDvQiMQG}6U$D0H=6qoNJp4@b zbwgVeycFc^@Tq>?6o#An-PETOJ_&z`?{Df}3x5h9>GMhWSRZDy#vA9lKA?UWKj*#D zJK>Hfy`eQ+*Y#;!&xcygbzL>RyE9zTk+=+3cJ%Zgx*tdu&9EMJ#r2*z($>HGdcGay zf~0!kNU|B|oA-a62aY|#dM3zInVl9(Knfg zX-dQPwaPtdY(a~8^Yq*Ft312Vk9&ea>|l*Ot#?`XZ9T)c4WftK5-*P8m;&R5^yg3` zA86#EX7R=}xDl?Ma>nR5)|j5W@Rp#!OXFWH{7uiEYSxx4gqXUox4CUBX&zAVB6qXd zjRQ_UnVy8tG>XV+Gp=h?`v3EAdx+=N@LtmhKF`9H=pnliI#Jgw zZasn454VI9>2FF$HU+t(S~(xR#}38@lf5TG#r&4NnvY zzl!j#SI}lwb1QlkVg?@lthm3FmTw3nc$b6~?gMKQ-uqf@U+cgo-xlwEVP4mISA=^S zUSbuwFZw{#ep}p&!n-UlvLdZp()T$0<_Ffq<(8zr5hd8w+@8F`S!vjaGJZH!*5h!a zZ$0tBa)R@q0u5LbCl+OC^YUUfT0no8o3zh?E345K)}@_~MAdSHXkOuU$%DK1yDC3| zFZ#DGeB2vdPvw{1*K<7A^XLWN8II!3@AUhz-aI>fvsuSWwClea`b2w!R|W!5eI9<0 zx4ZSPhi}ZEx_9IaJ`dgcdT7J5dj;~cwm#GV4#rvLC@IvvAy20i(wH`8c?EjiRCKdOc)pyJJ zs(f@#3v@MA5p{$ONu+ezyBxP?IYvX7zvLCvz(j7Vgq*8+J(SilLI_8-zQFL5% z&PTeXJG1o+`Vi?v%=2l{ClJ^ z<;*;5wrbRCx8KH^m8z8b)#^LThrEX6*?gQ>(^~0C!G|5KH~07Ezuj`no;x4gR1JlW z!Z&Jna%Rigy?V61YSBY;9hna2am;_n$?~euykHsbsGiNo6RRJA>iHi%o>43DXz1N4 zG}80L$x+rDXa_!&dJ&m-)e*Zd4~WI|^s7D6H>T!V zlJ3$cBOjSG_I4JFOlRhaB-VMNPOd3>cm%N?qeqi={a3@NVs*yZqspXxjz+~kM_x~u z<&WPfS(iOgU9&$^&VJlyjP9kJr}Y&*bE}Zq2`IXZ=(q15xi2$s>wawh`MH;v<}Lhv zPF+V@So9L}*jesCIt0I$A(gv>ZwKuw#i#9PH|$|_WEb>xJ+9A7Te?d3*%tq-*Kt

v)dh8)`@P_OHP_EZMH$vCl%$l8=d5cvyH+ovJdq^*0pe7; zb7k@<_tbpd{4PurBo+)#($&-z84&EDyWzUe;A(^+Cyz-40;7G3_!F^8zO zzH)E0Om8_KT2Xh5iiLW^^v5|h>aEhT)!4^n3Qol%VWP^z;;EZD>g>Y(j<^RM?I8_t z6q;<`7rML`QKqm^jcS0kOOuDNGW)-OuW zFr>W%lZR`xogp*cmhs$`yluyQbiU`23$edoeT8BNI^WZ=js$z%7ve6o^{Q|kVBWjLH-LW`F$f<$WkM#XmyeRf$&ccP9#;#^joqO-1tZ+ zM?3beF*a)+n!LV6ifghT&gzlHaOQ=xdsTGYlQg#Ew>`?)e|%_~b1z6YEY_wrn8RBW zq^{-_Hfrw=BL+JkSrZ)2eNtD=-bu85&dHrg!&SC`95?>JPy4a+Y880e!)$1$5`LI; zu#mz(y2QNdY{i(6k16-|%JjHr$CubyeKPziNFHT-+9S4eyzKvBn?$Zba>fQpaoTPx lRz~b8v$nT|IeJ=_9;fZO(3LB4J9S3#TUM*BN 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