Compare commits

...

49 Commits

Author SHA1 Message Date
fcb195ddb3 Fixed error on whiteboard view and login redirection 2026-03-10 21:52:40 +01:00
6a3fd249a4 Error handling on signup, login and home view 2026-03-10 20:35:52 +01:00
f4b986c79e Merge pull request #51 from StewKI/bugfixes
Bugfixes
2026-03-10 15:15:21 +01:00
6efa4e8465 SignalR now also tries to refresh if first connection results in unauthorized response 2026-03-10 14:52:35 +01:00
8fa0de6094 Canceling join request now actually changes the status to cancelled so trying to join same RequestToJoin whiteboard with code manually doesnt fail due to status being Pending 2026-03-09 23:06:27 +01:00
10b550a59a Code only displayed for owner and is not lost on refresh 2026-03-09 21:24:01 +01:00
b10292b880 Active users copied to last valid state upon invalidation 2026-03-09 21:13:21 +01:00
e94c54b2ad title 2026-03-08 19:57:02 +01:00
a1ff1fc487 aspnet image for worker 2026-03-08 19:14:42 +01:00
cceed90772 vue tsc build only 2026-03-08 19:10:56 +01:00
11f7176bb6 Merge pull request #50 from StewKI/bugfix-whiteboard-joining-flow
fixes
2026-03-08 18:16:39 +01:00
ebfcfbff2c Merge branch 'main' into bugfix-whiteboard-joining-flow 2026-03-08 16:56:10 +01:00
Andrija Stevanović
6edff864fe Merge pull request #49 from StewKI/feature-deploy
Deployment
2026-03-08 16:53:06 +01:00
18e7287a17 deploy adaptation for vps 2026-03-08 16:51:43 +01:00
3c50565d5e fixes 2026-03-08 16:24:55 +01:00
4ccb6303f3 windows start scripts 2026-03-08 16:24:38 +01:00
9030d1a8b0 deploy 2026-03-08 16:11:23 +01:00
7ae824b6fd Merge pull request #48 from StewKI/feature-updated-whiteboard-creation-logic
Now uses policy and max participants for creating whiteboards
2026-03-08 15:45:36 +01:00
2bc17e8cf4 Now uses policy and max participants for creating whiteboards 2026-03-08 15:42:20 +01:00
Andrija Stevanović
897a46aa7b Merge pull request #47 from StewKI/feature-front-polish
Frontend UI polish
2026-03-08 02:41:45 +01:00
56d6b262ff present ui position fix 2026-03-08 02:35:42 +01:00
3eccedfad4 ui polish 2026-03-08 02:29:39 +01:00
8b1a96f133 scipts for starting 2026-03-08 02:29:33 +01:00
4f64d998ef Merge pull request #45 from StewKI/feature-join-whiteboard-by-code
Feature join whiteboard by code
2026-03-08 00:31:22 +01:00
8aba087998 even more fix 2026-03-08 00:30:36 +01:00
9a3143d2f0 more fix 2026-03-08 00:27:53 +01:00
c4ee5b0394 fixes 2026-03-08 00:01:31 +01:00
643de642a1 Merge branch 'main' into feature-join-whiteboard-by-code
# Conflicts:
#	dotnet/AipsRT/Hubs/WhiteboardHub.cs
#	dotnet/AipsRT/Services/Interfaces/IMessagingService.cs
#	front/src/stores/whiteboard.ts
2026-03-07 21:40:09 +01:00
Andrija Stevanović
42f294863c Merge pull request #46 from StewKI/feature-feedback-loop
Feature feedback loop
2026-03-07 18:15:43 +01:00
24e42f1d0b removed unnecessary time gap prone to bug 2026-03-07 18:13:05 +01:00
0e6caa136e cleaned code 2026-03-07 17:51:02 +01:00
49aed2493a Merge branch 'main' into feature-feedback-loop 2026-03-07 17:09:33 +01:00
3de787e07c implement feedback loop 2026-03-07 16:15:21 +01:00
1996ae6c73 Merge branch 'main' into feature-join-whiteboard-by-code 2026-03-04 23:18:41 +01:00
4e6b5da71b RT 2026-03-04 23:13:32 +01:00
2fa0f3cb8b Messages 2026-03-04 23:13:21 +01:00
a2baa3d24c Queries 2026-03-04 23:13:12 +01:00
21425b22b5 Migration 2026-03-04 23:13:00 +01:00
0013bb18df Refactored 2026-03-04 23:12:44 +01:00
ef6305919d Refactored 2026-03-04 23:12:08 +01:00
88442f22dd Join with code endpoint and command 2026-03-04 23:10:48 +01:00
409f44476f front 2026-03-04 23:09:02 +01:00
8e2b41c0b3 pdf doc 2026-03-03 22:46:40 +01:00
4ff8c2cdfb Merge remote-tracking branch 'origin/main' 2026-03-03 21:10:25 +01:00
f37e556071 3rd phase docs 2026-03-03 21:10:02 +01:00
Andrija Stevanović
6337eb39ae Merge pull request #44 from StewKI/feature-feedback-loop
Povratna sprega priprema
2026-03-02 22:22:57 +01:00
5d57abe913 fix 2026-02-27 22:01:24 +01:00
c3b83d2041 something 2026-02-27 22:01:12 +01:00
47d62885b1 implement message types provider 2026-02-27 21:56:55 +01:00
136 changed files with 2825 additions and 618 deletions

29
.dockerignore Normal file
View File

