Compare commits

...

69 Commits

Author SHA1 Message Date
c611d03341 revert 2026-03-11 00:44:30 +01:00
db3038e181 rabbitmq env 2026-03-11 00:38:03 +01:00
241ee19e1c expose rabbitmq 2026-03-11 00:31:08 +01:00
b39391dc90 test tab removal 2026-03-11 00:07:41 +01:00
665a3c0dfa join loading indicator 2026-03-11 00:05:19 +01:00
eb50f45822 join fix 2026-03-10 23:57:24 +01:00
914f4ac97b fixed join 2026-03-10 23:46:58 +01:00
02fcfba5f1 fixed username frontend 2026-03-10 23:32:00 +01:00
be10d1896b Merge pull request #53 from StewKI/fix-error-handling-on-frontend
Fix error handling on frontend
2026-03-10 23:07:31 +01:00
0ed4f9545c Successful signup also calls login right after 2026-03-10 22:53:15 +01:00
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
Andrija Stevanović
94ec4e7135 Merge pull request #43 from StewKI/hotfix-recent-bugs
Fixing recent bugs
2026-02-21 18:43:16 +01:00
ff7c0d58a7 text shape selection movement fix 2026-02-21 18:41:47 +01:00
3610e0cb7d arrow movement fix 2026-02-21 18:31:37 +01:00
7030e76c03 Merge pull request #42 from StewKI/feature-deleting-whiteboards-and-code-in-toolbar
Feature deleting whiteboards and code in toolbar
2026-02-19 14:14:31 +01:00
83db13ef53 Refactoring 2026-02-19 14:13:34 +01:00
7177446470 Whiteboard code in toolbar in Whiteboard view 2026-02-19 00:25:12 +01:00
2a3e971f41 Deleting whiteboards 2026-02-19 00:24:46 +01:00
258e190bb2 bootstrap icons 2026-02-19 00:23:23 +01:00
7f4a7c034f Queries to filter out deleted whiteboards 2026-02-19 00:22:39 +01:00
c99aaa1062 Soft deleting whiteboards 2026-02-19 00:21:55 +01:00
145 changed files with 3215 additions and 679 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"]

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

@@ -0,0 +1,105 @@
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
ports:
- "15672:15672"
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>
<Folder Include="Application\Models\Shape\Command\DeleteShape\" />
<Folder Include="Domain\Models\WhiteboardMembership\Validation\" />
<Folder Include="Infrastructure\Persistence\Db\Migrations\" />
</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.Models.Shape.Command.CreateArrow;
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.Models.Shape.Command.CreateLine;
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.Models.Shape.Command.CreateRectangle;
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.Models.Shape.Command.CreateTextShape;
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.Models.Shape.Command.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.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;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.BanUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class BanUserFromWhiteboardCommandHandler : ICommandHandler<BanUserFromWh
if (whiteboard is null)
{
throw new ValidationException(BanUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId));
throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(BanUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId));
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
whiteboard.BanUser(_userContext.GetCurrentUserId(), membership);

View File

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

View File

@@ -0,0 +1,48 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Abstract.Validation;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.DeleteWhiteboard;
public class DeleteWhiteboardCommandHandler : ICommandHandler<DeleteWhiteboardCommand>
{
private readonly IUserContext _userContext;
private readonly IWhiteboardRepository _whiteboardRepository;
private readonly IUnitOfWork _unitOfWork;
public DeleteWhiteboardCommandHandler(
IUserContext userContext,
IWhiteboardRepository whiteboardRepository,
IUnitOfWork unitOfWork)
{
_userContext = userContext;
_whiteboardRepository = whiteboardRepository;
_unitOfWork = unitOfWork;
}
public async Task Handle(DeleteWhiteboardCommand command, CancellationToken cancellationToken = default)
{
var whiteboardId = new WhiteboardId(command.WhiteboardId);
var userId = _userContext.GetCurrentUserId();
var whiteboard = await _whiteboardRepository.GetByIdAsync(whiteboardId, cancellationToken);
if (whiteboard is null)
{
throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
}
if (!whiteboard.CanUserDelete(userId))
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanDeleteWhiteboard(userId));
}
await _whiteboardRepository.SoftDeleteAsync(whiteboardId, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}

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.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;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.KickUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class KickUserFromWhiteboardCommandHandler : ICommandHandler<KickUserFrom
if (whiteboard is null)
{
throw new ValidationException(KickUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId));
throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(KickUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId));
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
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.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;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.UnbanUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class UnbanUserFromWhiteboardCommandHandler : ICommandHandler<UnbanUserFr
if (whiteboard is null)
{
throw new ValidationException(UnbanUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId));
throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(UnbanUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId));
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
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,5 +1,7 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore;
@@ -31,8 +33,9 @@ public class GetRecentWhiteboardsQueryHandler : IQueryHandler<GetRecentWhiteboar
.Include(m => m.Whiteboard)
.Where(m => (
m.UserId == userIdGuid &&
m.IsBanned == false &&
m.Whiteboard != null
m.Status != WhiteboardMembershipStatus.Banned &&
m.Whiteboard != null &&
m.Whiteboard.State != WhiteboardState.Deleted
))
.OrderByDescending(m => m.LastInteractedAt)
.Select(m => m.Whiteboard!);

View File

@@ -1,5 +1,6 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore;
@@ -22,7 +23,7 @@ public class GetWhiteboardHistoryQueryHandler
var userIdGuid = new Guid(_userContext.GetCurrentUserId().IdValue);
return await _context.Whiteboards
.Where(w => w.OwnerId == userIdGuid)
.Where(w => w.OwnerId == userIdGuid && w.State != WhiteboardState.Deleted)
.ToListAsync(cancellationToken);
}
}

View File

@@ -34,6 +34,7 @@ public class GetWhiteboardInfoRTQueryHandler
return _context.Whiteboards
.Where(w => w.Id == whiteboardId)
.Include(w => w.Memberships)
.ThenInclude(m => m.User)
.Include(w => w.Owner)
.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

@@ -0,0 +1,8 @@
using AipsCore.Domain.Common.ValueObjects;
namespace AipsCore.Domain.Abstract;
public interface ISoftDeletableRepository<in TId> where TId : DomainId
{
Task SoftDeleteAsync(TId id, CancellationToken cancellationToken = default);
}

View File

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

View File

@@ -3,7 +3,9 @@ using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Domain.Models.Whiteboard.External;
public interface IWhiteboardRepository : IAbstractRepository<Whiteboard, WhiteboardId>
public interface IWhiteboardRepository
: 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 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)
{
string code = "user_already_added";
@@ -38,4 +70,12 @@ public class WhiteboardErrors : AbstractErrors<Whiteboard, WhiteboardId>
return CreateValidationError(code, message);
}
public static ValidationError OnlyOwnerCanDeleteWhiteboard(UserId currentUserId)
{
string code = "only_owner_can_delete_whiteboard";
string message = $"Only owner of whiteboard can delete the whiteboard. Current user id: '{currentUserId}' is not the owner.";
return CreateValidationError(code, message);
}
}

View File

@@ -36,7 +36,7 @@ public record WhiteboardCode : AbstractValueObject
{
whiteboardCode = Generate();
codeExists = await whiteboardRepository.WhiteboardCodeExists(whiteboardCode);
codeExists = await whiteboardRepository.WhiteboardCodeExistsAsync(whiteboardCode);
} while (codeExists);
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.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard : DomainModel<WhiteboardId>
public partial class Whiteboard
{
public async Task AddUserAsync(
User.User user,
IWhiteboardMembershipRepository membershipRepository,
CancellationToken cancellationToken = default)
public WhiteboardMembership.WhiteboardMembership RequestJoin(UserId userId)
{
var membership
= await membershipRepository.GetByWhiteboardAndUserAsync(this.Id, user.Id, cancellationToken);
if (membership is not null)
{
throw new ValidationException(WhiteboardErrors.UserAlreadyAdded(user.Id));
}
membership = WhiteboardMembership.WhiteboardMembership.Create(
this.Id.IdValue,
user.Id.IdValue,
return WhiteboardMembership.WhiteboardMembership.Create(
Id.IdValue,
userId.IdValue,
false,
false,
this.GetCanJoin(),
DateTime.UtcNow
);
await membershipRepository.AddAsync(membership, cancellationToken);
DetermineJoinStatus(),
DateTime.UtcNow);
}
private bool GetCanJoin()
public void RequestReJoin(WhiteboardMembership.WhiteboardMembership membership)
{
return this.JoinPolicy == WhiteboardJoinPolicy.FreeToJoin;
switch (membership.Status)
{
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;
}
}
private WhiteboardMembershipStatus DetermineJoinStatus()
{
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

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

View File

@@ -10,12 +10,10 @@ namespace AipsCore.Infrastructure.Authentication.AuthService;
public class EfAuthService : IAuthService
{
private readonly AipsDbContext _dbContext;
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;
}
@@ -27,8 +25,8 @@ public class EfAuthService : IAuthService
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
throw new Exception($"User registration failed: {errors}");
var validationErrors = result.Errors.Select(e => new ValidationError(e.Code, e.Description)).ToList();
throw new ValidationException(validationErrors);
}
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);
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Authentication.RefreshToken.RefreshToken", b =>
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -214,18 +214,15 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("CanJoin")
.HasColumnType("boolean");
b.Property<bool>("EditingEnabled")
.HasColumnType("boolean");
b.Property<bool>("IsBanned")
.HasColumnType("boolean");
b.Property<DateTime>("LastInteractedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
@@ -371,7 +368,7 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
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")
.WithMany()

View File

@@ -1,3 +1,4 @@
using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Infrastructure.Persistence.Abstract;
@@ -57,8 +58,27 @@ public class WhiteboardRepository
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);
}
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)
{
var entity = await Context.Whiteboards.FindAsync([new Guid(id.IdValue)], cancellationToken);
if (entity != null)
{
entity.State = WhiteboardState.Deleted;
entity.DeletedAt = DateTime.UtcNow;
Context.Whiteboards.Update(entity);
}
}
}