@@ -0,0 +1,29 @@
.git
.gitignore
# IDE
.idea
.vs
.vscode
**/.idea
# Build artifacts
**/bin
**/obj
front/node_modules
front/dist
# Documentation
Docs/
# Dev files
docker/
*.sh
# Env files (secrets must not be in the image)
.env
deploy/.env
# Misc
**/*.user
**/*.DotSettings.user

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,76 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.5.2 Chrome/142.0.7444.265 Electron/39.6.1 Safari/537.36" version="29.5.2">
<diagram name="Page-1" id="CTB3YmmPJF-eA0d3sg4m">
<mxGraphModel dx="1040" dy="1791" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="9ORZMBqL03SGZpvbqk50-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="AipsWebApi" vertex="1">
<mxGeometry height="60" width="120" x="490" y="170" as="geometry" />
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-11" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" value="AipsRT" vertex="1">
<mxGeometry height="60" width="120" x="190" y="170" as="geometry" />
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-12" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" value="RabbitMQ" vertex="1">
<mxGeometry height="60" width="120" x="190" y="310" as="geometry" />
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-13" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontStyle=4;fillColor=#f8cecc;strokeColor=#b85450;" value="AipsWorker" vertex="1">
<mxGeometry height="60" width="120" x="190" y="450" as="geometry" />
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-14" parent="1" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" value="DB" vertex="1">
<mxGeometry height="80" width="60" x="520" y="440" as="geometry" />
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-16" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-13" style="shape=flexArrow;endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" target="9ORZMBqL03SGZpvbqk50-14" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="390" y="380" as="sourcePoint" />
<mxPoint x="440" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-17" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-14" style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" value="">
<mxGeometry height="100" relative="1" width="100" as="geometry">
<mxPoint x="460" y="330" as="sourcePoint" />
<mxPoint x="550" y="230" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-19" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-11" style="shape=flexArrow;endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" target="9ORZMBqL03SGZpvbqk50-12" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="390" y="380" as="sourcePoint" />
<mxPoint x="440" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-20" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-12" style="shape=flexArrow;endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" target="9ORZMBqL03SGZpvbqk50-13" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="309" y="380" as="sourcePoint" />
<mxPoint x="309" y="460" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-22" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-13" style="endArrow=classic;html=1;rounded=0;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" target="9ORZMBqL03SGZpvbqk50-12" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="390" y="380" as="sourcePoint" />
<mxPoint x="440" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-23" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-12" style="endArrow=classic;html=1;rounded=0;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" target="9ORZMBqL03SGZpvbqk50-11" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="390" y="380" as="sourcePoint" />
<mxPoint x="440" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-25" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Klient" vertex="1">
<mxGeometry height="70" width="420" x="190" y="-40" as="geometry" />
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-27" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-11" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.142;entryY=1.015;entryDx=0;entryDy=0;entryPerimeter=0;" target="9ORZMBqL03SGZpvbqk50-25" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="290" y="140" as="sourcePoint" />
<mxPoint x="340" y="90" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9ORZMBqL03SGZpvbqk50-28" edge="1" parent="1" source="9ORZMBqL03SGZpvbqk50-10" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="390" y="210" as="sourcePoint" />
<mxPoint x="550" y="34" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,150 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.5.2 Chrome/142.0.7444.265 Electron/39.6.1 Safari/537.36" version="29.5.2">
<diagram name="Page-1" id="IZF_J8x8XDGuG-dahjO2">
<mxGraphModel dx="1040" dy="620" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="KJVqxsW90z-SvCUaH26X-66" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" value="" vertex="1">
<mxGeometry height="560" width="650" x="60" y="40" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-65" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" value="" vertex="1">
<mxGeometry height="480" width="650" x="60" y="600" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-1" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="&lt;i&gt;&amp;lt;&amp;lt;interface&amp;gt;&amp;gt;&lt;/i&gt;&lt;br&gt;&lt;div&gt;IMessagePublisher&lt;/div&gt;" vertex="1">
<mxGeometry height="66" width="210" x="144" y="680" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-2" parent="KJVqxsW90z-SvCUaH26X-1" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="+ Publish(message: IMesasge)" vertex="1">
<mxGeometry height="26" width="210" y="40" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-10" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="&lt;i&gt;&amp;lt;&amp;lt;interface&amp;gt;&amp;gt;&lt;/i&gt;&lt;div&gt;IMessageSubscriber&lt;/div&gt;" vertex="1">
<mxGeometry height="66" width="250" x="399" y="680" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-11" parent="KJVqxsW90z-SvCUaH26X-10" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="+ Subscribe(handler: Func&amp;lt;IMessage&amp;gt;)" vertex="1">
<mxGeometry height="26" width="250" y="40" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-15" parent="1" style="html=1;whiteSpace=wrap;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="&lt;i&gt;&amp;lt;&amp;lt;interface&amp;gt;&amp;gt;&lt;/i&gt;&lt;div&gt;IMessage&lt;/div&gt;" vertex="1">
<mxGeometry height="50" width="110" x="329" y="850" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-20" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-26" style="endArrow=block;dashed=1;endFill=0;endSize=12;html=1;rounded=0;exitX=0.465;exitY=1.106;exitDx=0;exitDy=0;entryX=0.465;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;exitPerimeter=0;" target="KJVqxsW90z-SvCUaH26X-1" value="">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="210" y="250" as="sourcePoint" />
<mxPoint x="490" y="200" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-21" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-2" style="endArrow=open;endSize=12;dashed=1;html=1;rounded=0;exitX=0.724;exitY=0.971;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0;fillColor=#f5f5f5;strokeColor=#666666;exitPerimeter=0;" target="KJVqxsW90z-SvCUaH26X-15" value="Use">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="230" y="56" as="sourcePoint" />
<mxPoint x="390" y="56" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-22" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-11" style="endArrow=open;endSize=12;dashed=1;html=1;rounded=0;exitX=0.247;exitY=0.9;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;fillColor=#f5f5f5;strokeColor=#666666;exitPerimeter=0;" target="KJVqxsW90z-SvCUaH26X-15" value="Use">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="480" y="6" as="sourcePoint" />
<mxPoint x="640" y="6" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-23" parent="1" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="RabbitMqPublisher" vertex="1">
<mxGeometry height="80" width="230" x="134" y="440" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-24" parent="KJVqxsW90z-SvCUaH26X-23" style="text;strokeColor=#666666;fillColor=#f5f5f5;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fontColor=#333333;" value="- connection: IRabbitMqConnection" vertex="1">
<mxGeometry height="26" width="230" y="26" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-25" parent="KJVqxsW90z-SvCUaH26X-23" style="line;strokeWidth=1;fillColor=#f5f5f5;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;strokeColor=#666666;fontColor=#333333;" value="" vertex="1">
<mxGeometry height="2" width="230" y="52" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-26" parent="KJVqxsW90z-SvCUaH26X-23" style="text;strokeColor=#666666;fillColor=#f5f5f5;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fontColor=#333333;" value="+ Publish(message: IMesasge)" vertex="1">
<mxGeometry height="26" width="230" y="54" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-27" parent="1" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="RabbitMqSubscriber" vertex="1">
<mxGeometry height="80" width="250" x="406" y="440" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-28" parent="KJVqxsW90z-SvCUaH26X-27" style="text;strokeColor=#666666;fillColor=#f5f5f5;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fontColor=#333333;" value="- connection: IRabbitMqConnection" vertex="1">
<mxGeometry height="26" width="250" y="26" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-29" parent="KJVqxsW90z-SvCUaH26X-27" style="line;strokeWidth=1;fillColor=#f5f5f5;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;strokeColor=#666666;fontColor=#333333;" value="" vertex="1">
<mxGeometry height="2" width="250" y="52" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-30" parent="KJVqxsW90z-SvCUaH26X-27" style="text;strokeColor=#666666;fillColor=#f5f5f5;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fontColor=#333333;" value="+ Subscribe(handler: Func&amp;lt;IMessage&amp;gt;)" vertex="1">
<mxGeometry height="26" width="250" y="54" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-31" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-30" style="endArrow=block;dashed=1;endFill=0;endSize=12;html=1;rounded=0;exitX=0.471;exitY=0.93;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" target="KJVqxsW90z-SvCUaH26X-10" value="">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="473" y="290" as="sourcePoint" />
<mxPoint x="630" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-32" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=40;fillColor=#f5f5f5;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fontColor=#333333;strokeColor=#666666;" value="&lt;i&gt;&amp;lt;&amp;lt;interface&amp;gt;&amp;gt;&lt;/i&gt;&lt;div&gt;IRabbitMqConnection&lt;/div&gt;" vertex="1">
<mxGeometry height="66" width="180" x="294" y="280" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-33" parent="KJVqxsW90z-SvCUaH26X-32" style="text;strokeColor=#666666;fillColor=#f5f5f5;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fontColor=#333333;" value="+ CreateChannel(): IChannel" vertex="1">
<mxGeometry height="26" width="180" y="40" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-36" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-23" style="endArrow=open;endSize=12;dashed=1;html=1;rounded=0;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.273;entryY=0.981;entryDx=0;entryDy=0;fillColor=#f5f5f5;strokeColor=#666666;entryPerimeter=0;" target="KJVqxsW90z-SvCUaH26X-33" value="Use">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="220" y="520" as="sourcePoint" />
<mxPoint x="320" y="440" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-37" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-27" style="endArrow=open;endSize=12;dashed=1;html=1;rounded=0;exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0.728;entryY=1.002;entryDx=0;entryDy=0;fillColor=#f5f5f5;strokeColor=#666666;entryPerimeter=0;" target="KJVqxsW90z-SvCUaH26X-33" value="Use">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="550" y="380" as="sourcePoint" />
<mxPoint x="640" y="460" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-38" parent="1" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="RabbitMqConnection" vertex="1">
<mxGeometry height="140" width="230" x="269" y="70" as="geometry">
<mxRectangle height="30" width="150" x="255" y="585" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-39" parent="KJVqxsW90z-SvCUaH26X-38" style="text;strokeColor=#666666;fillColor=#f5f5f5;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fontColor=#333333;" value="- connection: IConnection&lt;div&gt;- configuration: IConfiguration&lt;/div&gt;" vertex="1">
<mxGeometry height="44" width="230" y="26" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-40" parent="KJVqxsW90z-SvCUaH26X-38" style="line;strokeWidth=1;fillColor=#f5f5f5;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;strokeColor=#666666;fontColor=#333333;" value="" vertex="1">
<mxGeometry width="230" y="70" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-41" parent="KJVqxsW90z-SvCUaH26X-38" style="text;strokeColor=#666666;fillColor=#f5f5f5;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fontColor=#333333;" value="+ CreateChannel(): IChannel&lt;div&gt;- CreateConnection()&lt;/div&gt;&lt;div&gt;-&amp;nbsp;CreateConnectionFactory()&lt;/div&gt;" vertex="1">
<mxGeometry height="70" width="230" y="70" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-49" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-38" style="endArrow=block;dashed=1;endFill=0;endSize=12;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fillColor=#f5f5f5;strokeColor=#666666;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" target="KJVqxsW90z-SvCUaH26X-32" value="">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="370" y="-190" as="sourcePoint" />
<mxPoint x="600" y="470" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-51" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="MoveShapeMessage" vertex="1">
<mxGeometry height="52" width="140" x="210" y="990" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-52" parent="KJVqxsW90z-SvCUaH26X-51" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="+ ..." vertex="1">
<mxGeometry height="26" width="140" y="26" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-60" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="AddRectangleMessage" vertex="1">
<mxGeometry height="52" width="156" x="414" y="990" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-61" parent="KJVqxsW90z-SvCUaH26X-60" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;" value="+ ..." vertex="1">
<mxGeometry height="26" width="156" y="26" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-62" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-51" style="endArrow=block;dashed=1;endFill=0;endSize=12;html=1;rounded=0;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;fillColor=#f5f5f5;strokeColor=#666666;" target="KJVqxsW90z-SvCUaH26X-15" value="">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="350" y="936" as="sourcePoint" />
<mxPoint x="273.5" y="1026" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-63" edge="1" parent="1" source="KJVqxsW90z-SvCUaH26X-60" style="endArrow=block;dashed=1;endFill=0;endSize=12;html=1;rounded=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;fillColor=#f5f5f5;strokeColor=#666666;exitX=0.25;exitY=0;exitDx=0;exitDy=0;" target="KJVqxsW90z-SvCUaH26X-15" value="">
<mxGeometry relative="1" width="160" as="geometry">
<mxPoint x="603" y="76" as="sourcePoint" />
<mxPoint x="600" y="-44" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-64" parent="1" style="text;html=1;whiteSpace=wrap;align=center;verticalAlign=middle;rounded=0;" value="&lt;font style=&quot;font-size: 22px;&quot;&gt;...&lt;/font&gt;" vertex="1">
<mxGeometry height="30" width="60" x="354" y="996" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-67" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;fontSize=16;" value="Application Layer" vertex="1">
<mxGeometry height="30" width="125" x="321.5" y="600" as="geometry" />
</mxCell>
<mxCell id="KJVqxsW90z-SvCUaH26X-68" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;fontSize=16;" value="Infrastructure Layer" vertex="1">
<mxGeometry height="30" width="180" x="295" y="570" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

17
deploy/.env.example Normal file
View File

@@ -0,0 +1,17 @@
# PostgreSQL (shared VPS instance — create DB/user manually)
POSTGRES_DB=aips_db
POSTGRES_USER=aips_user
POSTGRES_PASSWORD=CHANGE_ME_strong_password_here
# RabbitMQ
RABBITMQ_DEFAULT_USER=aips_rabbit
RABBITMQ_DEFAULT_PASS=CHANGE_ME_rabbit_password
RABBITMQ_DEFAULT_VHOST=/
RABBITMQ_EXCHANGE=aips
# JWT
JWT_ISSUER=AIPS
JWT_AUDIENCE=AIPSWebApi
JWT_KEY=CHANGE_ME_generate_a_64_char_random_string_here
JWT_EXPIRATION_MINUTES=60
JWT_REFRESH_TOKEN_EXPIRATION_DAYS=7

14
deploy/Dockerfile.front Normal file
View File

@@ -0,0 +1,14 @@
FROM oven/bun:1 AS build
WORKDIR /app
COPY front/package.json front/bun.lock ./
RUN bun install --frozen-lockfile
COPY front/ .
RUN bun run build-only
FROM nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY deploy/nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

23
deploy/Dockerfile.rt Normal file
View File

@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY dotnet/dotnet.sln dotnet/dotnet.sln
COPY dotnet/AipsCore/AipsCore.csproj dotnet/AipsCore/
COPY dotnet/AipsWebApi/AipsWebApi.csproj dotnet/AipsWebApi/
COPY dotnet/AipsRT/AipsRT.csproj dotnet/AipsRT/
COPY dotnet/AipsWorker/AipsWorker.csproj dotnet/AipsWorker/
WORKDIR /src/dotnet
RUN dotnet restore dotnet.sln
WORKDIR /src
COPY dotnet/ dotnet/
WORKDIR /src/dotnet
RUN dotnet publish AipsRT/AipsRT.csproj -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "AipsRT.dll"]

23
deploy/Dockerfile.webapi Normal file
View File

@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY dotnet/dotnet.sln dotnet/dotnet.sln
COPY dotnet/AipsCore/AipsCore.csproj dotnet/AipsCore/
COPY dotnet/AipsWebApi/AipsWebApi.csproj dotnet/AipsWebApi/
COPY dotnet/AipsRT/AipsRT.csproj dotnet/AipsRT/
COPY dotnet/AipsWorker/AipsWorker.csproj dotnet/AipsWorker/
WORKDIR /src/dotnet
RUN dotnet restore dotnet.sln
WORKDIR /src
COPY dotnet/ dotnet/
WORKDIR /src/dotnet
RUN dotnet publish AipsWebApi/AipsWebApi.csproj -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "AipsWebApi.dll"]

22
deploy/Dockerfile.worker Normal file
View File

@@ -0,0 +1,22 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY dotnet/dotnet.sln dotnet/dotnet.sln
COPY dotnet/AipsCore/AipsCore.csproj dotnet/AipsCore/
COPY dotnet/AipsWebApi/AipsWebApi.csproj dotnet/AipsWebApi/
COPY dotnet/AipsRT/AipsRT.csproj dotnet/AipsRT/
COPY dotnet/AipsWorker/AipsWorker.csproj dotnet/AipsWorker/
WORKDIR /src/dotnet
RUN dotnet restore dotnet.sln
WORKDIR /src
COPY dotnet/ dotnet/
WORKDIR /src/dotnet
RUN dotnet publish AipsWorker/AipsWorker.csproj -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "AipsWorker.dll"]

103
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,103 @@
services:
rabbitmq:
image: rabbitmq:3-management
container_name: aips-rabbitmq
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_DEFAULT_VHOST}
volumes:
- rabbitmqdata:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 10s
retries: 5
webapi:
build:
context: ..
dockerfile: deploy/Dockerfile.webapi
container_name: aips-webapi
restart: unless-stopped
environment:
ASPNETCORE_URLS: "http://+:8080"
ASPNETCORE_ENVIRONMENT: "Production"
DB_CONN_STRING: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
RABBITMQ_AMQP_URI: "amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/${RABBITMQ_DEFAULT_VHOST}"
RABBITMQ_EXCHANGE: "${RABBITMQ_EXCHANGE}"
JWT_ISSUER: "${JWT_ISSUER}"
JWT_AUDIENCE: "${JWT_AUDIENCE}"
JWT_KEY: "${JWT_KEY}"
JWT_EXPIRATION_MINUTES: "${JWT_EXPIRATION_MINUTES}"
JWT_REFRESH_TOKEN_EXPIRATION_DAYS: "${JWT_REFRESH_TOKEN_EXPIRATION_DAYS}"
networks:
- default
- back_network
depends_on:
rabbitmq:
condition: service_healthy
rt:
build:
context: ..
dockerfile: deploy/Dockerfile.rt
container_name: aips-rt
restart: unless-stopped
environment:
ASPNETCORE_URLS: "http://+:8080"
ASPNETCORE_ENVIRONMENT: "Production"
DB_CONN_STRING: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
RABBITMQ_AMQP_URI: "amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/${RABBITMQ_DEFAULT_VHOST}"
RABBITMQ_EXCHANGE: "${RABBITMQ_EXCHANGE}"
JWT_ISSUER: "${JWT_ISSUER}"
JWT_AUDIENCE: "${JWT_AUDIENCE}"
JWT_KEY: "${JWT_KEY}"
JWT_EXPIRATION_MINUTES: "${JWT_EXPIRATION_MINUTES}"
JWT_REFRESH_TOKEN_EXPIRATION_DAYS: "${JWT_REFRESH_TOKEN_EXPIRATION_DAYS}"
networks:
- default
- back_network
depends_on:
rabbitmq:
condition: service_healthy
worker:
build:
context: ..
dockerfile: deploy/Dockerfile.worker
container_name: aips-worker
restart: unless-stopped
environment:
DB_CONN_STRING: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
RABBITMQ_AMQP_URI: "amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/${RABBITMQ_DEFAULT_VHOST}"
RABBITMQ_EXCHANGE: "${RABBITMQ_EXCHANGE}"
JWT_ISSUER: "${JWT_ISSUER}"
JWT_AUDIENCE: "${JWT_AUDIENCE}"
JWT_KEY: "${JWT_KEY}"
networks:
- default
- back_network
depends_on:
rabbitmq:
condition: service_healthy
nginx:
build:
context: ..
dockerfile: deploy/Dockerfile.front
container_name: aips-nginx
restart: unless-stopped
ports:
- "8090:80"
depends_on:
- webapi
- rt
networks:
back_network:
external: true
volumes:
rabbitmqdata:

View File

@@ -0,0 +1,45 @@
server {
listen 80;
server_name aips.stewki.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name aips.stewki.com;
ssl_certificate /etc/letsencrypt/live/aips.stewki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/aips.stewki.com/privkey.pem;
client_max_body_size 10M;
location / {
proxy_pass http://host.docker.internal:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /hubs/ {
proxy_pass http://host.docker.internal:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}

46
deploy/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,46 @@
upstream webapi {
server webapi:8080;
}
upstream rt {
server rt:8080;
}
server {
listen 80;
server_name _;
client_max_body_size 10M;
# REST API
location /api/ {
proxy_pass http://webapi;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# SignalR hubs (WebSocket support)
location /hubs/ {
proxy_pass http://rt;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# Vue SPA
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}

27
dos-start-back.bat Normal file
View File

@@ -0,0 +1,27 @@
<# : batch portion
@echo off
set "SCRIPT_DIR=%~dp0"
powershell -ExecutionPolicy Bypass "iex((Get-Content '%~f0' -Raw))"
exit /b
#>
Set-Location (Join-Path $env:SCRIPT_DIR "dotnet")
$jobs = @()
$jobs += Start-Job -ScriptBlock { Set-Location $using:PWD; dotnet run --project AipsWebApi 2>&1 | ForEach-Object { "[WebApi] $_" } }
$jobs += Start-Job -ScriptBlock { Set-Location $using:PWD; dotnet run --project AipsRT 2>&1 | ForEach-Object { "[RT] $_" } }
$jobs += Start-Job -ScriptBlock { Set-Location $using:PWD; dotnet run --project AipsWorker 2>&1 | ForEach-Object { "[Worker] $_" } }
try {
while ($jobs | Where-Object { $_.State -eq 'Running' }) {
foreach ($job in $jobs) {
Receive-Job -Job $job
}
Start-Sleep -Milliseconds 200
}
foreach ($job in $jobs) {
Receive-Job -Job $job
}
} finally {
$jobs | Stop-Job -PassThru | Remove-Job
}

4
dos-start-front.bat Normal file
View File

@@ -0,0 +1,4 @@
@echo off
cd /d "%~dp0front"
bun dev

4
dos-start-infra.bat Normal file
View File

@@ -0,0 +1,4 @@
@echo off
cd /d "%~dp0docker"
docker compose -p aips --env-file ..\.env up

View File

@@ -21,7 +21,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Application\Models\Shape\Command\DeleteShape\" /> <Folder Include="Application\Models\Shape\Command\DeleteShape\" />
<Folder Include="Domain\Models\WhiteboardMembership\Validation\" />
<Folder Include="Infrastructure\Persistence\Db\Migrations\" /> <Folder Include="Infrastructure\Persistence\Db\Migrations\" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,6 @@
namespace AipsCore.Application.Abstract;
public interface IWhiteboardAwareContext
{
Guid GetWhiteboardId();
}

View File

@@ -0,0 +1,6 @@
namespace AipsCore.Application.Abstract.MessageBroking;
public interface IMessageTypesProvider
{
ICollection<Type> GetAllMessageTypes();
}

View File

@@ -1,7 +0,0 @@
namespace AipsCore.Application.Abstract.MessageBroking;
public enum MessageTag
{
Worker,
RT
}

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
namespace AipsCore.Application.Common.Message.AcceptUserRequestToJoin;
public record AcceptUserRequestToJoinMessage(AcceptUserRequestToJoinCommand Command) : IMessage;

View File

@@ -0,0 +1,19 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking;
namespace AipsCore.Application.Common.Message.AcceptUserRequestToJoin;
public class AcceptUserRequestToJoinMessageHandler : IMessageHandler<AcceptUserRequestToJoinMessage>
{
private readonly IDispatcher _dispatcher;
public AcceptUserRequestToJoinMessageHandler(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task Handle(AcceptUserRequestToJoinMessage message, CancellationToken cancellationToken)
{
await _dispatcher.Execute(message.Command, cancellationToken);
}
}

View File

@@ -1,6 +1,13 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Shape.Command.CreateArrow; using AipsCore.Application.Models.Shape.Command.CreateArrow;
namespace AipsCore.Application.Common.Message.AddArrow; namespace AipsCore.Application.Common.Message.AddArrow;
public record AddArrowMessage(CreateArrowCommand Command) : IMessage; public record AddArrowMessage(CreateArrowCommand Command) : IMessage, IWhiteboardAwareContext
{
public Guid GetWhiteboardId()
{
return Guid.Parse(Command.WhiteboardId);
}
}

View File

@@ -1,6 +1,13 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Shape.Command.CreateLine; using AipsCore.Application.Models.Shape.Command.CreateLine;
namespace AipsCore.Application.Common.Message.AddLine; namespace AipsCore.Application.Common.Message.AddLine;
public record AddLineMessage(CreateLineCommand Command) : IMessage; public record AddLineMessage(CreateLineCommand Command) : IMessage, IWhiteboardAwareContext
{
public Guid GetWhiteboardId()
{
return Guid.Parse(Command.WhiteboardId);
}
}

View File

@@ -1,6 +1,13 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Shape.Command.CreateRectangle; using AipsCore.Application.Models.Shape.Command.CreateRectangle;
namespace AipsCore.Application.Common.Message.AddRectangle; namespace AipsCore.Application.Common.Message.AddRectangle;
public record AddRectangleMessage(CreateRectangleCommand Command) : IMessage; public record AddRectangleMessage(CreateRectangleCommand Command) : IMessage, IWhiteboardAwareContext
{
public Guid GetWhiteboardId()
{
return Guid.Parse(Command.WhiteboardId);
}
}

View File

@@ -1,6 +1,13 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Shape.Command.CreateTextShape; using AipsCore.Application.Models.Shape.Command.CreateTextShape;
namespace AipsCore.Application.Common.Message.AddTextShape; namespace AipsCore.Application.Common.Message.AddTextShape;
public record AddTextShapeMessage(CreateTextShapeCommand Command) : IMessage; public record AddTextShapeMessage(CreateTextShapeCommand Command) : IMessage, IWhiteboardAwareContext
{
public Guid GetWhiteboardId()
{
return Guid.Parse(Command.WhiteboardId);
}
}

View File

@@ -0,0 +1,13 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Domain.Common.Validation;
namespace AipsCore.Application.Common.Message.ErrorMessage;
public record ErrorMessage(Guid WhiteboardId, ICollection<ValidationError> Errors) : IMessage, IWhiteboardAwareContext
{
public Guid GetWhiteboardId()
{
return WhiteboardId;
}
}

View File

@@ -0,0 +1,18 @@
using AipsCore.Application.Abstract.MessageBroking;
namespace AipsCore.Application.Common.Message.ErrorMessage;
public class ErrorMessageHandler : IMessageHandler<ErrorMessage>
{
private readonly IErrorMessageHandleStrategy _handleStrategy;
public ErrorMessageHandler(IErrorMessageHandleStrategy handleStrategy)
{
_handleStrategy = handleStrategy;
}
public async Task Handle(ErrorMessage message, CancellationToken cancellationToken)
{
await _handleStrategy.Handle(message, cancellationToken);
}
}

View File

@@ -0,0 +1,6 @@
namespace AipsCore.Application.Common.Message.ErrorMessage;
public interface IErrorMessageHandleStrategy
{
Task Handle(ErrorMessage message, CancellationToken cancellationToken);
}

View File

@@ -1,6 +1,13 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Shape.Command.MoveShape; using AipsCore.Application.Models.Shape.Command.MoveShape;
namespace AipsCore.Application.Common.Message.MoveShape; namespace AipsCore.Application.Common.Message.MoveShape;
public record MoveShapeMessage(MoveShapeCommand Command) : IMessage; public record MoveShapeMessage(Guid WhiteboardId, MoveShapeCommand Command) : IMessage, IWhiteboardAwareContext
{
public Guid GetWhiteboardId()
{
return WhiteboardId;
}
}

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
namespace AipsCore.Application.Common.Message.RejectUserRequestToJoin;
public record RejectUserRequestToJoinMessage(RejectUserRequestToJoinCommand Command): IMessage;

View File

@@ -0,0 +1,19 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking;
namespace AipsCore.Application.Common.Message.RejectUserRequestToJoin;
public class RejectUserRequestToJoinMessageHandler : IMessageHandler<RejectUserRequestToJoinMessage>
{
private readonly IDispatcher _dispatcher;
public RejectUserRequestToJoinMessageHandler(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task Handle(RejectUserRequestToJoinMessage message, CancellationToken cancellationToken)
{
await _dispatcher.Execute(message.Command, cancellationToken);
}
}

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
namespace AipsCore.Application.Common.Message.UserCanceledRequestToJoin;
public record UserCanceledRequestToJoinMessage(UserCanceledRequestToJoinCommand Command): IMessage;

View File

@@ -0,0 +1,19 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking;
namespace AipsCore.Application.Common.Message.UserCanceledRequestToJoin;
public class UserCanceledRequestToJoinMessageHandler : IMessageHandler<UserCanceledRequestToJoinMessage>
{
private readonly IDispatcher _dispatcher;
public UserCanceledRequestToJoinMessageHandler(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task Handle(UserCanceledRequestToJoinMessage message, CancellationToken cancellationToken)
{
await _dispatcher.Execute(message.Command, cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
public record AcceptUserRequestToJoinCommand(string WhiteboardId, string UserId): ICommand;

View File

@@ -0,0 +1,40 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
public class AcceptUserRequestToJoinCommandHandler : ICommandHandler<AcceptUserRequestToJoinCommand>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUnitOfWork _unitOfWork;
public AcceptUserRequestToJoinCommandHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository, IUnitOfWork unitOfWork)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_unitOfWork = unitOfWork;
}
public async Task Handle(AcceptUserRequestToJoinCommand command, CancellationToken cancellationToken = default)
{
var whiteboardId = new WhiteboardId(command.WhiteboardId);
var userId = new UserId(command.UserId);
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
membership.UpdateStatus(WhiteboardMembershipStatus.Accepted);
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard;
public record AddUserToWhiteboardCommand(
string UserId,
string WhiteboardId)
: ICommand;

View File

@@ -1,12 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard;
public static class AddUserToWhiteboardCommandErrors
{
public static ValidationError WhiteboardDoesNotExist(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_exists",
Message: $"Whiteboard with id '{whiteboardId}' does not exist.");
}

View File

@@ -1,65 +0,0 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.External;
using AipsCore.Domain.Models.User.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External;
namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard;
public class AddUserToWhiteboardCommandHandler
: ICommandHandler<AddUserToWhiteboardCommand>
{
private readonly IWhiteboardRepository _whiteboardRepository;
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUserRepository _userRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IDispatcher _dispatcher;
public AddUserToWhiteboardCommandHandler(
IWhiteboardRepository whiteboardRepository,
IWhiteboardMembershipRepository whiteboardMembershipRepository,
IUserRepository userRepository,
IUnitOfWork unitOfWork,
IDispatcher dispatcher)
{
_whiteboardRepository = whiteboardRepository;
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_userRepository = userRepository;
_unitOfWork = unitOfWork;
_dispatcher = dispatcher;
}
private Domain.Models.Whiteboard.Whiteboard? _whiteboard;
private Domain.Models.User.User? _user;
public async Task Handle(AddUserToWhiteboardCommand command, CancellationToken cancellationToken = default)
{
_whiteboard = await _whiteboardRepository.GetByIdAsync(new WhiteboardId(command.WhiteboardId), cancellationToken);
_user = await _userRepository.GetByIdAsync(new UserId(command.UserId), cancellationToken);
Validate(command);
await _whiteboard!.AddUserAsync(_user!, _whiteboardMembershipRepository, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
private void Validate(AddUserToWhiteboardCommand command)
{
if (_whiteboard is null)
{
throw new ValidationException(WhiteboardErrors.NotFound(new WhiteboardId(command.WhiteboardId)));
}
if (_user is null)
{
throw new ValidationException(UserErrors.NotFound(new UserId(command.UserId)));
}
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.BanUserFromWhiteboard;
public static class BanUserFromWhiteboardCommandErrors
{
public static ValidationError WhiteboardMembershipNotFound(WhiteboardId whiteboardId, UserId userId)
=> new ValidationError(
Code: "whiteboard_membership_not_found",
Message: $"User with id '{userId.IdValue}' is not a member of whiteboard with id '{whiteboardId.IdValue}'");
public static ValidationError WhiteboardNotFound(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_found",
Message: $"Whiteboard with id '{whiteboardId.IdValue}' not found.");
}

View File

@@ -4,8 +4,10 @@ using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation; using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects; using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External; using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects; using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External; using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.BanUserFromWhiteboard; namespace AipsCore.Application.Models.Whiteboard.Command.BanUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class BanUserFromWhiteboardCommandHandler : ICommandHandler<BanUserFromWh
if (whiteboard is null) if (whiteboard is null)
{ {
throw new ValidationException(BanUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId)); throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
} }
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken); var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null) if (membership is null)
{ {
throw new ValidationException(BanUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId)); throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
} }
whiteboard.BanUser(_userContext.GetCurrentUserId(), membership); whiteboard.BanUser(_userContext.GetCurrentUserId(), membership);

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
public record JoinWithCodeCommand(string Code): ICommand<JoinWithCodeDto>;

View File

@@ -0,0 +1,65 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
namespace AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
public class JoinWithCodeCommandHandler : ICommandHandler<JoinWithCodeCommand, JoinWithCodeDto>
{
private readonly IWhiteboardRepository _whiteboardRepository;
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IUserContext _userContext;
public JoinWithCodeCommandHandler(
IWhiteboardRepository whiteboardRepository,
IWhiteboardMembershipRepository whiteboardMembershipRepository,
IUnitOfWork unitOfWork,
IUserContext userContext)
{
_whiteboardRepository = whiteboardRepository;
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_unitOfWork = unitOfWork;
_userContext = userContext;
}
public async Task<JoinWithCodeDto> Handle(JoinWithCodeCommand command, CancellationToken cancellationToken = default)
{
var userId = _userContext.GetCurrentUserId();
var code = new WhiteboardCode(command.Code);
var whiteboard = await _whiteboardRepository.GetByCodeAsync(code, cancellationToken);
if (whiteboard is null)
{
throw new ValidationException(WhiteboardErrors.NotFound(code));
}
if (!whiteboard.ShouldRequestToJoin(userId))
{
return new JoinWithCodeDto(whiteboard.Id.IdValue, WhiteboardMembershipStatus.Accepted);
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboard.Id, userId, cancellationToken);
if (membership is null)
{
membership = whiteboard.RequestJoin(userId);
}
else
{
whiteboard.RequestReJoin(membership);
}
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return new JoinWithCodeDto(whiteboard.Id.IdValue, membership.Status);
}
}

View File

@@ -0,0 +1,5 @@
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
public record JoinWithCodeDto(string WhiteboardId, WhiteboardMembershipStatus Status);

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.KickUserFromWhiteboard;
public static class KickUserFromWhiteboardCommandErrors
{
public static ValidationError WhiteboardMembershipNotFound(WhiteboardId whiteboardId, UserId userId)
=> new ValidationError(
Code: "whiteboard_membership_not_found",
Message: $"User with id '{userId}' is not a member of whiteboard with id '{whiteboardId}'");
public static ValidationError WhiteboardNotFound(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_found",
Message: $"Whiteboard with id '{whiteboardId}' not found.");
}

View File

@@ -4,8 +4,10 @@ using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation; using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects; using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External; using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects; using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External; using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.KickUserFromWhiteboard; namespace AipsCore.Application.Models.Whiteboard.Command.KickUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class KickUserFromWhiteboardCommandHandler : ICommandHandler<KickUserFrom
if (whiteboard is null) if (whiteboard is null)
{ {
throw new ValidationException(KickUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId)); throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
} }
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken); var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null) if (membership is null)
{ {
throw new ValidationException(KickUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId)); throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
} }
whiteboard.KickUser(_userContext.GetCurrentUserId(), membership); whiteboard.KickUser(_userContext.GetCurrentUserId(), membership);

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
public record RejectUserRequestToJoinCommand(string WhiteboardId, string UserId): ICommand;

View File

@@ -0,0 +1,40 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
public class RejectUserRequestToJoinCommandHandler : ICommandHandler<RejectUserRequestToJoinCommand>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUnitOfWork _unitOfWork;
public RejectUserRequestToJoinCommandHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository, IUnitOfWork unitOfWork)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_unitOfWork = unitOfWork;
}
public async Task Handle(RejectUserRequestToJoinCommand command, CancellationToken cancellationToken = default)
{
var whiteboardId = new WhiteboardId(command.WhiteboardId);
var userId = new UserId(command.UserId);
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
membership.UpdateStatus(WhiteboardMembershipStatus.Rejected);
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.UnbanUserFromWhiteboard;
public static class UnbanUserFromWhiteboardCommandErrors
{
public static ValidationError WhiteboardMembershipNotFound(WhiteboardId whiteboardId, UserId userId)
=> new ValidationError(
Code: "whiteboard_membership_not_found",
Message: $"User with id '{userId}' is not a member of whiteboard with id '{whiteboardId}'");
public static ValidationError WhiteboardNotFound(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_found",
Message: $"Whiteboard with id '{whiteboardId}' not found.");
}

View File

@@ -4,8 +4,10 @@ using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation; using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects; using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External; using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects; using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External; using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.UnbanUserFromWhiteboard; namespace AipsCore.Application.Models.Whiteboard.Command.UnbanUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class UnbanUserFromWhiteboardCommandHandler : ICommandHandler<UnbanUserFr
if (whiteboard is null) if (whiteboard is null)
{ {
throw new ValidationException(UnbanUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId)); throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
} }
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken); var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null) if (membership is null)
{ {
throw new ValidationException(UnbanUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId)); throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
} }
whiteboard.UnbanUser(_userContext.GetCurrentUserId(), membership); whiteboard.UnbanUser(_userContext.GetCurrentUserId(), membership);

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
public record UserCanceledRequestToJoinCommand(string WhiteboardId, string UserId): ICommand;