View File

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

View File

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

View File

@@ -1,6 +1,12 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Common.Message.MoveShape;
using AipsCore.Application.Abstract;
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.Shapes;
using AipsRT.Model.Whiteboard.Structs;
@@ -15,33 +21,147 @@ public class WhiteboardHub : Hub
{
private readonly WhiteboardManager _whiteboardManager;
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;
_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)
{
if (!_whiteboardManager.WhiteboardExists(whiteboardId))
await _whiteboardManager.AddWhiteboard(whiteboardId);
{
await _whiteboardManager.LoadWhiteboard(whiteboardId);
}
await Groups.AddToGroupAsync(Context.ConnectionId, whiteboardId.ToString());
var whiteboard = _whiteboardManager.GetWhiteboard(whiteboardId)!;
var userId = CurrentUserId;
var ownerId = whiteboard.OwnerId;
WhiteboardMembershipStatus status;
if (userId == ownerId)
{
status = WhiteboardMembershipStatus.Accepted;
}
else
{
status = await _membershipService.GetMembershipStatus(whiteboardId, userId);
}
_whiteboardManager.AddUserToWhiteboard(userId, whiteboardId);
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)!;
_whiteboardManager.AddUserToWhiteboard(Guid.Parse(Context.UserIdentifier!), whiteboardId);
await Clients.Caller.SendAsync("InitWhiteboard", state);
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
.SendAsync("Joined", Context.UserIdentifier!);
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);
whiteboard.AddUser(user);
}
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.Group(whiteboard.WhiteboardId.ToString()).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)
{
var userId = CurrentUserId;
_whiteboardManager.RemoveUserFromWhiteboard(userId);
_whiteboardManager.GetWhiteboard(whiteboardId)?.RemoveActiveUser(userId);
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
.SendAsync("Leaved", Context.UserIdentifier!);
}
private Guid CurrentUserId => Guid.Parse(Context.UserIdentifier!);
@@ -127,6 +247,6 @@ public class WhiteboardHub : Hub
{
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.Models.Whiteboard.Query.GetWhiteboardInfoRT;
using AipsCore.Domain.Models.Shape.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsRT.Model.Whiteboard.Shapes.Map;
using AipsRT.Model.Users;
namespace AipsRT.Model.Whiteboard;
@@ -26,8 +28,15 @@ public class GetWhiteboardService
{
WhiteboardId = entity.Id,
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)
{
switch (shape.Type)

View File

@@ -10,7 +10,7 @@ public class Arrow : Shape
public override void Move(Position newPosition)
{
var difference = newPosition - EndPosition;
var difference = newPosition - Position;
EndPosition += difference;
base.Move(newPosition);
}

View File

@@ -10,7 +10,7 @@ public class Line : Shape
public override void Move(Position newPosition)
{
var difference = newPosition - EndPosition;
var difference = newPosition - Position;
EndPosition += difference;
base.Move(newPosition);
}

View File

@@ -1,4 +1,5 @@
using AipsRT.Model.Whiteboard.Shapes;
using AipsRT.Model.Users;
namespace AipsRT.Model.Whiteboard;
@@ -7,6 +8,15 @@ public class Whiteboard
public Guid WhiteboardId { 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; } = [];
@@ -38,4 +48,12 @@ public class Whiteboard
Shapes.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;
}
public async Task AddWhiteboard(Guid whiteboardId)
public async Task LoadWhiteboard(Guid whiteboardId)
{
var getWhiteboardService = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<GetWhiteboardService>();
var whiteboard = await getWhiteboardService.GetWhiteboard(whiteboardId);
@@ -47,9 +47,9 @@ public class WhiteboardManager
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)

View File

@@ -1,12 +1,18 @@
using AipsCore.Application.Common.Message.ErrorMessage;
using AipsCore.Infrastructure.DI;
using AipsRT.Hubs;
using AipsRT.Model.Memberships;
using AipsRT.Model.Users;
using AipsRT.Model.Whiteboard;
using AipsRT.Services;
using AipsRT.Services.Interfaces;
using DotNetEnv;
using Microsoft.AspNetCore.SignalR;
if (File.Exists("../../.env"))
{
Env.Load("../../.env");
}
var builder = WebApplication.CreateBuilder(args);
@@ -15,6 +21,13 @@ builder.Configuration.AddEnvironmentVariables();
builder.Services.AddSignalR();
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.AddSingleton<WhiteboardManager>();

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