View File

@@ -0,0 +1,40 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
public class CancelJoinRequestCommandHandler : ICommandHandler<UserCanceledRequestToJoinCommand>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUnitOfWork _unitOfWork;
public CancelJoinRequestCommandHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository, IUnitOfWork unitOfWork)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_unitOfWork = unitOfWork;
}
public async Task Handle(UserCanceledRequestToJoinCommand command, CancellationToken cancellationToken = default)
{
var whiteboardId = new WhiteboardId(command.WhiteboardId);
var userId = new UserId(command.UserId);
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
membership.UpdateStatus(WhiteboardMembershipStatus.Cancelled);
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
public record GetMembershipStatusQuery(string WhiteboardId, string UserId): IQuery<WhiteboardMembershipStatus>;

View File

@@ -0,0 +1,34 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
public class GetMembershipStatusQueryHandler : IQueryHandler<GetMembershipStatusQuery, WhiteboardMembershipStatus>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
public GetMembershipStatusQueryHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
}
public async Task<WhiteboardMembershipStatus> Handle(GetMembershipStatusQuery query, CancellationToken cancellationToken = default)
{
var userId = new UserId(query.UserId);
var whiteboardId = new WhiteboardId(query.WhiteboardId);
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
return membership.Status;
}
}

View File

@@ -1,6 +1,7 @@
using AipsCore.Application.Abstract.Query; using AipsCore.Application.Abstract.Query;
using AipsCore.Application.Abstract.UserContext; using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Models.Whiteboard.Enums; using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Infrastructure.Persistence.Db; using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -32,7 +33,7 @@ public class GetRecentWhiteboardsQueryHandler : IQueryHandler<GetRecentWhiteboar
.Include(m => m.Whiteboard) .Include(m => m.Whiteboard)
.Where(m => ( .Where(m => (
m.UserId == userIdGuid && m.UserId == userIdGuid &&
m.IsBanned == false && m.Status != WhiteboardMembershipStatus.Banned &&
m.Whiteboard != null && m.Whiteboard != null &&
m.Whiteboard.State != WhiteboardState.Deleted m.Whiteboard.State != WhiteboardState.Deleted
)) ))

View File

@@ -34,6 +34,7 @@ public class GetWhiteboardInfoRTQueryHandler
return _context.Whiteboards return _context.Whiteboards
.Where(w => w.Id == whiteboardId) .Where(w => w.Id == whiteboardId)
.Include(w => w.Memberships) .Include(w => w.Memberships)
.ThenInclude(m => m.User)
.Include(w => w.Owner) .Include(w => w.Owner)
.Include(w => w.Shapes); .Include(w => w.Shapes);
} }

View File

@@ -1,11 +0,0 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
public record CreateWhiteboardMembershipCommand(
string WhiteboardId,
bool IsBanned,
bool EditingEnabled,
bool CanJoin)
: ICommand<WhiteboardMembershipId>;

View File

@@ -1,42 +0,0 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
public class CreateWhiteboardMembershipCommandHandler : ICommandHandler<CreateWhiteboardMembershipCommand, WhiteboardMembershipId>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUserContext _userContext;
private readonly IUnitOfWork _unitOfWork;
public CreateWhiteboardMembershipCommandHandler(
IWhiteboardMembershipRepository whiteboardMembershipRepository,
IUserContext userContext,
IUnitOfWork unitOfWork)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_userContext = userContext;
_unitOfWork = unitOfWork;
}
public async Task<WhiteboardMembershipId> Handle(CreateWhiteboardMembershipCommand command, CancellationToken cancellationToken = default)
{
var userId = _userContext.GetCurrentUserId();
var whiteboardMembership = Domain.Models.WhiteboardMembership.WhiteboardMembership.Create(
command.WhiteboardId,
userId.IdValue,
command.IsBanned,
command.EditingEnabled,
command.CanJoin,
DateTime.UtcNow);
await _whiteboardMembershipRepository.SaveAsync(whiteboardMembership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return whiteboardMembership.Id;
}
}

View File

@@ -7,7 +7,7 @@ public class MaxLengthRule : AbstractRule
{ {
private readonly string _stringValue; private readonly string _stringValue;
private readonly int _maximumLentgh; private readonly int _maximumLentgh;
protected override string ErrorCode => "minimum_length"; protected override string ErrorCode => "maximum_length";
protected override string ErrorMessage protected override string ErrorMessage
=> $"Length of '{ValueObjectName}' must be at most {_maximumLentgh} characters"; => $"Length of '{ValueObjectName}' must be at most {_maximumLentgh} characters";

View File

@@ -6,5 +6,6 @@ namespace AipsCore.Domain.Models.Whiteboard.External;
public interface IWhiteboardRepository public interface IWhiteboardRepository
: IAbstractRepository<Whiteboard, WhiteboardId>, ISoftDeletableRepository<WhiteboardId> : IAbstractRepository<Whiteboard, WhiteboardId>, ISoftDeletableRepository<WhiteboardId>
{ {
Task<bool> WhiteboardCodeExists(WhiteboardCode whiteboardCode); Task<bool> WhiteboardCodeExistsAsync(WhiteboardCode whiteboardCode);
Task<Whiteboard?> GetByCodeAsync(WhiteboardCode whiteboardCode, CancellationToken cancellationToken = default);
} }

View File

@@ -7,6 +7,38 @@ namespace AipsCore.Domain.Models.Whiteboard.Validation;
public class WhiteboardErrors : AbstractErrors<Whiteboard, WhiteboardId> public class WhiteboardErrors : AbstractErrors<Whiteboard, WhiteboardId>
{ {
public static ValidationError NotFound(WhiteboardCode whiteboardCode)
{
const string code = "not_found";
string message = $"Whiteboard with code '{whiteboardCode.CodeValue}' was not found!";
return CreateValidationError(code,message);
}
public static ValidationError CannotJoin(WhiteboardCode whiteboardCode)
{
const string code = "cannot_join_whiteboard";
string message = $"Cannot join the whiteboard with code '{whiteboardCode.CodeValue}'";
return CreateValidationError(code,message);
}
public static ValidationError UserBanned(UserId userId)
{
const string code = "user_banned_from_whiteboard";
string message = $"User with id '{userId}' is banned from this whiteboard.";
return CreateValidationError(code,message);
}
public static ValidationError UserAlreadyTryingToJoin(UserId userId)
{
const string code = "user_already_trying_to_join_whiteboard";
string message = $"User with id '{userId}' is already trying to join the whiteboard.";
return CreateValidationError(code,message);
}
public static ValidationError UserAlreadyAdded(UserId userId) public static ValidationError UserAlreadyAdded(UserId userId)
{ {
string code = "user_already_added"; string code = "user_already_added";

View File

@@ -36,7 +36,7 @@ public record WhiteboardCode : AbstractValueObject
{ {
whiteboardCode = Generate(); whiteboardCode = Generate();
codeExists = await whiteboardRepository.WhiteboardCodeExists(whiteboardCode); codeExists = await whiteboardRepository.WhiteboardCodeExistsAsync(whiteboardCode);
} while (codeExists); } while (codeExists);
return whiteboardCode; return whiteboardCode;

View File

@@ -1,42 +1,47 @@
using System.Runtime.InteropServices.Swift;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation; using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Enums; using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Domain.Models.Whiteboard.Validation; using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects; using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
namespace AipsCore.Domain.Models.Whiteboard; namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard : DomainModel<WhiteboardId> public partial class Whiteboard
{ {
public async Task AddUserAsync( public WhiteboardMembership.WhiteboardMembership RequestJoin(UserId userId)
User.User user,
IWhiteboardMembershipRepository membershipRepository,
CancellationToken cancellationToken = default)
{ {
var membership return WhiteboardMembership.WhiteboardMembership.Create(
= await membershipRepository.GetByWhiteboardAndUserAsync(this.Id, user.Id, cancellationToken); Id.IdValue,
userId.IdValue,
false,
DetermineJoinStatus(),
DateTime.UtcNow);
}
if (membership is not null) public void RequestReJoin(WhiteboardMembership.WhiteboardMembership membership)
{
switch (membership.Status)
{ {
throw new ValidationException(WhiteboardErrors.UserAlreadyAdded(user.Id)); case WhiteboardMembershipStatus.Banned:
throw new ValidationException(WhiteboardErrors.UserBanned(membership.UserId));
case WhiteboardMembershipStatus.Pending:
throw new ValidationException(WhiteboardErrors.UserAlreadyTryingToJoin(membership.UserId));
case WhiteboardMembershipStatus.Accepted:
break;
default:
membership.UpdateStatus(DetermineJoinStatus());
break;
} }
membership = WhiteboardMembership.WhiteboardMembership.Create(
this.Id.IdValue,
user.Id.IdValue,
false,
false,
this.GetCanJoin(),
DateTime.UtcNow
);
await membershipRepository.AddAsync(membership, cancellationToken);
} }
private bool GetCanJoin() private WhiteboardMembershipStatus DetermineJoinStatus()
{ {
return this.JoinPolicy == WhiteboardJoinPolicy.FreeToJoin; return JoinPolicy switch
{
WhiteboardJoinPolicy.FreeToJoin => WhiteboardMembershipStatus.Accepted,
WhiteboardJoinPolicy.RequestToJoin => WhiteboardMembershipStatus.Pending,
WhiteboardJoinPolicy.Private => throw new ValidationException(WhiteboardErrors.CannotJoin(Code)),
_ => throw new ArgumentOutOfRangeException()
};
} }
} }

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void BanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (WhiteboardOwnerId != currentUserId)
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanBanOtherUsers(currentUserId));
}
whiteboardMembership.Ban();
}
}

View File

@@ -6,6 +6,11 @@ public partial class Whiteboard
{ {
public bool CanUserDelete(UserId userId) public bool CanUserDelete(UserId userId)
{ {
return WhiteboardOwnerId.IdValue == userId.IdValue; return IsOwner(userId);
}
public bool ShouldRequestToJoin(UserId userId)
{
return !IsOwner(userId);
} }
} }

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void KickUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (WhiteboardOwnerId != currentUserId)
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanKickOtherUsers(currentUserId));
}
whiteboardMembership.Kick();
}
}

View File

@@ -0,0 +1,43 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void BanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (IsOwner(currentUserId))
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanBanOtherUsers(currentUserId));
}
whiteboardMembership.Ban();
}
public void UnbanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (IsOwner(currentUserId))
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanUnbanOtherUsers(currentUserId));
}
whiteboardMembership.Unban();
}
public void KickUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (IsOwner(currentUserId))
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanKickOtherUsers(currentUserId));
}
whiteboardMembership.Kick();
}
private bool IsOwner(UserId userId)
{
return WhiteboardOwnerId.IdValue == userId.IdValue;
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void UnbanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (WhiteboardOwnerId != currentUserId)
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanUnbanOtherUsers(currentUserId));
}
whiteboardMembership.Unban();
}
}

View File

@@ -0,0 +1,13 @@
namespace AipsCore.Domain.Models.WhiteboardMembership.Enums;
public enum WhiteboardMembershipStatus
{
Pending,
Accepted,
Active,
Inactive,
Rejected,
Cancelled,
Kicked,
Banned
}

View File

@@ -0,0 +1,18 @@
using AipsCore.Domain.Abstract.Validation;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership.Validation;
public class WhiteboardMembershipErrors : AbstractErrors<WhiteboardMembership, WhiteboardMembershipId>
{
public static ValidationError NotFound(WhiteboardId whiteboardId, UserId userId)
{
const string code = "whiteboard_membership_not_found";
string message = $"Whiteboard membership with whiteboard id {whiteboardId.IdValue} and user id {userId.IdValue} not found.";
return CreateValidationError(code, message);
}
}

View File

@@ -1,20 +0,0 @@
using AipsCore.Domain.Abstract.Rule;
using AipsCore.Domain.Abstract.ValueObject;
namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
public record WhiteboardMembershipCanJoin : AbstractValueObject
{
public bool CanJoinValue { get; init; }
public WhiteboardMembershipCanJoin(bool CanJoinValue)
{
this.CanJoinValue = CanJoinValue;
Validate();
}
protected override ICollection<IRule> GetValidationRules()
{
return [];
}
}

View File

@@ -1,20 +0,0 @@
using AipsCore.Domain.Abstract.Rule;
using AipsCore.Domain.Abstract.ValueObject;
namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
public record WhiteboardMembershipIsBanned : AbstractValueObject
{
public bool IsBannedValue { get; init; }
public WhiteboardMembershipIsBanned(bool IsBannedValue)
{
this.IsBannedValue = IsBannedValue;
Validate();
}
protected override ICollection<IRule> GetValidationRules()
{
return [];
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Ban()
{
IsBanned = new WhiteboardMembershipIsBanned(true);
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Kick()
{
CanJoin = new WhiteboardMembershipCanJoin(false);
}
}

View File

@@ -0,0 +1,26 @@
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Ban()
{
Status = WhiteboardMembershipStatus.Banned;
}
public void Unban()
{
Status = WhiteboardMembershipStatus.Cancelled;
}
public void Kick()
{
Status = WhiteboardMembershipStatus.Kicked;
}
public void UpdateStatus(WhiteboardMembershipStatus newStatus)
{
Status = newStatus;
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Unban()
{
IsBanned = new WhiteboardMembershipIsBanned(false);
}
}

View File

@@ -1,6 +1,7 @@
using AipsCore.Domain.Abstract; using AipsCore.Domain.Abstract;
using AipsCore.Domain.Models.User.ValueObjects; using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects; using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects; using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership; namespace AipsCore.Domain.Models.WhiteboardMembership;
@@ -9,97 +10,84 @@ public partial class WhiteboardMembership : DomainModel<WhiteboardMembershipId>
{ {
public WhiteboardId WhiteboardId { get; private set; } public WhiteboardId WhiteboardId { get; private set; }
public UserId UserId { get; private set; } public UserId UserId { get; private set; }
public WhiteboardMembershipIsBanned IsBanned { get; private set; }
public WhiteboardMembershipEditingEnabled EditingEnabled { get; private set; } public WhiteboardMembershipEditingEnabled EditingEnabled { get; private set; }
public WhiteboardMembershipCanJoin CanJoin { get; private set; } public WhiteboardMembershipStatus Status { get; private set; }
public WhiteboardMembershipLastInteractedAt LastInteractedAt { get; private set; } public WhiteboardMembershipLastInteractedAt LastInteractedAt { get; private set; }
public WhiteboardMembership( public WhiteboardMembership(
WhiteboardMembershipId id, WhiteboardMembershipId id,
Whiteboard.Whiteboard owner, Whiteboard.Whiteboard whiteboard,
User.User user, User.User user,
WhiteboardMembershipIsBanned isBanned,
WhiteboardMembershipEditingEnabled editingEnabled, WhiteboardMembershipEditingEnabled editingEnabled,
WhiteboardMembershipCanJoin canJoin, WhiteboardMembershipStatus status,
WhiteboardMembershipLastInteractedAt lastInteractedAt) WhiteboardMembershipLastInteractedAt lastInteractedAt)
: base(id) : base(id)
{ {
WhiteboardId = owner.Id; WhiteboardId = whiteboard.Id;
UserId = user.Id; UserId = user.Id;
IsBanned = isBanned;
EditingEnabled = editingEnabled; EditingEnabled = editingEnabled;
CanJoin = canJoin; Status = status;
LastInteractedAt = lastInteractedAt; LastInteractedAt = lastInteractedAt;
} }
public WhiteboardMembership( public WhiteboardMembership(
WhiteboardMembershipId id, WhiteboardMembershipId id,
WhiteboardId ownerId, WhiteboardId whiteboardId,
UserId userId, UserId userId,
WhiteboardMembershipIsBanned isBanned,
WhiteboardMembershipEditingEnabled editingEnabled, WhiteboardMembershipEditingEnabled editingEnabled,
WhiteboardMembershipCanJoin canJoin, WhiteboardMembershipStatus status,
WhiteboardMembershipLastInteractedAt lastInteractedAt) WhiteboardMembershipLastInteractedAt lastInteractedAt)
: base(id) : base(id)
{ {
WhiteboardId = ownerId; WhiteboardId = whiteboardId;
UserId = userId; UserId = userId;
IsBanned = isBanned;
EditingEnabled = editingEnabled; EditingEnabled = editingEnabled;
CanJoin = canJoin; Status = status;
LastInteractedAt = lastInteractedAt; LastInteractedAt = lastInteractedAt;
} }
public static WhiteboardMembership Create( public static WhiteboardMembership Create(
string id, string id,
string ownerId, string whiteboardId,
string userId, string userId,
bool isBanned,
bool editingEnabled, bool editingEnabled,
bool canJoin, WhiteboardMembershipStatus status,
DateTime lastInteractedAt) DateTime lastInteractedAt)
{ {
var whiteboardMembershipId = new WhiteboardMembershipId(id); var whiteboardMembershipId = new WhiteboardMembershipId(id);
var whiteboardId = new WhiteboardId(ownerId); var whiteboardIdVo = new WhiteboardId(whiteboardId);
var userIdVo = new UserId(userId); var userIdVo = new UserId(userId);
var isBannedVo = new WhiteboardMembershipIsBanned(isBanned);
var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled); var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled);
var canJoinVo = new WhiteboardMembershipCanJoin(canJoin);
var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt); var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt);
return new WhiteboardMembership( return new WhiteboardMembership(
whiteboardMembershipId, whiteboardMembershipId,
whiteboardId, whiteboardIdVo,
userIdVo, userIdVo,
isBannedVo,
editingEnabledVo, editingEnabledVo,
canJoinVo, status,
lastInteractedAtVo); lastInteractedAtVo);
} }
public static WhiteboardMembership Create( public static WhiteboardMembership Create(
string ownerId, string whiteboardId,
string userId, string userId,
bool isBanned,
bool editingEnabled, bool editingEnabled,
bool canJoin, WhiteboardMembershipStatus status,
DateTime lastInteractedAt) DateTime lastInteractedAt)
{ {
var whiteboardMembershipId = WhiteboardMembershipId.Any(); var whiteboardMembershipId = WhiteboardMembershipId.Any();
var whiteboardId = new WhiteboardId(ownerId); var whiteboardIdVo = new WhiteboardId(whiteboardId);
var userIdVo = new UserId(userId); var userIdVo = new UserId(userId);
var isBannedVo = new WhiteboardMembershipIsBanned(isBanned);
var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled); var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled);
var canJoinVo = new WhiteboardMembershipCanJoin(canJoin);
var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt); var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt);
return new WhiteboardMembership( return new WhiteboardMembership(
whiteboardMembershipId, whiteboardMembershipId,
whiteboardId, whiteboardIdVo,
userIdVo, userIdVo,
isBannedVo,
editingEnabledVo, editingEnabledVo,
canJoinVo, status,
lastInteractedAtVo); lastInteractedAtVo);
} }
} }

View File

@@ -10,12 +10,10 @@ namespace AipsCore.Infrastructure.Authentication.AuthService;
public class EfAuthService : IAuthService public class EfAuthService : IAuthService
{ {
private readonly AipsDbContext _dbContext;
private readonly UserManager<Persistence.User.User> _userManager; private readonly UserManager<Persistence.User.User> _userManager;
public EfAuthService(AipsDbContext dbContext, UserManager<Persistence.User.User> userManager) public EfAuthService(UserManager<Persistence.User.User> userManager)
{ {
_dbContext = dbContext;
_userManager = userManager; _userManager = userManager;
} }
@@ -27,8 +25,8 @@ public class EfAuthService : IAuthService
if (!result.Succeeded) if (!result.Succeeded)
{ {
var errors = string.Join(", ", result.Errors.Select(e => e.Description)); var validationErrors = result.Errors.Select(e => new ValidationError(e.Code, e.Description)).ToList();
throw new Exception($"User registration failed: {errors}"); throw new ValidationException(validationErrors);
} }
await _userManager.AddToRoleAsync(entity, UserRole.User.Name); await _userManager.AddToRoleAsync(entity, UserRole.User.Name);

View File

@@ -0,0 +1,503 @@
// <auto-generated />
using System;
using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AipsCore.Infrastructure.Persistence.Db.Migrations
{
[DbContext(typeof(AipsDbContext))]
[Migration("20260226153018_ReworkedMembership")]
partial class ReworkedMembership
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Shape.Shape", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AuthorId")
.HasColumnType("uuid");
b.Property<string>("Color")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int?>("EndPositionX")
.HasColumnType("integer");
b.Property<int?>("EndPositionY")
.HasColumnType("integer");
b.Property<int>("PositionX")
.HasColumnType("integer");
b.Property<int>("PositionY")
.HasColumnType("integer");
b.Property<int?>("TextSize")
.HasColumnType("integer");
b.Property<string>("TextValue")
.HasColumnType("text");
b.Property<int?>("Thickness")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<Guid>("WhiteboardId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("WhiteboardId");
b.ToTable("Shapes");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.User.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("JoinPolicy")
.HasColumnType("integer");
b.Property<int>("MaxParticipants")
.HasColumnType("integer");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Whiteboards");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.WhiteboardMembership.WhiteboardMembership", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EditingEnabled")
.HasColumnType("boolean");
b.Property<DateTime>("LastInteractedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("WhiteboardId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("WhiteboardId");
b.ToTable("WhiteboardMemberships");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Shape.Shape", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "Author")
.WithMany("Shapes")
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", "Whiteboard")
.WithMany("Shapes")
.HasForeignKey("WhiteboardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Author");
b.Navigation("Whiteboard");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "Owner")
.WithMany("Whiteboards")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.WhiteboardMembership.WhiteboardMembership", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User")
.WithMany("Memberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", "Whiteboard")
.WithMany("Memberships")
.HasForeignKey("WhiteboardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("Whiteboard");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.User.User", b =>
{
b.Navigation("Memberships");
b.Navigation("Shapes");
b.Navigation("Whiteboards");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b =>
{
b.Navigation("Memberships");
b.Navigation("Shapes");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AipsCore.Infrastructure.Persistence.Db.Migrations
{
/// <inheritdoc />
public partial class ReworkedMembership : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CanJoin",
table: "WhiteboardMemberships");
migrationBuilder.DropColumn(
name: "IsBanned",
table: "WhiteboardMemberships");
migrationBuilder.AddColumn<int>(
name: "Status",
table: "WhiteboardMemberships",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Status",
table: "WhiteboardMemberships");
migrationBuilder.AddColumn<bool>(
name: "CanJoin",
table: "WhiteboardMemberships",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsBanned",
table: "WhiteboardMemberships",
type: "boolean",
nullable: false,
defaultValue: false);
}
}
}

View File

@@ -22,7 +22,7 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Authentication.RefreshToken.RefreshToken", b => modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -214,18 +214,15 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<bool>("CanJoin")
.HasColumnType("boolean");
b.Property<bool>("EditingEnabled") b.Property<bool>("EditingEnabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<bool>("IsBanned")
.HasColumnType("boolean");
b.Property<DateTime>("LastInteractedAt") b.Property<DateTime>("LastInteractedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<Guid>("UserId") b.Property<Guid>("UserId")
.HasColumnType("uuid"); .HasColumnType("uuid");
@@ -371,7 +368,7 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
b.ToTable("AspNetUserTokens", (string)null); b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Authentication.RefreshToken.RefreshToken", b => modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{ {
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User") b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User")
.WithMany() .WithMany()

View File

@@ -58,11 +58,18 @@ public class WhiteboardRepository
entity.State = model.State; entity.State = model.State;
} }
public async Task<bool> WhiteboardCodeExists(WhiteboardCode whiteboardCode) public async Task<bool> WhiteboardCodeExistsAsync(WhiteboardCode whiteboardCode)
{ {
return await Context.Whiteboards.AnyAsync(w => w.Code == whiteboardCode.CodeValue); return await Context.Whiteboards.AnyAsync(w => w.Code == whiteboardCode.CodeValue);
} }
public async Task<Domain.Models.Whiteboard.Whiteboard?> GetByCodeAsync(WhiteboardCode whiteboardCode, CancellationToken cancellationToken = default)
{
var entity = await Context.Whiteboards.FirstOrDefaultAsync(w => w.Code == whiteboardCode.CodeValue, cancellationToken);
return entity != null ? MapToModel(entity) : null;
}
public async Task SoftDeleteAsync(WhiteboardId id, CancellationToken cancellationToken = default) public async Task SoftDeleteAsync(WhiteboardId id, CancellationToken cancellationToken = default)
{ {
var entity = await Context.Whiteboards.FindAsync([new Guid(id.IdValue)], cancellationToken); var entity = await Context.Whiteboards.FindAsync([new Guid(id.IdValue)], cancellationToken);

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Infrastructure.Persistence.WhiteboardMembership; namespace AipsCore.Infrastructure.Persistence.WhiteboardMembership;
@@ -15,11 +16,9 @@ public class WhiteboardMembership
public User.User? User { get; set; } = null!; public User.User? User { get; set; } = null!;
public bool IsBanned { get; set; }
public bool EditingEnabled { get; set; } public bool EditingEnabled { get; set; }
public bool CanJoin { get; set; } public WhiteboardMembershipStatus Status { get; set; }
public DateTime LastInteractedAt { get; set; } public DateTime LastInteractedAt { get; set; }
} }

View File

@@ -24,9 +24,8 @@ public class WhiteboardMembershipRepository
entity.Id.ToString(), entity.Id.ToString(),
entity.WhiteboardId.ToString(), entity.WhiteboardId.ToString(),
entity.UserId.ToString(), entity.UserId.ToString(),
entity.IsBanned,
entity.EditingEnabled, entity.EditingEnabled,
entity.CanJoin, entity.Status,
entity.LastInteractedAt entity.LastInteractedAt
); );
} }
@@ -38,18 +37,16 @@ public class WhiteboardMembershipRepository
Id = new Guid(model.Id.IdValue), Id = new Guid(model.Id.IdValue),
WhiteboardId = new Guid(model.WhiteboardId.IdValue), WhiteboardId = new Guid(model.WhiteboardId.IdValue),
UserId = new Guid(model.UserId.IdValue), UserId = new Guid(model.UserId.IdValue),
IsBanned = model.IsBanned.IsBannedValue,
EditingEnabled = model.EditingEnabled.EditingEnabledValue, EditingEnabled = model.EditingEnabled.EditingEnabledValue,
CanJoin = model.CanJoin.CanJoinValue, Status = model.Status,
LastInteractedAt = model.LastInteractedAt.LastInteractedAtValue LastInteractedAt = model.LastInteractedAt.LastInteractedAtValue
}; };
} }
protected override void UpdateEntity(WhiteboardMembership entity, Domain.Models.WhiteboardMembership.WhiteboardMembership model) protected override void UpdateEntity(WhiteboardMembership entity, Domain.Models.WhiteboardMembership.WhiteboardMembership model)
{ {
entity.IsBanned = model.IsBanned.IsBannedValue;
entity.EditingEnabled = model.EditingEnabled.EditingEnabledValue; entity.EditingEnabled = model.EditingEnabled.EditingEnabledValue;
entity.CanJoin = model.CanJoin.CanJoinValue; entity.Status = model.Status;
entity.LastInteractedAt = model.LastInteractedAt.LastInteractedAtValue; entity.LastInteractedAt = model.LastInteractedAt.LastInteractedAtValue;
} }

View File

@@ -1,6 +1,12 @@
using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Abstract;
using AipsCore.Application.Common.Message.MoveShape;
using AipsCore.Application.Models.Shape.Command.MoveShape; using AipsCore.Application.Models.Shape.Command.MoveShape;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsRT.Model.Memberships;
using AipsRT.Model.Users;
using AipsRT.Model.Whiteboard; using AipsRT.Model.Whiteboard;
using AipsRT.Model.Whiteboard.Shapes; using AipsRT.Model.Whiteboard.Shapes;
using AipsRT.Model.Whiteboard.Structs; using AipsRT.Model.Whiteboard.Structs;
@@ -15,33 +21,147 @@ public class WhiteboardHub : Hub
{ {
private readonly WhiteboardManager _whiteboardManager; private readonly WhiteboardManager _whiteboardManager;
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private readonly MembershipService _membershipService;
private readonly UserService _userService;
public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService) public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService, MembershipService membershipService, UserService userService)
{ {
_whiteboardManager = whiteboardManager; _whiteboardManager = whiteboardManager;
_messagingService = messagingService; _messagingService = messagingService;
_membershipService = membershipService;
_userService = userService;
} }
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = CurrentUserId;
var whiteboard = _whiteboardManager.GetWhiteboardForUser(userId);
if (whiteboard != null)
{
whiteboard.RemoveActiveUser(userId);
_whiteboardManager.RemoveUserFromWhiteboard(userId);
await Clients.Group(whiteboard.WhiteboardId.ToString())
.SendAsync("Leaved", userId.ToString());
}
await base.OnDisconnectedAsync(exception);
}
public async Task JoinWhiteboard(Guid whiteboardId) public async Task JoinWhiteboard(Guid whiteboardId)
{ {
if (!_whiteboardManager.WhiteboardExists(whiteboardId)) if (!_whiteboardManager.WhiteboardExists(whiteboardId))
await _whiteboardManager.AddWhiteboard(whiteboardId); {
await _whiteboardManager.LoadWhiteboard(whiteboardId);
}
await Groups.AddToGroupAsync(Context.ConnectionId, whiteboardId.ToString()); await Groups.AddToGroupAsync(Context.ConnectionId, whiteboardId.ToString());
var whiteboard = _whiteboardManager.GetWhiteboard(whiteboardId)!;
var state = _whiteboardManager.GetWhiteboard(whiteboardId)!; var userId = CurrentUserId;
var ownerId = whiteboard.OwnerId;
_whiteboardManager.AddUserToWhiteboard(Guid.Parse(Context.UserIdentifier!), whiteboardId); WhiteboardMembershipStatus status;
if (userId == ownerId)
{
status = WhiteboardMembershipStatus.Accepted;
}
else
{
status = await _membershipService.GetMembershipStatus(whiteboardId, userId);
}
await Clients.Caller.SendAsync("InitWhiteboard", state); _whiteboardManager.AddUserToWhiteboard(userId, whiteboardId);
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
.SendAsync("Joined", Context.UserIdentifier!); if (status == WhiteboardMembershipStatus.Accepted)
{
var joiningUser = whiteboard.Users.FirstOrDefault(u => u.UserId == userId);
if (joiningUser == null)
{
if (ownerId == userId)
{
joiningUser = whiteboard.Owner;
}
else
{
joiningUser = new User(userId, Context.User?.Identity?.Name ?? "Unknown", "");
whiteboard.AddUser(joiningUser);
}
}
whiteboard.AddActiveUser(joiningUser);
var state = _whiteboardManager.GetWhiteboard(whiteboardId)!;
await Clients.Caller.SendAsync("InitWhiteboard", state);
await Clients.GroupExcept(whiteboardId.ToString(),
Context.ConnectionId).SendAsync("Joined", joiningUser);
}
else
{
await Clients.Caller.SendAsync("WaitingForApproval", userId.ToString());
var user = whiteboard.Users.FirstOrDefault(u => u.UserId == userId);
if (user == null)
{
user = await _userService.GetUser(userId);
}
await Clients.User(ownerId.ToString()).SendAsync("UserWaitingForApproval", user);
}
}
public async Task AcceptUser(Guid targetUserId)
{
var whiteboard = CurrentWhiteboard;
await _messagingService.AcceptedUser(new AcceptUserRequestToJoinCommand(whiteboard.WhiteboardId.ToString(), targetUserId.ToString()));
var user = whiteboard.Users.FirstOrDefault(u => u.UserId == targetUserId);
whiteboard.AddActiveUser(user!);
await Clients.User(targetUserId.ToString()).SendAsync("Accepted");
await Clients.User(targetUserId.ToString()).SendAsync("InitWhiteboard", whiteboard);
await Clients.GroupExcept(whiteboard.WhiteboardId.ToString(),
Context.ConnectionId).SendAsync("Joined", user);
}
public async Task RejectUser(Guid targetUserId)
{
var whiteboard = CurrentWhiteboard;
await _messagingService.RejectedUser(new RejectUserRequestToJoinCommand(whiteboard.WhiteboardId.ToString(), targetUserId.ToString()));
await Clients.User(targetUserId.ToString()).SendAsync("Rejected");
}
public async Task CancelJoinRequest()
{
var userId = CurrentUserId;
var whiteboard = _whiteboardManager.GetWhiteboardForUser(userId);
if (whiteboard != null)
{
await _messagingService.CancelJoinRequest(new UserCanceledRequestToJoinCommand(whiteboard.WhiteboardId.ToString(), userId.ToString()));
await Clients.User(whiteboard.OwnerId.ToString()).SendAsync("UserCanceledJoinRequest", userId.ToString());
}
} }
public async Task LeaveWhiteboard(Guid whiteboardId) public async Task LeaveWhiteboard(Guid whiteboardId)
{ {
var userId = CurrentUserId;
_whiteboardManager.RemoveUserFromWhiteboard(userId);
_whiteboardManager.GetWhiteboard(whiteboardId)?.RemoveActiveUser(userId);
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId) await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
.SendAsync("Leaved", Context.UserIdentifier!); .SendAsync("Leaved", Context.UserIdentifier!);
} }
private Guid CurrentUserId => Guid.Parse(Context.UserIdentifier!); private Guid CurrentUserId => Guid.Parse(Context.UserIdentifier!);
@@ -127,6 +247,6 @@ public class WhiteboardHub : Hub
{ {
await MoveShape(moveShape); await MoveShape(moveShape);
await _messagingService.MoveShape(moveShape); await _messagingService.MoveShape(CurrentWhiteboard.WhiteboardId, moveShape);
} }
} }

View File

@@ -0,0 +1,21 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsRT.Model.Memberships;
public class MembershipService
{
private readonly IDispatcher _dispatcher;
public MembershipService(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task<WhiteboardMembershipStatus> GetMembershipStatus(Guid whiteboardId, Guid userId)
{
var query = new GetMembershipStatusQuery(whiteboardId.ToString(), userId.ToString());
return await _dispatcher.Execute(query);
}
}

View File

@@ -0,0 +1,17 @@
namespace AipsRT.Model.Users;
public class User
{
public Guid UserId { get; private set; }
public string Username { get; private set; }
public string Email { get; private set; }
public User(Guid userId, string username, string email)
{
UserId = userId;
Username = username;
Email = email;
}
}

View File

@@ -0,0 +1,23 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Models.User.Query.GetUser;
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
namespace AipsRT.Model.Users;
public class UserService
{
private readonly IDispatcher _dispatcher;
public UserService(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task<User> GetUser(Guid userId)
{
var query = new GetUserQuery(userId.ToString());
var userQueryDto = await _dispatcher.Execute(query);
return new User(Guid.Parse(userQueryDto.Id), userQueryDto.UserName, userQueryDto.Email);
}
}

View File

@@ -1,7 +1,9 @@
using AipsCore.Application.Abstract; using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT; using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT;
using AipsCore.Domain.Models.Shape.Enums; using AipsCore.Domain.Models.Shape.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsRT.Model.Whiteboard.Shapes.Map; using AipsRT.Model.Whiteboard.Shapes.Map;
using AipsRT.Model.Users;
namespace AipsRT.Model.Whiteboard; namespace AipsRT.Model.Whiteboard;
@@ -26,8 +28,15 @@ public class GetWhiteboardService
{ {
WhiteboardId = entity.Id, WhiteboardId = entity.Id,
OwnerId = entity.OwnerId, OwnerId = entity.OwnerId,
Owner = new User(entity.Owner.Id, entity.Owner.UserName!, entity.Owner.Email!),
Code = entity.Code,
}; };
foreach (var membership in entity.Memberships)
{
whiteboard.AddUser(new User(membership.UserId, membership.User!.UserName!, membership.User.Email!));
}
foreach (var shape in entity.Shapes) foreach (var shape in entity.Shapes)
{ {
switch (shape.Type) switch (shape.Type)

View File

@@ -1,13 +1,23 @@
using AipsRT.Model.Whiteboard.Shapes; using AipsRT.Model.Whiteboard.Shapes;
using AipsRT.Model.Users;
namespace AipsRT.Model.Whiteboard; namespace AipsRT.Model.Whiteboard;
public class Whiteboard public class Whiteboard
{ {
public Guid WhiteboardId { get; set; } public Guid WhiteboardId { get; set; }
public Guid OwnerId { get; set; }
public Guid OwnerId { get; set; }
public User Owner { get; set; } = null!;
public string Code {get; set;}
public List<User> Users { get; } = [];
public List<User> ActiveUsers { get; } = [];
public void AddActiveUser(User user) => ActiveUsers.Add(user);
public void RemoveActiveUser(Guid userId) => ActiveUsers.RemoveAll(u => u.UserId == userId);
public List<Shape> Shapes { get; } = []; public List<Shape> Shapes { get; } = [];
public List<Rectangle> Rectangles { get; } = []; public List<Rectangle> Rectangles { get; } = [];
@@ -20,22 +30,30 @@ public class Whiteboard
Shapes.Add(shape); Shapes.Add(shape);
Rectangles.Add(shape); Rectangles.Add(shape);
} }
public void AddArrow(Arrow shape) public void AddArrow(Arrow shape)
{ {
Shapes.Add(shape); Shapes.Add(shape);
Arrows.Add(shape); Arrows.Add(shape);
} }
public void AddLine(Line shape) public void AddLine(Line shape)
{ {
Shapes.Add(shape); Shapes.Add(shape);
Lines.Add(shape); Lines.Add(shape);
} }
public void AddTextShape(TextShape shape) public void AddTextShape(TextShape shape)
{ {
Shapes.Add(shape); Shapes.Add(shape);
TextShapes.Add(shape); TextShapes.Add(shape);
} }
public void AddUser(User user)
{
if (!Users.Contains(user))
{
Users.Add(user);
}
}
} }

View File

@@ -14,7 +14,7 @@ public class WhiteboardManager
_scopeFactory = scopeFactory; _scopeFactory = scopeFactory;
} }
public async Task AddWhiteboard(Guid whiteboardId) public async Task LoadWhiteboard(Guid whiteboardId)
{ {
var getWhiteboardService = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<GetWhiteboardService>(); var getWhiteboardService = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<GetWhiteboardService>();
var whiteboard = await getWhiteboardService.GetWhiteboard(whiteboardId); var whiteboard = await getWhiteboardService.GetWhiteboard(whiteboardId);
@@ -47,9 +47,9 @@ public class WhiteboardManager
return _userInWhiteboards[userId]; return _userInWhiteboards[userId];
} }
public void RemoveUserFromWhiteboard(Guid userId, Guid whiteboardId) public void RemoveUserFromWhiteboard(Guid userId)
{ {
_userInWhiteboards.TryRemove(whiteboardId, out _); _userInWhiteboards.TryRemove(userId, out _);
} }
public Whiteboard? GetWhiteboardForUser(Guid userId) public Whiteboard? GetWhiteboardForUser(Guid userId)

View File

@@ -1,12 +1,18 @@
using AipsCore.Application.Common.Message.ErrorMessage;
using AipsCore.Infrastructure.DI; using AipsCore.Infrastructure.DI;
using AipsRT.Hubs; using AipsRT.Hubs;
using AipsRT.Model.Memberships;
using AipsRT.Model.Users;
using AipsRT.Model.Whiteboard; using AipsRT.Model.Whiteboard;
using AipsRT.Services; using AipsRT.Services;
using AipsRT.Services.Interfaces; using AipsRT.Services.Interfaces;
using DotNetEnv; using DotNetEnv;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
Env.Load("../../.env"); if (File.Exists("../../.env"))
{
Env.Load("../../.env");
}
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -15,6 +21,13 @@ builder.Configuration.AddEnvironmentVariables();
builder.Services.AddSignalR(); builder.Services.AddSignalR();
builder.Services.AddAips(builder.Configuration); builder.Services.AddAips(builder.Configuration);
builder.Services.AddAipsMessageHandlers();
builder.Services.AddSingleton<IErrorMessageHandleStrategy, RtErrorHandleStrategy>();
builder.Services.AddHostedService<ErrorSubscriberBackgroundService>();
builder.Services.AddTransient<MembershipService>();
builder.Services.AddTransient<UserService>();
builder.Services.AddScoped<GetWhiteboardService>(); builder.Services.AddScoped<GetWhiteboardService>();
builder.Services.AddSingleton<WhiteboardManager>(); builder.Services.AddSingleton<WhiteboardManager>();

View File

@@ -0,0 +1,25 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Common.Message.ErrorMessage;
namespace AipsRT.Services;
public class ErrorSubscriberBackgroundService : BackgroundService
{
private readonly IMessageSubscriber _subscriber;
private readonly IDispatcher _dispatcher;
public ErrorSubscriberBackgroundService(IMessageSubscriber subscriber, IDispatcher dispatcher)
{
_subscriber = subscriber;
_dispatcher = dispatcher;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _subscriber.SubscribeAsync<ErrorMessage>(async (errorMessage, ct) =>
{
await _dispatcher.Execute(errorMessage, ct);
});
}
}

View File

@@ -1,5 +1,8 @@
using AipsCore.Application.Models.Shape.Command.CreateTextShape; using AipsCore.Application.Models.Shape.Command.CreateTextShape;
using AipsCore.Application.Models.Shape.Command.MoveShape; using AipsCore.Application.Models.Shape.Command.MoveShape;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
using AipsRT.Model.Whiteboard.Shapes; using AipsRT.Model.Whiteboard.Shapes;
namespace AipsRT.Services.Interfaces; namespace AipsRT.Services.Interfaces;
@@ -10,6 +13,10 @@ public interface IMessagingService
Task CreatedArrow(Guid whiteboardId, Arrow arrow); Task CreatedArrow(Guid whiteboardId, Arrow arrow);
Task CreateLine(Guid whiteboardId, Line line); Task CreateLine(Guid whiteboardId, Line line);
Task CreateTextShape(Guid whiteboardId, TextShape textShape); Task CreateTextShape(Guid whiteboardId, TextShape textShape);
Task MoveShape(MoveShapeCommand moveShape); Task MoveShape(Guid whiteboardId, MoveShapeCommand moveShape);
Task AcceptedUser(AcceptUserRequestToJoinCommand command);
Task RejectedUser(RejectUserRequestToJoinCommand command);
Task CancelJoinRequest(UserCanceledRequestToJoinCommand command);
} }

View File

@@ -1,14 +1,20 @@
using AipsCore.Application.Abstract.MessageBroking; using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Common.Message.AcceptUserRequestToJoin;
using AipsCore.Application.Common.Message.AddArrow; using AipsCore.Application.Common.Message.AddArrow;
using AipsCore.Application.Common.Message.AddLine; using AipsCore.Application.Common.Message.AddLine;
using AipsCore.Application.Common.Message.AddRectangle; using AipsCore.Application.Common.Message.AddRectangle;
using AipsCore.Application.Common.Message.AddTextShape; using AipsCore.Application.Common.Message.AddTextShape;
using AipsCore.Application.Common.Message.MoveShape; using AipsCore.Application.Common.Message.MoveShape;
using AipsCore.Application.Common.Message.RejectUserRequestToJoin;
using AipsCore.Application.Common.Message.UserCanceledRequestToJoin;
using AipsCore.Application.Models.Shape.Command.CreateArrow; using AipsCore.Application.Models.Shape.Command.CreateArrow;
using AipsCore.Application.Models.Shape.Command.CreateLine; using AipsCore.Application.Models.Shape.Command.CreateLine;
using AipsCore.Application.Models.Shape.Command.CreateRectangle; using AipsCore.Application.Models.Shape.Command.CreateRectangle;
using AipsCore.Application.Models.Shape.Command.CreateTextShape; using AipsCore.Application.Models.Shape.Command.CreateTextShape;
using AipsCore.Application.Models.Shape.Command.MoveShape; using AipsCore.Application.Models.Shape.Command.MoveShape;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.UserCanceledRequestToJoin;
using AipsRT.Model.Whiteboard.Shapes; using AipsRT.Model.Whiteboard.Shapes;
using AipsRT.Services.Interfaces; using AipsRT.Services.Interfaces;
@@ -95,9 +101,27 @@ public class MessagingService : IMessagingService
await _messagePublisher.PublishAsync(message); await _messagePublisher.PublishAsync(message);
} }
public async Task MoveShape(MoveShapeCommand moveShape) public async Task MoveShape(Guid whiteboardId, MoveShapeCommand moveShape)
{ {
var message = new MoveShapeMessage(moveShape); var message = new MoveShapeMessage(whiteboardId, moveShape);
await _messagePublisher.PublishAsync(message); await _messagePublisher.PublishAsync(message);
} }
public async Task AcceptedUser(AcceptUserRequestToJoinCommand command)
{
var message = new AcceptUserRequestToJoinMessage(command);
await _messagePublisher.PublishAsync(message);
}
public async Task RejectedUser(RejectUserRequestToJoinCommand command)
{
var message = new RejectUserRequestToJoinMessage(command);
await _messagePublisher.PublishAsync(message);
}
public async Task CancelJoinRequest(UserCanceledRequestToJoinCommand command)
{
var message = new UserCanceledRequestToJoinMessage(command);
await _messagePublisher.PublishAsync(message);
}
} }

View File

@@ -0,0 +1,36 @@
using AipsCore.Application.Common.Message.ErrorMessage;
using AipsRT.Hubs;
using AipsRT.Model.Whiteboard;
using Microsoft.AspNetCore.SignalR;
namespace AipsRT.Services;
public class RtErrorHandleStrategy : IErrorMessageHandleStrategy
{
private readonly IHubContext<WhiteboardHub> _hubContext;
private readonly WhiteboardManager _whiteboardManager;
public RtErrorHandleStrategy(IHubContext<WhiteboardHub> hubContext, WhiteboardManager whiteboardManager)
{
_hubContext = hubContext;
_whiteboardManager = whiteboardManager;
}
public async Task Handle(ErrorMessage message, CancellationToken cancellationToken)
{
var activeUsers = _whiteboardManager.GetWhiteboard(message.WhiteboardId)!.ActiveUsers;
await _whiteboardManager.LoadWhiteboard(message.WhiteboardId);
var whiteboard = _whiteboardManager.GetWhiteboard(message.WhiteboardId)!;
foreach (var user in activeUsers)
{
whiteboard.AddActiveUser(user);
}
await _hubContext.Clients
.Group(whiteboard.WhiteboardId.ToString())
.SendAsync("InitWhiteboard", whiteboard, cancellationToken);
}
}

View File

@@ -1,10 +1,10 @@
using AipsCore.Application.Abstract; using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard; using AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard;
using AipsCore.Application.Models.Whiteboard.Command.DeleteWhiteboard; using AipsCore.Application.Models.Whiteboard.Command.DeleteWhiteboard;
using AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
using AipsCore.Application.Models.Whiteboard.Query.GetRecentWhiteboards; using AipsCore.Application.Models.Whiteboard.Query.GetRecentWhiteboards;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboard; using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboard;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardHistory; using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardHistory;
using AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Whiteboard = AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard; using Whiteboard = AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard;
@@ -69,7 +69,7 @@ public class WhiteboardController : ControllerBase
[Authorize] [Authorize]
[HttpPost("join")] [HttpPost("join")]
public async Task<ActionResult> JoinWhiteboard(CreateWhiteboardMembershipCommand command, CancellationToken cancellationToken) public async Task<ActionResult<JoinWithCodeDto>> JoinWhiteboardWithCode(JoinWithCodeCommand command, CancellationToken cancellationToken)
{ {
var result = await _dispatcher.Execute(command, cancellationToken); var result = await _dispatcher.Execute(command, cancellationToken);
return Ok(result); return Ok(result);

View File

@@ -3,7 +3,10 @@ using AipsCore.Infrastructure.Persistence.Db;
using AipsWebApi.Middleware; using AipsWebApi.Middleware;
using DotNetEnv; using DotNetEnv;
Env.Load("../../.env"); if (File.Exists("../../.env"))
{
Env.Load("../../.env");
}
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);

Some files were not shown because too many files have changed in this diff Show More