Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d113872e6b | ||
463ce36c13 |
CHANGELOG.md
docs
package.jsonpublic
src
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,3 +1,26 @@
|
||||
# [1.1.0](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.4...v1.1.0) (2025-04-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **callback:** login and private route redirect ([37baf57](https://git.nostrdev.com/sigit/sigit.io/commit/37baf5709397e7099a735d6390cfceffff646f11)), closes [#229](https://git.nostrdev.com/sigit/sigit.io/issues/229)
|
||||
- **dm:** always add sigit relay when sending private DMs ([e405b73](https://git.nostrdev.com/sigit/sigit.io/commit/e405b735f7ca03e6c1fabe8ddaf4ba3e3c6cec6d))
|
||||
- **dm:** don't send private DM twice to same signer ([1474faf](https://git.nostrdev.com/sigit/sigit.io/commit/1474fafde7b76f465d895ef0457960c0704ec444))
|
||||
- hide DisplaySigit actions ([08b13c2](https://git.nostrdev.com/sigit/sigit.io/commit/08b13c291b3cae097e01416127e8674e53b59c53)), closes [#246](https://git.nostrdev.com/sigit/sigit.io/issues/246)
|
||||
- **login:** remove default login redirect ([6f4b41d](https://git.nostrdev.com/sigit/sigit.io/commit/6f4b41d84b7968286bd3aa7503afe23375e58fdf))
|
||||
- **marks:** date input ([8de86aa](https://git.nostrdev.com/sigit/sigit.io/commit/8de86aac28c38fc4fb8d34eca04e0ce50b6ab13e))
|
||||
- only send to next signer on create ([3b4bf9a](https://git.nostrdev.com/sigit/sigit.io/commit/3b4bf9aa29b337aa07e0aba9d06e89116482499c))
|
||||
- **search:** intercept nsec1, delete, and show warning ([4b5625e](https://git.nostrdev.com/sigit/sigit.io/commit/4b5625e5bd1354678bdade46ee511be187fe754b))
|
||||
- **search:** tim input, add timeout ([f7d0718](https://git.nostrdev.com/sigit/sigit.io/commit/f7d0718b7820ebc66310f572b0ceb6e91f5393bd)), closes [#308](https://git.nostrdev.com/sigit/sigit.io/issues/308)
|
||||
|
||||
### Features
|
||||
|
||||
- add private dm sending ([e85e951](https://git.nostrdev.com/sigit/sigit.io/commit/e85e9519d2a55775ddda30b3d743158e4db6cb39))
|
||||
- **create:** add local draft, save progress to local storage ([9f4a891](https://git.nostrdev.com/sigit/sigit.io/commit/9f4a891d5002532b72e9e068a85bd809491aeeef)), closes [#175](https://git.nostrdev.com/sigit/sigit.io/issues/175)
|
||||
- **draft:** serialize sigit and save/load to local storage ([13b8851](https://git.nostrdev.com/sigit/sigit.io/commit/13b88516cac858dd481ba4974509de5b35dffdb1))
|
||||
- enable pwa ([#324](https://git.nostrdev.com/sigit/sigit.io/issues/324)) ([13044d6](https://git.nostrdev.com/sigit/sigit.io/commit/13044d6b39e464b6e7de6a825b9a15858a652e4a)), closes [#93](https://git.nostrdev.com/sigit/sigit.io/issues/93)
|
||||
- **marks:** add full name ([cc681af](https://git.nostrdev.com/sigit/sigit.io/commit/cc681af11a34619ce6c41f6316b4fe9e831ff6c2))
|
||||
- **marks:** add job title and datetime ([c8f0d13](https://git.nostrdev.com/sigit/sigit.io/commit/c8f0d135f13dfea86068f7efb6c4c7152b299085))
|
||||
|
||||
## [1.0.4](https://git.nostrdev.com/sigit/sigit.io/compare/v1.0.3...v1.0.4) (2025-01-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
@ -1,181 +0,0 @@
|
||||
<mxfile host="drawio-plugin" modified="2024-12-24T14:10:11.548Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" etag="j0ni0qfsydyzknoEy44Z" version="22.1.22" type="embed">
|
||||
<diagram id="ADjf0_COJFV7FXKRQUJh" name="Page-1">
|
||||
<mxGraphModel dx="1130" dy="824" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" background="#ffffff" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="13" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="11" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="11" value="User login" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="30" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="15" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="12" target="14" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="12" value="Find all blossom servers provided by a user" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="150" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="14" value="Fetch all SIGITS from each blossom server and display them all" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="280" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="17" target="21" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="17" value="User opens a SIGIT" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="30" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="21" target="23" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21" value="Display a blossom server where this SIGIT was found" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="345" y="150" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="23" value="Display other blossom servers where SIGIT is not found, and button which will publish to a blossom server" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="345" y="250" width="120" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="270" y="380" as="sourcePoint" />
|
||||
<mxPoint x="270" y="30" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="580" y="380" as="sourcePoint" />
|
||||
<mxPoint x="580" y="30" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="28" target="29" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="28" value="Settings page (settings/servers)" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="640" y="30" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="32" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="29" target="31" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="29" value="Allow user to add and remove Blossom servers" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="665" y="150" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="31" value="Show suggested Blossom servers" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="665" y="250" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="33" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="40" y="426" as="sourcePoint" />
|
||||
<mxPoint x="820" y="426" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="34" target="38" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="34" value="User 1" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="620" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="35" value="<span style="color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;">Servers: A, B, C</span>" style="text;whiteSpace=wrap;html=1;fontSize=15;" parent="1" vertex="1">
|
||||
<mxGeometry x="70" y="570" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="36" target="41" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="36" value="User 2" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="620" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="37" value="<span style="color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;">Servers: D, E</span>" style="text;whiteSpace=wrap;html=1;fontSize=15;" parent="1" vertex="1">
|
||||
<mxGeometry x="350" y="570" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="38" target="36" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="38" value="Creates document, sign and publish to A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="770" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="44" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="41" target="43" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="41" value="Reads the files From A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="770" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="43" value="Sign, update the meta.json and publish to D, E" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="920" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="45" value="<span style="color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;">File Servers (Blossom)</span>" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
|
||||
<mxGeometry x="330" y="440" width="190" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="46" value="<span style="color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;">Sigits</span>" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="510" width="50" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="47" value="<span style="color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;">User App Data</span>" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="1050" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="52" value="Loads the page" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="49" target="51" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="49" value="User" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="1290" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="50" value="<span style="color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;">Servers: A, B, C</span>" style="text;whiteSpace=wrap;html=1;fontSize=15;" parent="1" vertex="1">
|
||||
<mxGeometry x="70" y="1240" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="61" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="51" target="60" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="51" value="Fetche the app data from A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="1450" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="64" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="58" target="63" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="58" value="List the sigits found in the app data" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="1750" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="62" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="60" target="58" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="60" value="We have 3 data sources, we merge all 3 sets into 1 object" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="1600" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="63" value="User opens a SIGIT (flow is on the top)" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="65" y="1920" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="65" value="<p style="margin:0px;margin-top:4px;text-align:center;text-decoration:underline;"><b>interface UserAppData</b></p><hr><p style="margin: 0px 0px 0px 8px; font-size: 14px;">sigits<br style="border-color: var(--border-color); text-align: center;"><span style="text-align: center;">blossomVersions</span><br style="border-color: var(--border-color); text-align: center;"><span style="text-align: center;">processedGiftWrapps</span><br style="border-color: var(--border-color); text-align: center;"><span style="text-align: center;">keyPair</span><br></p>" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="1100" width="160" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="68" value="Creates a SIGIT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="66" target="67" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="66" value="User" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1290" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="70" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="67" target="69" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67" value="Publish the SIGIT to A,B,C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1450" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="72" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="69" target="71" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="69" value="Capture the SIGIT urls and publish them in UserAppData to the servers A,B,C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1610" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="74" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="71" target="73">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="71" value="Keep last&nbsp; 10 versions of blossom which includes SIGIT" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1770" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="79" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="73" target="77">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="73" value="Every blossom version can have multiple links that user added" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="345" y="1920" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="77" value="Get latest version, if it has multiple URLs choose the first one which matches the hash" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="2060" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "sigit",
|
||||
"private": true,
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"homepage": "https://sigit.io/",
|
||||
"license": "AGPL-3.0-or-later ",
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"SIGIT_BLOSSOM": "https://blossom.sigit.io"
|
||||
}
|
17
src/App.scss
17
src/App.scss
@ -135,7 +135,7 @@ li {
|
||||
// Consistent styling for every file mark
|
||||
// Reverts some of the design defaults for font
|
||||
.file-mark {
|
||||
font-family: 'Roboto', serif;
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
letter-spacing: normal;
|
||||
@ -169,18 +169,3 @@ li {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
width: 100%;
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 15px;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -12,13 +12,6 @@ import { MarkRenderSignature } from './Render'
|
||||
export const SignatureStrategy: MarkStrategy = {
|
||||
input: MarkInputSignature,
|
||||
render: MarkRenderSignature,
|
||||
/**
|
||||
* Encrypts a stringified signature object, creates an encrypted JSON file,
|
||||
* and uploads it to a file storage if the user is online.
|
||||
* @param value
|
||||
* @param encryptionKey
|
||||
* @returns the original value string
|
||||
*/
|
||||
encryptAndUpload: async (value, encryptionKey) => {
|
||||
// Value is the stringified signature object
|
||||
// Encode it to the arrayBuffer
|
||||
@ -44,11 +37,9 @@ export const SignatureStrategy: MarkStrategy = {
|
||||
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||
|
||||
try {
|
||||
const urls = await uploadToFileStorage(file)
|
||||
console.info(
|
||||
`${file.name} uploaded to following file storages: ${urls.join(', ')}`
|
||||
)
|
||||
return value
|
||||
const url = await uploadToFileStorage(file)
|
||||
console.info(`${file.name} uploaded to file storage`)
|
||||
return url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(
|
||||
@ -89,7 +80,8 @@ export const SignatureStrategy: MarkStrategy = {
|
||||
if (arrayBuffer) {
|
||||
// decode json
|
||||
const decoder = new TextDecoder()
|
||||
return decoder.decode(arrayBuffer)
|
||||
const json = decoder.decode(arrayBuffer)
|
||||
return json
|
||||
}
|
||||
|
||||
return value
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
getUpdatedMark,
|
||||
updateCurrentUserMarks
|
||||
} from '../../utils'
|
||||
import { EMPTY } from '../../utils'
|
||||
import { EMPTY } from '../../utils/const.ts'
|
||||
import { Container } from '../Container'
|
||||
import signPageStyles from '../../pages/sign/style.module.scss'
|
||||
import { CurrentUserFile } from '../../types/file.ts'
|
||||
@ -20,18 +20,10 @@ import {
|
||||
faFileDownload,
|
||||
faPen
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Typography } from '@mui/material'
|
||||
import styles from '../UsersDetails.tsx/style.module.scss'
|
||||
|
||||
interface PdfMarkingProps {
|
||||
currentUserMarks: CurrentUserMark[]
|
||||
files: CurrentUserFile[]
|
||||
/**
|
||||
* Currently, loading spinner is present if `files` array is of length 0,
|
||||
* Which means if no files are found, loading spinner will be spinning indefinitely
|
||||
* For that reason `noFiles` is introduced to set the loading off when fetching is finished.
|
||||
*/
|
||||
noFiles?: boolean
|
||||
handleSign: () => void
|
||||
handleSignOffline: () => void
|
||||
meta: Meta | null
|
||||
@ -48,7 +40,6 @@ interface PdfMarkingProps {
|
||||
*/
|
||||
const PdfMarking = ({
|
||||
files,
|
||||
noFiles,
|
||||
currentUserMarks,
|
||||
setCurrentUserMarks,
|
||||
setUpdatedMarks,
|
||||
@ -134,18 +125,6 @@ const PdfMarking = ({
|
||||
setSelectedMarkValue(value)
|
||||
}
|
||||
|
||||
const renderRightColumn = () => {
|
||||
if (meta !== null) return <UsersDetails meta={meta} />
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
<Typography>No meta found</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className={signPageStyles.container}>
|
||||
@ -161,7 +140,7 @@ const PdfMarking = ({
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
right={renderRightColumn()}
|
||||
right={meta !== null && <UsersDetails meta={meta} />}
|
||||
leftIcon={faFileDownload}
|
||||
centerIcon={faPen}
|
||||
rightIcon={faCircleInfo}
|
||||
@ -169,30 +148,21 @@ const PdfMarking = ({
|
||||
<PdfView
|
||||
currentFile={currentFile}
|
||||
files={files}
|
||||
noFiles={noFiles}
|
||||
handleMarkClick={handleMarkClick}
|
||||
selectedMarkValue={selectedMarkValue}
|
||||
selectedMark={selectedMark}
|
||||
currentUserMarks={currentUserMarks}
|
||||
otherUserMarks={otherUserMarks}
|
||||
/>
|
||||
{noFiles && (
|
||||
<Typography textAlign="center">
|
||||
We were not able to retrieve the files.
|
||||
</Typography>
|
||||
)}
|
||||
</StickySideColumns>
|
||||
|
||||
{!noFiles && (
|
||||
<MarkFormField
|
||||
handleSubmit={handleSubmit}
|
||||
handleSelectedMarkValueChange={handleChange}
|
||||
selectedMark={selectedMark}
|
||||
selectedMarkValue={selectedMarkValue}
|
||||
currentUserMarks={currentUserMarks}
|
||||
handleCurrentUserMarkChange={handleCurrentUserMarkChange}
|
||||
/>
|
||||
)}
|
||||
<MarkFormField
|
||||
handleSubmit={handleSubmit}
|
||||
handleSelectedMarkValueChange={handleChange}
|
||||
selectedMark={selectedMark}
|
||||
selectedMarkValue={selectedMarkValue}
|
||||
currentUserMarks={currentUserMarks}
|
||||
handleCurrentUserMarkChange={handleCurrentUserMarkChange}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
|
@ -4,18 +4,12 @@ import { CurrentUserFile } from '../../types/file.ts'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { FileDivider } from '../FileDivider.tsx'
|
||||
import React from 'react'
|
||||
import { LoadingSpinner } from '../LoadingSpinner'
|
||||
import { LoadingSpinner } from '../LoadingSpinner/index.tsx'
|
||||
|
||||
interface PdfViewProps {
|
||||
currentFile: CurrentUserFile | null
|
||||
currentUserMarks: CurrentUserMark[]
|
||||
files: CurrentUserFile[]
|
||||
/**
|
||||
* Currently, loading spinner is present if `files` array is of length 0,
|
||||
* Which means if no files are found, loading spinner will be spinning indefinitely
|
||||
* For that reason `noFiles` is introduced to set the loading off when fetching is finished.
|
||||
*/
|
||||
noFiles?: boolean
|
||||
handleMarkClick: (id: number) => void
|
||||
otherUserMarks: Mark[]
|
||||
selectedMark: CurrentUserMark | null
|
||||
@ -27,7 +21,6 @@ interface PdfViewProps {
|
||||
*/
|
||||
const PdfView = ({
|
||||
files,
|
||||
noFiles,
|
||||
currentUserMarks,
|
||||
handleMarkClick,
|
||||
selectedMarkValue,
|
||||
@ -87,8 +80,6 @@ const PdfView = ({
|
||||
<FileDivider key={`separator-${i}`} />,
|
||||
curr
|
||||
])
|
||||
) : noFiles ? (
|
||||
''
|
||||
) : (
|
||||
<LoadingSpinner variant="small" />
|
||||
)}
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
faCheck,
|
||||
faClock,
|
||||
faEye,
|
||||
faServer,
|
||||
faFile,
|
||||
faFileCircleExclamation
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
@ -41,7 +40,6 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
signers,
|
||||
viewers,
|
||||
fileHashes,
|
||||
zipUrls,
|
||||
signersStatus,
|
||||
createdAt,
|
||||
completedAt,
|
||||
@ -102,18 +100,6 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to parse the base URL from Blossom server full path
|
||||
*/
|
||||
const getBaseUrl = (url: string): string => {
|
||||
try {
|
||||
const parsedUrl = new URL(url)
|
||||
return `${parsedUrl.protocol}//${parsedUrl.host}`
|
||||
} catch (error) {
|
||||
return 'Invalid URL'
|
||||
}
|
||||
}
|
||||
|
||||
return submittedBy ? (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
@ -284,20 +270,6 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
<FontAwesomeIcon icon={faFileCircleExclamation} /> —
|
||||
</>
|
||||
)}
|
||||
<span className={styles.detailsItem}>
|
||||
<FontAwesomeIcon icon={faEye} /> {signedStatus}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<p>File Servers</p>
|
||||
|
||||
{zipUrls &&
|
||||
zipUrls.map((url) => (
|
||||
<span className={styles.detailsItem} key={url}>
|
||||
<FontAwesomeIcon icon={faServer} /> {getBaseUrl(url)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : undefined
|
||||
|
@ -18,14 +18,11 @@ import {
|
||||
import { useAppDispatch, useAppSelector } from './store'
|
||||
import { useNDKContext } from './useNDKContext'
|
||||
import { useDvm } from './useDvm'
|
||||
import { getFileServerMap } from '../utils/file-servers.ts'
|
||||
import store from '../store/store.ts'
|
||||
import { setServerMapAction } from '../store/servers/action.ts'
|
||||
|
||||
export const useAuth = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { getRelayInfo } = useDvm()
|
||||
const { findMetadata, getNDKRelayList, fetchEvent } = useNDKContext()
|
||||
const { findMetadata, getNDKRelayList } = useNDKContext()
|
||||
|
||||
const authState = useAppSelector((state) => state.auth)
|
||||
const relaysState = useAppSelector((state) => state.relays)
|
||||
@ -94,44 +91,31 @@ export const useAuth = () => {
|
||||
})
|
||||
)
|
||||
|
||||
const [ndkRelayList, serverMap] = await Promise.all([
|
||||
getNDKRelayList(pubkey),
|
||||
getFileServerMap(pubkey, fetchEvent)
|
||||
])
|
||||
|
||||
const ndkRelayList = await getNDKRelayList(pubkey)
|
||||
const relays = ndkRelayList.relays
|
||||
|
||||
if (relays.length < 1) {
|
||||
// Navigate user to relays page if a relay map is empty
|
||||
// Navigate user to relays page if relay map is empty
|
||||
return appPrivateRoutes.relays
|
||||
}
|
||||
|
||||
if (Object.keys(serverMap).length < 1) {
|
||||
// Navigate user to servers page if a server map is empty
|
||||
return appPrivateRoutes.servers
|
||||
}
|
||||
|
||||
getRelayInfo(relays)
|
||||
|
||||
const relayMap = getRelayMapFromNDKRelayList(ndkRelayList)
|
||||
|
||||
if (authState.loggedIn) {
|
||||
if (!compareObjects(relaysState?.map, relayMap))
|
||||
dispatch(setRelayMapAction(relayMap))
|
||||
if (!compareObjects(store.getState().servers?.map, serverMap.map))
|
||||
dispatch(setServerMapAction(serverMap.map))
|
||||
if (authState.loggedIn && !compareObjects(relaysState?.map, relayMap)) {
|
||||
dispatch(setRelayMapAction(relayMap))
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
getNDKRelayList,
|
||||
fetchEvent,
|
||||
getRelayInfo,
|
||||
authState.loggedIn,
|
||||
findMetadata,
|
||||
relaysState?.map
|
||||
getNDKRelayList,
|
||||
getRelayInfo,
|
||||
authState,
|
||||
relaysState
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -28,7 +28,6 @@ import {
|
||||
} from '../store/actions'
|
||||
import { Keys } from '../store/auth/types'
|
||||
import {
|
||||
BlossomVersion,
|
||||
isSigitNotification,
|
||||
Meta,
|
||||
SigitNotification,
|
||||
@ -126,7 +125,7 @@ export const useNDK = () => {
|
||||
return {
|
||||
sigits: {},
|
||||
processedGiftWraps: [],
|
||||
blossomVersions: [],
|
||||
blossomUrls: [],
|
||||
keyPair: {
|
||||
private: bytesToHex(secret),
|
||||
public: pubKey
|
||||
@ -149,7 +148,6 @@ export const useNDK = () => {
|
||||
|
||||
// Parse the decrypted content
|
||||
const parsedContent = await parseJson<{
|
||||
blossomVersions: BlossomVersion[]
|
||||
blossomUrls: string[]
|
||||
keyPair: Keys
|
||||
}>(decrypted).catch((err) => {
|
||||
@ -167,23 +165,14 @@ export const useNDK = () => {
|
||||
// Return null if parsing fails
|
||||
if (!parsedContent) return null
|
||||
|
||||
// If old property blossomUrls is found, convert it to new appraoch blossomVersions
|
||||
if (parsedContent.blossomUrls) {
|
||||
parsedContent.blossomVersions = parsedContent.blossomUrls.map((url) => {
|
||||
return {
|
||||
urls: [url]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const { blossomVersions, keyPair } = parsedContent
|
||||
const { blossomUrls, keyPair } = parsedContent
|
||||
|
||||
// Return null if no blossom URLs are found
|
||||
if (blossomVersions.length === 0) return null
|
||||
if (blossomUrls.length === 0) return null
|
||||
|
||||
// Fetch additional user app data from the last blossom version urls
|
||||
// Fetch additional user app data from the first blossom URL
|
||||
const dataFromBlossom = await getUserAppDataFromBlossom(
|
||||
blossomVersions[0],
|
||||
blossomUrls[0],
|
||||
keyPair.private
|
||||
)
|
||||
|
||||
@ -194,7 +183,7 @@ export const useNDK = () => {
|
||||
|
||||
// Return the final user application data
|
||||
return {
|
||||
blossomVersions,
|
||||
blossomUrls,
|
||||
keyPair,
|
||||
sigits,
|
||||
processedGiftWraps
|
||||
@ -240,9 +229,9 @@ export const useNDK = () => {
|
||||
|
||||
if (!isUpdated) return null
|
||||
|
||||
const blossomVersions = [...appData.blossomVersions]
|
||||
const blossomUrls = [...appData.blossomUrls]
|
||||
|
||||
const newBlossomUrls = await uploadUserAppDataToBlossom(
|
||||
const newBlossomUrl = await uploadUserAppDataToBlossom(
|
||||
sigits,
|
||||
appData.processedGiftWraps,
|
||||
appData.keyPair.private
|
||||
@ -257,26 +246,18 @@ export const useNDK = () => {
|
||||
return null
|
||||
})
|
||||
|
||||
if (!newBlossomUrls) return null
|
||||
if (!newBlossomUrl) return null
|
||||
|
||||
// insert new server (blossom) urls at the start of the array
|
||||
blossomVersions.unshift({
|
||||
urls: newBlossomUrls
|
||||
})
|
||||
// Insert new blossom URL at the start of the array
|
||||
blossomUrls.unshift(newBlossomUrl)
|
||||
|
||||
// only keep last 10 blossom versions (urls), delete older ones
|
||||
// Every version can be uploaded to multiple servers
|
||||
if (blossomVersions.length > 10) {
|
||||
const versionsToDelete = blossomVersions.splice(10)
|
||||
versionsToDelete.forEach((version) => {
|
||||
for (const url of version.urls) {
|
||||
deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
|
||||
console.log(
|
||||
`An error occurred while removing an old file of user app data from the file server: ${url}`,
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
// Keep only the last 10 Blossom URLs, delete older ones
|
||||
if (blossomUrls.length > 10) {
|
||||
const filesToDelete = blossomUrls.splice(10)
|
||||
filesToDelete.forEach((url) => {
|
||||
deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
|
||||
console.log('Error removing old file from Blossom server:', err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -286,7 +267,7 @@ export const useNDK = () => {
|
||||
.nip04Encrypt(
|
||||
usersPubkey,
|
||||
JSON.stringify({
|
||||
blossomVersions: blossomVersions,
|
||||
blossomUrls,
|
||||
keyPair: appData.keyPair
|
||||
})
|
||||
)
|
||||
@ -334,7 +315,7 @@ export const useNDK = () => {
|
||||
dispatch(
|
||||
updateUserAppDataAction({
|
||||
sigits,
|
||||
blossomVersions: blossomVersions,
|
||||
blossomUrls,
|
||||
processedGiftWraps: [...appData.processedGiftWraps],
|
||||
keyPair: {
|
||||
...appData.keyPair
|
||||
@ -419,7 +400,7 @@ export const useNDK = () => {
|
||||
|
||||
try {
|
||||
meta = await fetchMetaFromFileStorage(
|
||||
notification.metaUrls,
|
||||
notification.metaUrl,
|
||||
encryptionKey
|
||||
)
|
||||
} catch (error) {
|
||||
|
@ -38,7 +38,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
}>({})
|
||||
const [markConfig, setMarkConfig] = useState<Mark[]>([])
|
||||
const [title, setTitle] = useState<string>('')
|
||||
const [zipUrls, setZipUrls] = useState<string[]>([])
|
||||
const [zipUrl, setZipUrl] = useState<string>('')
|
||||
|
||||
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
|
||||
[signer: `npub1${string}`]: DocSignatureEvent
|
||||
@ -83,15 +83,15 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
setId(id)
|
||||
setSig(sig)
|
||||
|
||||
const { title, signers, viewers, fileHashes, markConfig, zipUrls } =
|
||||
await parseCreateSignatureEventContent(content)
|
||||
const { title, signers, viewers, fileHashes, markConfig, zipUrl } =
|
||||
parseCreateSignatureEventContent(content)
|
||||
|
||||
setTitle(title)
|
||||
setSigners(signers)
|
||||
setViewers(viewers)
|
||||
setFileHashes(fileHashes)
|
||||
setMarkConfig(markConfig)
|
||||
setZipUrls(zipUrls)
|
||||
setZipUrl(zipUrl)
|
||||
|
||||
let encryptionKey: string | undefined
|
||||
if (meta.keys) {
|
||||
@ -272,7 +272,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
fileHashes,
|
||||
markConfig,
|
||||
title,
|
||||
zipUrls,
|
||||
zipUrl,
|
||||
parsedSignatureEvents,
|
||||
completedAt,
|
||||
signedStatus,
|
||||
|
@ -18,8 +18,7 @@ store.subscribe(
|
||||
saveState({
|
||||
auth: store.getState().auth,
|
||||
user: store.getState().user,
|
||||
relays: store.getState().relays,
|
||||
servers: store.getState().servers
|
||||
relays: store.getState().relays
|
||||
})
|
||||
}, 1000)
|
||||
)
|
||||
|
@ -50,8 +50,7 @@ import {
|
||||
clearSigitDraft,
|
||||
saveSigitDraft,
|
||||
getSigitDraft,
|
||||
timeout,
|
||||
isValidNip05
|
||||
timeout
|
||||
} from '../../utils'
|
||||
import { Container } from '../../components/Container'
|
||||
import fileListStyles from '../../components/FileList/style.module.scss'
|
||||
@ -268,7 +267,8 @@ export const CreatePage = () => {
|
||||
if (!foundUsers.length) {
|
||||
const searchTerm = userSearchInput.trim()
|
||||
// If it's NIP05 (includes @ or is a valid domain) send request to .well-known
|
||||
if (isValidNip05(userSearchInput)) {
|
||||
const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/
|
||||
if (searchTerm.startsWith('_@') || domainRegex.test(searchTerm)) {
|
||||
setSearchUsersLoading(true)
|
||||
|
||||
const pubkey = await handleSearchUserNip05(searchTerm)
|
||||
@ -800,10 +800,10 @@ export const CreatePage = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
// Upload the file to the storage/s
|
||||
const uploadFiles = async (
|
||||
// Upload the file to the storage
|
||||
const uploadFile = async (
|
||||
arrayBuffer: ArrayBuffer
|
||||
): Promise<string[] | null> => {
|
||||
): Promise<string | null> => {
|
||||
const blob = new Blob([arrayBuffer])
|
||||
// Create a File object with the Blob data
|
||||
const file = new File([blob], `compressed-${unixNow()}.sigit`, {
|
||||
@ -838,14 +838,14 @@ export const CreatePage = () => {
|
||||
fileHashes: {
|
||||
[key: string]: string
|
||||
},
|
||||
zipUrls: string[]
|
||||
zipUrl: string
|
||||
) => {
|
||||
const content: CreateSignatureEventContent = {
|
||||
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
||||
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
||||
fileHashes,
|
||||
markConfig,
|
||||
zipUrls,
|
||||
zipUrl,
|
||||
title
|
||||
}
|
||||
|
||||
@ -930,14 +930,14 @@ export const CreatePage = () => {
|
||||
)
|
||||
|
||||
setLoadingSpinnerDesc('Uploading files.zip to file storage')
|
||||
const fileUrls = await uploadFiles(encryptedArrayBuffer)
|
||||
if (!fileUrls) return
|
||||
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
||||
if (!fileUrl) return
|
||||
|
||||
setLoadingSpinnerDesc('Generating create signature')
|
||||
const createSignature = await generateCreateSignature(
|
||||
markConfig,
|
||||
fileHashes,
|
||||
fileUrls
|
||||
fileUrl
|
||||
)
|
||||
if (!createSignature) return
|
||||
|
||||
@ -973,11 +973,11 @@ export const CreatePage = () => {
|
||||
const event = await updateUsersAppData([meta])
|
||||
if (!event) return
|
||||
|
||||
const metaUrls = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
|
||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
||||
const promises = sendNotifications({
|
||||
metaUrls,
|
||||
metaUrl,
|
||||
keys: meta.keys
|
||||
})
|
||||
|
||||
@ -1046,7 +1046,7 @@ export const CreatePage = () => {
|
||||
const createSignature = await generateCreateSignature(
|
||||
markConfig,
|
||||
fileHashes,
|
||||
[]
|
||||
''
|
||||
)
|
||||
if (!createSignature) return
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle'
|
||||
import RouterIcon from '@mui/icons-material/Router'
|
||||
import StorageIcon from '@mui/icons-material/Storage'
|
||||
import { Button } from '@mui/material'
|
||||
import { useAppSelector } from '../../hooks'
|
||||
import { useAppSelector } from '../../hooks/store'
|
||||
import { NavLink, Outlet, To } from 'react-router-dom'
|
||||
import { appPrivateRoutes } from '../../routes'
|
||||
import { Container } from '../../components/Container'
|
||||
@ -66,7 +65,6 @@ export const SettingsLayout = () => {
|
||||
'Profile'
|
||||
)}
|
||||
{Item(appPrivateRoutes.relays, <RouterIcon />, 'Relays')}
|
||||
{Item(appPrivateRoutes.servers, <StorageIcon />, 'Servers')}
|
||||
{loginMethod === LoginMethod.nostrLogin &&
|
||||
Item(
|
||||
appPrivateRoutes.nostrLogin,
|
||||
|
@ -26,7 +26,6 @@ import {
|
||||
compareObjects,
|
||||
getRelayMapFromNDKRelayList,
|
||||
hexToNpub,
|
||||
isValidRelayUri,
|
||||
publishRelayMap,
|
||||
shorten,
|
||||
timeout
|
||||
@ -197,7 +196,12 @@ export const RelaysPage = () => {
|
||||
const relayURI = `${webSocketPrefix}${newRelayURI?.trim().replace(webSocketPrefix, '')}`
|
||||
|
||||
// Check if new relay URI is a valid string
|
||||
if (relayURI && !isValidRelayUri(relayURI)) {
|
||||
if (
|
||||
relayURI &&
|
||||
!/^wss?:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
||||
relayURI
|
||||
)
|
||||
) {
|
||||
if (relayURI !== webSocketPrefix) {
|
||||
setNewRelayURIerror(
|
||||
'New relay URI is not valid. Example of valid relay URI: wss://sigit.relay.io'
|
||||
|
@ -1,264 +0,0 @@
|
||||
import styles from './style.module.scss'
|
||||
import { Container } from '../../../components/Container'
|
||||
import { Footer } from '../../../components/Footer/Footer.tsx'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
InputAdornment,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import StorageIcon from '@mui/icons-material/Storage'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { FileServerMap, KeyboardCode } from '../../../types'
|
||||
import {
|
||||
getFileServerMap,
|
||||
publishFileServer
|
||||
} from '../../../utils/file-servers.ts'
|
||||
import { useAppSelector, useNDKContext } from '../../../hooks'
|
||||
import { useDidMount } from '../../../hooks'
|
||||
import { isValidUrl, MAXIMUM_BLOSSOMS_LENGTH } from '../../../utils'
|
||||
import axios from 'axios'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const protocol = 'https://'
|
||||
|
||||
const errors = {
|
||||
urlNotValid:
|
||||
'New server URL is not valid. Example of valid server URL: blossom.sigit.io'
|
||||
}
|
||||
|
||||
export const ServersPage = () => {
|
||||
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
|
||||
|
||||
const [newServerURL, setNewServerURL] = useState<string>('')
|
||||
const [newRelayURLerror, setNewRelayURLerror] = useState<string>()
|
||||
const [loadingServers, setLoadingServers] = useState<boolean>(true)
|
||||
|
||||
const [blossomServersMap, setBlossomServersMap] = useState<FileServerMap>({})
|
||||
|
||||
const { ndk, fetchEvent, publish } = useNDKContext()
|
||||
|
||||
useDidMount(() => {
|
||||
fetchFileServers()
|
||||
})
|
||||
|
||||
const fetchFileServers = async () => {
|
||||
if (usersPubkey) {
|
||||
const servers = await getFileServerMap(usersPubkey, fetchEvent)
|
||||
|
||||
if (servers.map) {
|
||||
if (Object.keys(servers.map).length === 0) {
|
||||
serverRequirementWarning()
|
||||
}
|
||||
|
||||
setBlossomServersMap(servers.map)
|
||||
}
|
||||
} else {
|
||||
noUserKeyWarning()
|
||||
}
|
||||
|
||||
setLoadingServers(false)
|
||||
}
|
||||
|
||||
const noUserKeyWarning = () => toast.warning('No user key available.')
|
||||
|
||||
const serverRequirementWarning = () =>
|
||||
toast.warning('At least one Blossom server is needed for SIGit to work.')
|
||||
|
||||
const handleAddNewServer = async () => {
|
||||
if (!newServerURL.length) {
|
||||
setNewRelayURLerror(errors.urlNotValid)
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(blossomServersMap).length === MAXIMUM_BLOSSOMS_LENGTH) {
|
||||
return toast.warning(
|
||||
`You can only add a maximum of ${MAXIMUM_BLOSSOMS_LENGTH} blossom servers.`
|
||||
)
|
||||
}
|
||||
|
||||
const serverURL = `${protocol}${newServerURL?.trim().replace(protocol, '')}`
|
||||
if (!serverURL) return
|
||||
|
||||
// Check if new server is a valid URL
|
||||
if (!isValidUrl(serverURL)) {
|
||||
if (serverURL !== protocol) {
|
||||
setNewRelayURLerror(errors.urlNotValid)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(blossomServersMap).includes(serverURL))
|
||||
return toast.warning('This server is already added.')
|
||||
|
||||
const valid = await validateFileServer(serverURL)
|
||||
if (!valid)
|
||||
return toast.warning(
|
||||
`Server URL ${serverURL} does not seem to be a valid file server.`
|
||||
)
|
||||
|
||||
setNewRelayURLerror('')
|
||||
const tempBlossomServersMap = blossomServersMap
|
||||
tempBlossomServersMap[serverURL] = { write: true, read: true }
|
||||
setBlossomServersMap(tempBlossomServersMap)
|
||||
setNewServerURL('')
|
||||
|
||||
publishFileServersList(tempBlossomServersMap)
|
||||
}
|
||||
|
||||
const handleDeleteServer = (serverURL: string) => {
|
||||
if (Object.keys(blossomServersMap).length === 1)
|
||||
return serverRequirementWarning()
|
||||
|
||||
// Remove server from the list
|
||||
const tempBlossomServersMap = cloneDeep(blossomServersMap)
|
||||
delete tempBlossomServersMap[serverURL]
|
||||
|
||||
setBlossomServersMap(tempBlossomServersMap)
|
||||
// Publish new list to the relays
|
||||
publishFileServersList(tempBlossomServersMap)
|
||||
}
|
||||
|
||||
const publishFileServersList = (fileServersMap: FileServerMap) => {
|
||||
if (!usersPubkey)
|
||||
return toast.warning(
|
||||
'No user key available, please reload and try again.'
|
||||
)
|
||||
|
||||
publishFileServer(fileServersMap, usersPubkey, ndk, publish)
|
||||
.then((res) => {
|
||||
toast.success(res)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
const handleInputKeydown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
event.code === KeyboardCode.Enter ||
|
||||
event.code === KeyboardCode.NumpadEnter
|
||||
) {
|
||||
event.preventDefault()
|
||||
handleAddNewServer()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file server is up and valid
|
||||
* For now check will just include sending a GET request and checking if
|
||||
* returned HTML includes word `Blossom`.
|
||||
*
|
||||
* Probably later, there will be appropriate sepc universal to all file servers
|
||||
* which would include some kind of "check" endpoint.
|
||||
* @param serverURL
|
||||
*/
|
||||
const validateFileServer = (serverURL: string) => {
|
||||
return new Promise((resolve) => {
|
||||
axios
|
||||
.get(serverURL)
|
||||
.then((res) => {
|
||||
if (res && res.data?.toLowerCase().includes('blossom server')) {
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error validating file server.', err)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className={`settings-container ${styles.container}`}>
|
||||
<Box className={styles.serverAddContainer}>
|
||||
<TextField
|
||||
label="Add new blossom server"
|
||||
value={newServerURL}
|
||||
onKeyDown={handleInputKeydown}
|
||||
onChange={(e) => setNewServerURL(e.target.value)}
|
||||
helperText={newRelayURLerror}
|
||||
error={!!newRelayURLerror}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">{protocol}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
className={styles.serverURItextfield}
|
||||
/>
|
||||
<Button variant="contained" onClick={() => handleAddNewServer()}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
<Box className={styles.sectionTitle}>
|
||||
<StorageIcon className={styles.sectionIcon} />
|
||||
<span>YOUR BLOSSOM SERVERS</span>
|
||||
</Box>
|
||||
|
||||
{loadingServers && (
|
||||
<div className="text-center">
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{blossomServersMap && (
|
||||
<Box className={styles.serversContainer}>
|
||||
{Object.keys(blossomServersMap).map((key) => (
|
||||
<ServerItem
|
||||
key={key}
|
||||
serverURL={key}
|
||||
preventDelete={Object.keys(blossomServersMap).length === 1}
|
||||
handleDeleteServer={handleDeleteServer}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Footer />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
interface ServerItemProps {
|
||||
serverURL: string
|
||||
preventDelete?: boolean
|
||||
handleDeleteServer?: (serverURL: string) => void
|
||||
}
|
||||
|
||||
const ServerItem = ({
|
||||
serverURL,
|
||||
handleDeleteServer,
|
||||
preventDelete
|
||||
}: ServerItemProps) => {
|
||||
return (
|
||||
<Box className={styles.server}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<span
|
||||
className={[
|
||||
styles.connectionStatus,
|
||||
styles.connectionStatusConnected
|
||||
].join(' ')}
|
||||
/>
|
||||
|
||||
<ListItemText primary={serverURL} />
|
||||
|
||||
<Box
|
||||
onClick={() => handleDeleteServer && handleDeleteServer(serverURL)}
|
||||
className={`${styles.leaveServerContainer} ${preventDelete ? styles.disabled : ''}`}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<span>Remove</span>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
@import '../../../styles/colors.scss';
|
||||
|
||||
.container {
|
||||
color: $text-color;
|
||||
|
||||
.serverURItextfield {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.serverAddContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sectionIcon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin-top: 35px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.serversContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.server {
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
|
||||
.relayDivider {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.leaveServerContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.showInfo {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.showInfoIcon {
|
||||
margin-right: 3px;
|
||||
margin-bottom: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.relayInfoContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.relayInfoTitle {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.relayInfoSubTitle {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.copyItem {
|
||||
margin-left: 10px;
|
||||
color: #34495e;
|
||||
vertical-align: bottom;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.connectionStatus {
|
||||
border-radius: 9999px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.connectionStatusConnected {
|
||||
background-color: $relay-status-connected;
|
||||
}
|
||||
|
||||
.connectionStatusNotConnected {
|
||||
background-color: $relay-status-notconnected;
|
||||
}
|
||||
|
||||
.connectionStatusUnknown {
|
||||
background-color: $input-text-color;
|
||||
}
|
||||
}
|
||||
}
|
@ -78,7 +78,6 @@ export const SignPage = () => {
|
||||
}
|
||||
|
||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||
const [noFiles, setNoFiles] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
@ -219,54 +218,26 @@ export const SignPage = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const { zipUrls, encryptionKey } = res
|
||||
const { zipUrl, encryptionKey } = res
|
||||
|
||||
if (!zipUrls || zipUrls.length === 0) {
|
||||
toast.warning('No zip files found in the zipUrls')
|
||||
setIsLoading(false)
|
||||
setNoFiles(true)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* We start iterating through all URLs and fetch the zip. If zip is unreachable,
|
||||
* or it fails the hash check, we skip to the next one. Iteration will stop
|
||||
* on the first successful zip, so that's why we do it sequentially.
|
||||
*/
|
||||
for (let i = 0; i < zipUrls.length; i++) {
|
||||
const zipUrl = zipUrls[i]
|
||||
const isLastZipUrl = i === zipUrls.length - 1
|
||||
|
||||
setLoadingSpinnerDesc('Fetching file from file server')
|
||||
|
||||
const res = await axios
|
||||
.get(zipUrl, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`error occurred in getting file from ${zipUrls}`,
|
||||
err
|
||||
)
|
||||
toast.error(
|
||||
err.message || `error occurred in getting file from ${zipUrls}`
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
if (res) {
|
||||
setLoadingSpinnerDesc('Fetching file from file server')
|
||||
axios
|
||||
.get(zipUrl, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
.then((res) => {
|
||||
handleArrayBufferFromBlossom(res.data, encryptionKey)
|
||||
setMeta(metaInNavState)
|
||||
break
|
||||
} else {
|
||||
// No data returned, break from the loop
|
||||
if (isLastZipUrl) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`error occurred in getting file from ${zipUrl}`, err)
|
||||
toast.error(
|
||||
err.message || `error occurred in getting file from ${zipUrl}`
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
processSigit()
|
||||
@ -407,10 +378,6 @@ export const SignPage = () => {
|
||||
setMeta(parsedMetaJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the signing process
|
||||
* When user signs, files will automatically be published to all user preferred servers
|
||||
*/
|
||||
const initializeSigning = async (type: 'online' | 'offline') => {
|
||||
if (Object.entries(files).length === 0 || !meta) return
|
||||
|
||||
@ -583,9 +550,9 @@ export const SignPage = () => {
|
||||
return
|
||||
}
|
||||
|
||||
let metaUrls: string[]
|
||||
let metaUrl: string
|
||||
try {
|
||||
metaUrls = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
@ -627,10 +594,7 @@ export const SignPage = () => {
|
||||
setLoadingSpinnerDesc('Sending notifications')
|
||||
const users = Array.from(userSet)
|
||||
const promises = users.map((user) =>
|
||||
sendNotification(npubToHex(user)!, {
|
||||
metaUrls: metaUrls,
|
||||
keys: meta.keys
|
||||
})
|
||||
sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys })
|
||||
)
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
@ -751,7 +715,6 @@ export const SignPage = () => {
|
||||
handleSignOffline={handleSignOffline}
|
||||
otherUserMarks={otherUserMarks}
|
||||
meta={meta}
|
||||
noFiles={noFiles}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -65,12 +65,6 @@ import { SignerService } from '../../services/index.ts'
|
||||
|
||||
interface PdfViewProps {
|
||||
files: CurrentUserFile[]
|
||||
/**
|
||||
* Currently, loading spinner is present if `files` array is of length 0,
|
||||
* Which means if no files are found, loading spinner will be spinning indefinitely
|
||||
* For that reason `noFiles` is introduced to set the loading off when fetching is finished.
|
||||
*/
|
||||
noFiles?: boolean
|
||||
currentFile: CurrentUserFile | null
|
||||
parsedSignatureEvents: {
|
||||
[signer: `npub1${string}`]: DocSignatureEvent
|
||||
@ -79,7 +73,6 @@ interface PdfViewProps {
|
||||
|
||||
const SlimPdfView = ({
|
||||
files,
|
||||
noFiles,
|
||||
currentFile,
|
||||
parsedSignatureEvents
|
||||
}: PdfViewProps) => {
|
||||
@ -172,8 +165,6 @@ const SlimPdfView = ({
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
) : noFiles ? (
|
||||
''
|
||||
) : (
|
||||
<LoadingSpinner variant="small" />
|
||||
)}
|
||||
@ -223,7 +214,7 @@ export const VerifyPage = () => {
|
||||
|
||||
const {
|
||||
submittedBy,
|
||||
zipUrls,
|
||||
zipUrl,
|
||||
encryptionKey,
|
||||
signers,
|
||||
viewers,
|
||||
@ -384,7 +375,7 @@ export const VerifyPage = () => {
|
||||
const users = Array.from(userSet)
|
||||
const promises = users.map((user) =>
|
||||
sendNotification(npubToHex(user)!, {
|
||||
metaUrls: metaUrl,
|
||||
metaUrl,
|
||||
keys: meta.keys!
|
||||
})
|
||||
)
|
||||
@ -411,64 +402,35 @@ export const VerifyPage = () => {
|
||||
const processSigit = async () => {
|
||||
setIsLoading(true)
|
||||
|
||||
// We have multiple zipUrls, we should fetch one by one and take the first one which successfully decrypts
|
||||
// If a file is altered decrytption will fail
|
||||
setLoadingSpinnerDesc('Fetching file from file server')
|
||||
try {
|
||||
const res = await axios.get(zipUrl, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
|
||||
if (!zipUrls || zipUrls.length === 0) {
|
||||
toast.warning('No zip files found in the zipUrls')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
const fileName = zipUrl.split('/').pop()
|
||||
const file = new File([res.data], fileName!)
|
||||
|
||||
for (let i = 0; i < zipUrls.length; i++) {
|
||||
const zipUrl = zipUrls[i]
|
||||
const isLastZipUrl = i === zipUrls.length - 1
|
||||
const encryptedArrayBuffer = await file.arrayBuffer()
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer,
|
||||
encryptionKey
|
||||
).catch((err) => {
|
||||
console.log('err in decryption:>> ', err)
|
||||
toast.error(err.message || 'An error occurred in decrypting file.')
|
||||
return null
|
||||
})
|
||||
|
||||
try {
|
||||
// Fetch zip data
|
||||
const res = await axios.get(zipUrl, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
|
||||
// Prepare file from response
|
||||
const fileName = zipUrl.split('/').pop()
|
||||
const file = new File([res.data], fileName!)
|
||||
const encryptedArrayBuffer = await file.arrayBuffer()
|
||||
|
||||
// Decrypt the array buffer
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer,
|
||||
encryptionKey
|
||||
).catch((err) => {
|
||||
const error = err.message
|
||||
? `Decryption error: ${err.message}`
|
||||
: 'An error occurred in decrypting file.'
|
||||
console.error('Error in decryption:>> ', err)
|
||||
toast.error(error)
|
||||
return null // Continue iteration for next zipUrl
|
||||
})
|
||||
|
||||
if (!arrayBuffer) {
|
||||
if (!isLastZipUrl) continue // Skip to next zipUrl if decryption fails
|
||||
break // If last zipUrl break out of loop
|
||||
}
|
||||
|
||||
// Load zip archive
|
||||
if (arrayBuffer) {
|
||||
const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => {
|
||||
console.error('Error in loading zip file :>> ', err)
|
||||
console.log('err in loading zip file :>> ', err)
|
||||
toast.error(
|
||||
err.message || 'An error occurred in loading zip file.'
|
||||
)
|
||||
return null // Skip to next zipUrl
|
||||
return null
|
||||
})
|
||||
|
||||
if (!zip) {
|
||||
if (!isLastZipUrl) continue // Skip to next zipUrl
|
||||
// If it's the last zip url, and still no `zip` found, break out of loop,
|
||||
// it means no files were successfully fetched or all files failed the validation (hash check).
|
||||
break
|
||||
}
|
||||
if (!zip) return
|
||||
|
||||
const files: { [fileName: string]: SigitFile } = {}
|
||||
const fileHashes: { [key: string]: string | null } = {}
|
||||
@ -476,43 +438,47 @@ export const VerifyPage = () => {
|
||||
(entry) => entry.name
|
||||
)
|
||||
|
||||
// Generate hashes for all entries in the files folder of zipArchive
|
||||
for (const entryFileName of fileNames) {
|
||||
const entryArrayBuffer = await readContentOfZipEntry(
|
||||
// generate hashes for all entries in files folder of zipArchive
|
||||
// these hashes can be used to verify the originality of files
|
||||
for (const fileName of fileNames) {
|
||||
const arrayBuffer = await readContentOfZipEntry(
|
||||
zip,
|
||||
entryFileName,
|
||||
fileName,
|
||||
'arraybuffer'
|
||||
)
|
||||
if (entryArrayBuffer) {
|
||||
files[entryFileName] = await convertToSigitFile(
|
||||
entryArrayBuffer,
|
||||
entryFileName
|
||||
)
|
||||
|
||||
fileHashes[entryFileName.replace(/^files\//, '')] =
|
||||
await getHash(entryArrayBuffer)
|
||||
if (arrayBuffer) {
|
||||
files[fileName] = await convertToSigitFile(
|
||||
arrayBuffer,
|
||||
fileName!
|
||||
)
|
||||
const hash = await getHash(arrayBuffer)
|
||||
|
||||
if (hash) {
|
||||
fileHashes[fileName.replace(/^files\//, '')] = hash
|
||||
}
|
||||
} else {
|
||||
fileHashes[entryFileName.replace(/^files\//, '')] = null
|
||||
fileHashes[fileName.replace(/^files\//, '')] = null
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentFileHashes(fileHashes)
|
||||
setFiles(files)
|
||||
setIsLoading(false)
|
||||
} catch (err) {
|
||||
const message = `error occurred in getting file from ${zipUrl}`
|
||||
console.error(message, err)
|
||||
if (err instanceof Error) toast.error(err.message)
|
||||
else toast.error(message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
} catch (err) {
|
||||
const message = `error occurred in getting file from ${zipUrl}`
|
||||
console.error(message, err)
|
||||
if (err instanceof Error) toast.error(err.message)
|
||||
else toast.error(message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
processSigit()
|
||||
}
|
||||
}, [encryptionKey, metaInNavState, zipUrls])
|
||||
}, [encryptionKey, metaInNavState, zipUrl])
|
||||
|
||||
const handleVerify = useCallback(async (selectedFile: File) => {
|
||||
setIsLoading(true)
|
||||
@ -779,9 +745,9 @@ export const VerifyPage = () => {
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('storing meta on blossom server')
|
||||
|
||||
let metaUrls: string[]
|
||||
let metaUrl: string
|
||||
try {
|
||||
metaUrls = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
@ -823,7 +789,7 @@ export const VerifyPage = () => {
|
||||
setLoadingSpinnerDesc('Sending notifications')
|
||||
const users = Array.from(userSet)
|
||||
const promises = users.map((user) =>
|
||||
sendNotification(npubToHex(user)!, { metaUrls, keys: meta.keys })
|
||||
sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys })
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
@ -899,15 +865,7 @@ export const VerifyPage = () => {
|
||||
currentFile={currentFile}
|
||||
files={getCurrentUserFiles(files, currentFileHashes, fileHashes)}
|
||||
parsedSignatureEvents={parsedSignatureEvents}
|
||||
noFiles={!zipUrls || zipUrls.length === 0}
|
||||
/>
|
||||
{!zipUrls || zipUrls.length === 0 ? (
|
||||
<Typography textAlign="center">
|
||||
We were not able to retrieve the files.
|
||||
</Typography>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</StickySideColumns>
|
||||
)}
|
||||
</Container>
|
||||
|
@ -6,7 +6,6 @@ export const appPrivateRoutes = {
|
||||
sign: '/sign',
|
||||
profileSettings: '/settings/profile',
|
||||
relays: '/settings/relays',
|
||||
servers: '/settings/servers',
|
||||
nostrLogin: '/settings/nostrLogin'
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ import { SettingsLayout } from '../pages/settings/Settings'
|
||||
import { SignPage } from '../pages/sign'
|
||||
import { VerifyPage } from '../pages/verify'
|
||||
import { PrivateRoute } from './PrivateRoute'
|
||||
import { ServersPage } from '../pages/settings/servers'
|
||||
|
||||
/**
|
||||
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
||||
@ -108,10 +107,6 @@ export const privateRoutes: CustomRouteProps<unknown>[] = [
|
||||
path: appPrivateRoutes.relays,
|
||||
element: <RelaysPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.servers,
|
||||
element: <ServersPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.nostrLogin,
|
||||
element: <NostrLoginPage />
|
||||
|
@ -1,54 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { ILocalConfig } from '../../types/config.ts'
|
||||
|
||||
class LocalConfig {
|
||||
// Static property to hold the single instance of LocalCache
|
||||
private static instance: LocalConfig | null = null
|
||||
|
||||
private config: ILocalConfig
|
||||
|
||||
// Private constructor to prevent direct instantiation
|
||||
private constructor() {
|
||||
// Set default config
|
||||
this.config = {
|
||||
SIGIT_BLOSSOM: 'https://blossom.sigit.io'
|
||||
}
|
||||
}
|
||||
|
||||
// Method to initialize the database
|
||||
private async init() {
|
||||
axios
|
||||
.get<ILocalConfig>('/config.json')
|
||||
.then((response) => {
|
||||
console.log('response', response)
|
||||
|
||||
if (typeof response.data === 'object') {
|
||||
this.config = response.data
|
||||
} else {
|
||||
throw 'Failed to load config.json: File not found'
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load config.json:', error)
|
||||
console.warn('Default config will be used.')
|
||||
})
|
||||
}
|
||||
|
||||
// Static method to get the single instance of LocalCache
|
||||
public static getInstance(): LocalConfig {
|
||||
// If the instance doesn't exist, create it
|
||||
if (!LocalConfig.instance) {
|
||||
LocalConfig.instance = new LocalConfig()
|
||||
LocalConfig.instance.init()
|
||||
}
|
||||
// Return the single instance of LocalCache
|
||||
return LocalConfig.instance
|
||||
}
|
||||
|
||||
public getConfig() {
|
||||
return this.config
|
||||
}
|
||||
}
|
||||
|
||||
// Export the single instance of LocalCache
|
||||
export const localConfig = LocalConfig.getInstance()
|
@ -11,9 +11,6 @@ export const SET_USER_PROFILE = 'SET_USER_PROFILE'
|
||||
|
||||
export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE'
|
||||
|
||||
export const SET_SERVER_MAP = 'SET_SERVER_MAP'
|
||||
export const SET_SERVER_MAP_UPDATED = 'SET_SERVER_MAP_UPDATED'
|
||||
|
||||
export const SET_RELAY_MAP = 'SET_RELAY_MAP'
|
||||
export const SET_RELAY_INFO = 'SET_RELAY_INFO'
|
||||
export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED'
|
||||
|
@ -9,14 +9,11 @@ import { RelaysDispatchTypes, RelaysState } from './relays/types'
|
||||
import UserAppDataReducer from './userAppData/reducer'
|
||||
import { UserAppDataDispatchTypes } from './userAppData/types'
|
||||
import { UserDispatchTypes, UserState } from './user/types'
|
||||
import { ServersDispatchTypes, FileServersState } from './servers/types.ts'
|
||||
import serversReducer from './servers/reducer'
|
||||
|
||||
export interface State {
|
||||
auth: AuthState
|
||||
user: UserState
|
||||
relays: RelaysState
|
||||
servers: FileServersState
|
||||
userAppData?: UserAppData
|
||||
}
|
||||
|
||||
@ -24,14 +21,12 @@ type AppActions =
|
||||
| AuthDispatchTypes
|
||||
| UserDispatchTypes
|
||||
| RelaysDispatchTypes
|
||||
| ServersDispatchTypes
|
||||
| UserAppDataDispatchTypes
|
||||
|
||||
export const appReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
user: userReducer,
|
||||
relays: relaysReducer,
|
||||
servers: serversReducer,
|
||||
userAppData: UserAppDataReducer
|
||||
})
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
import * as ActionTypes from '../actionTypes'
|
||||
import { SetServerMapAction, SetServerMapUpdatedAction } from './types'
|
||||
import { ServerMap } from '../../types'
|
||||
|
||||
export const setServerMapAction = (payload: ServerMap): SetServerMapAction => ({
|
||||
type: ActionTypes.SET_SERVER_MAP,
|
||||
payload
|
||||
})
|
||||
|
||||
export const setServerMapUpdatedAction = (): SetServerMapUpdatedAction => ({
|
||||
type: ActionTypes.SET_SERVER_MAP_UPDATED
|
||||
})
|
@ -1,28 +0,0 @@
|
||||
import * as ActionTypes from '../actionTypes'
|
||||
import { ServersDispatchTypes, FileServersState } from './types'
|
||||
|
||||
const initialState: FileServersState = {
|
||||
map: undefined,
|
||||
mapUpdated: undefined
|
||||
}
|
||||
|
||||
const reducer = (
|
||||
state = initialState,
|
||||
action: ServersDispatchTypes
|
||||
): FileServersState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_SERVER_MAP:
|
||||
return { ...state, map: action.payload, mapUpdated: Date.now() }
|
||||
|
||||
case ActionTypes.SET_SERVER_MAP_UPDATED:
|
||||
return { ...state, mapUpdated: Date.now() }
|
||||
|
||||
case ActionTypes.RESTORE_STATE:
|
||||
return action.payload.servers || initialState
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
@ -1,22 +0,0 @@
|
||||
import * as ActionTypes from '../actionTypes'
|
||||
import { RestoreState } from '../actions'
|
||||
import { ServerMap } from '../../types'
|
||||
|
||||
export type FileServersState = {
|
||||
map?: ServerMap
|
||||
mapUpdated?: number
|
||||
}
|
||||
|
||||
export interface SetServerMapAction {
|
||||
type: typeof ActionTypes.SET_SERVER_MAP
|
||||
payload: ServerMap
|
||||
}
|
||||
|
||||
export interface SetServerMapUpdatedAction {
|
||||
type: typeof ActionTypes.SET_SERVER_MAP_UPDATED
|
||||
}
|
||||
|
||||
export type ServersDispatchTypes =
|
||||
| SetServerMapAction
|
||||
| SetServerMapUpdatedAction
|
||||
| RestoreState
|
@ -5,7 +5,7 @@ import { UserAppDataDispatchTypes } from './types'
|
||||
const initialState: UserAppData = {
|
||||
sigits: {},
|
||||
processedGiftWraps: [],
|
||||
blossomVersions: []
|
||||
blossomUrls: []
|
||||
}
|
||||
|
||||
const reducer = (
|
||||
|
@ -1,3 +0,0 @@
|
||||
export interface ILocalConfig {
|
||||
SIGIT_BLOSSOM: string
|
||||
}
|
@ -28,7 +28,7 @@ export interface CreateSignatureEventContent {
|
||||
fileHashes: { [key: string]: string }
|
||||
markConfig: Mark[]
|
||||
title: string
|
||||
zipUrls: string[]
|
||||
zipUrl: string
|
||||
}
|
||||
|
||||
export interface SignedEventContent {
|
||||
@ -71,14 +71,9 @@ export interface UserAppData {
|
||||
*/
|
||||
keyPair?: Keys
|
||||
/**
|
||||
* Array for storing Urls for the files which stores all sigits and processedGiftWraps on file servers (blossom).
|
||||
* We keep the last 10 versions
|
||||
* Array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom.
|
||||
*/
|
||||
blossomVersions: BlossomVersion[]
|
||||
}
|
||||
|
||||
export interface BlossomVersion {
|
||||
urls: string[]
|
||||
blossomUrls: string[]
|
||||
}
|
||||
|
||||
export interface DocSignatureEvent extends Event {
|
||||
@ -86,12 +81,12 @@ export interface DocSignatureEvent extends Event {
|
||||
}
|
||||
|
||||
export interface SigitNotification {
|
||||
metaUrls: string[]
|
||||
metaUrl: string
|
||||
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||
}
|
||||
|
||||
export function isSigitNotification(obj: unknown): obj is SigitNotification {
|
||||
return typeof (obj as SigitNotification).metaUrls === 'object'
|
||||
return typeof (obj as SigitNotification).metaUrl === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,8 +6,7 @@ export enum MetaStorageErrorType {
|
||||
'FETCH_FAILED' = 'Fetching meta.json requires an encryption key.',
|
||||
'HASH_VERIFICATION_FAILED' = 'Unable to verify meta.json.',
|
||||
'DECRYPTION_FAILED' = 'Error decryping meta.json.',
|
||||
'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.',
|
||||
'NO_URLS_PROCESSED_SUCCESSFULLY' = 'No URLs were available to process.'
|
||||
'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.'
|
||||
}
|
||||
|
||||
export class MetaStorageError extends Error {
|
||||
|
@ -1,18 +0,0 @@
|
||||
/**
|
||||
* We are using a map instead of an array because the future plan is to have
|
||||
* read and write file servers, so this model is prepared for that nostr feature.
|
||||
*/
|
||||
export type FileServerMap = {
|
||||
[key: string]: {
|
||||
read: boolean
|
||||
write: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileServerPutResponse {
|
||||
sha256: string
|
||||
size: number
|
||||
uploaded: number
|
||||
type: string
|
||||
url: string
|
||||
}
|
@ -6,5 +6,3 @@ export * from './zip'
|
||||
export * from './event'
|
||||
export * from './drawing'
|
||||
export * from './draft'
|
||||
export * from './server'
|
||||
export * from './file-server.ts'
|
||||
|
@ -1,6 +0,0 @@
|
||||
export type ServerMap = {
|
||||
[key: string]: {
|
||||
read: boolean
|
||||
write: boolean
|
||||
}
|
||||
}
|
@ -1,12 +1,7 @@
|
||||
import { localConfig } from '../services/config'
|
||||
import { ILocalConfig } from '../types/config.ts'
|
||||
|
||||
export const EMPTY: string = ''
|
||||
export const ARRAY_BUFFER = 'arraybuffer'
|
||||
export const DEFLATE = 'DEFLATE'
|
||||
|
||||
const config: ILocalConfig = localConfig.getConfig()
|
||||
|
||||
/**
|
||||
* Number of milliseconds in one week.
|
||||
*/
|
||||
@ -19,7 +14,7 @@ export const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
export const SIGIT_RELAY = 'wss://relay.sigit.io'
|
||||
|
||||
export const SIGIT_BLOSSOM = config.SIGIT_BLOSSOM
|
||||
export const SIGIT_BLOSSOM = 'https://blossom.sigit.io'
|
||||
|
||||
export const DEFAULT_LOOK_UP_RELAY_LIST = [
|
||||
SIGIT_RELAY,
|
||||
@ -27,8 +22,6 @@ export const DEFAULT_LOOK_UP_RELAY_LIST = [
|
||||
'wss://purplepag.es'
|
||||
]
|
||||
|
||||
export const MAXIMUM_BLOSSOMS_LENGTH = 3
|
||||
|
||||
// Uses the https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types list
|
||||
// Updated on 2024/08/22
|
||||
export const MOST_COMMON_MEDIA_TYPES = new Map([
|
||||
|
@ -1,114 +0,0 @@
|
||||
import { FileServerMap } from '../types'
|
||||
import { NostrController } from '../controllers'
|
||||
import { SIGIT_BLOSSOM } from './const.ts'
|
||||
import { unixNow } from './nostr.ts'
|
||||
import { Filter, UnsignedEvent, kinds } from 'nostr-tools'
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
NDKSubscriptionOptions
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
|
||||
/**
|
||||
* Fetches the relays to get preferred file servers for the given npub
|
||||
* @param npub hex pubkey
|
||||
* @param fetchEvent provided by Nostr dev kit
|
||||
*/
|
||||
const getFileServerMap = async (
|
||||
npub: string,
|
||||
fetchEvent: (
|
||||
filter: NDKFilter,
|
||||
opts?: NDKSubscriptionOptions | undefined
|
||||
) => Promise<NDKEvent | null>
|
||||
): Promise<{ map: FileServerMap; mapUpdated?: number }> => {
|
||||
try {
|
||||
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/96.md
|
||||
const eventFilter: Filter = {
|
||||
kinds: [kinds.FileServerPreference],
|
||||
authors: [npub]
|
||||
}
|
||||
|
||||
const event = await fetchEvent(eventFilter)
|
||||
|
||||
if (event) {
|
||||
// Handle found event 10096
|
||||
const fileServersMap: FileServerMap = {}
|
||||
|
||||
const serverTags = event.tags.filter((tag) => tag[0] === 'server')
|
||||
|
||||
serverTags.forEach((tag) => {
|
||||
const url = tag[1]
|
||||
const serverType = tag[2]
|
||||
|
||||
// if 3rd element of server tag is undefined, server is WRITE and READ
|
||||
fileServersMap[url] = {
|
||||
write: serverType ? serverType === 'write' : true,
|
||||
read: serverType ? serverType === 'read' : true
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.resolve({
|
||||
map: fileServersMap,
|
||||
mapUpdated: event.created_at
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve({ map: getDefaultFileServerMap() })
|
||||
}
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes a preferred file servers list for the given npub
|
||||
* @param serverMap list of preferred servers
|
||||
* @param npub hex pubkey
|
||||
* @param ndk provided by Nostr dev kit
|
||||
* @param publish provided by Nostr dev kit
|
||||
*/
|
||||
const publishFileServer = async (
|
||||
serverMap: FileServerMap,
|
||||
npub: string,
|
||||
ndk: NDK,
|
||||
publish: (event: NDKEvent) => Promise<string[]>
|
||||
): Promise<string> => {
|
||||
const timestamp = unixNow()
|
||||
const serverURLs = Object.keys(serverMap)
|
||||
|
||||
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||
const tags: string[][] = serverURLs.map((serverURL) => {
|
||||
const serverTag = ['server', serverURL]
|
||||
|
||||
return serverTag.filter((value) => value !== '')
|
||||
})
|
||||
|
||||
const newRelayMapEvent: UnsignedEvent = {
|
||||
kind: kinds.FileServerPreference,
|
||||
tags,
|
||||
content: '',
|
||||
pubkey: npub,
|
||||
created_at: timestamp
|
||||
}
|
||||
|
||||
const nostrController = NostrController.getInstance()
|
||||
const signedEvent = await nostrController.signEvent(newRelayMapEvent)
|
||||
|
||||
const ndkEvent = new NDKEvent(ndk, signedEvent)
|
||||
const publishResult = await publish(ndkEvent)
|
||||
|
||||
if (publishResult && publishResult.length) {
|
||||
return Promise.resolve(
|
||||
`Preferred file servers published on: ${publishResult.join('\n')}`
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
'Publishing updated preferred file servers was unsuccessful.'
|
||||
)
|
||||
}
|
||||
|
||||
const getDefaultFileServerMap = (): FileServerMap => ({
|
||||
[SIGIT_BLOSSOM]: { write: true, read: true }
|
||||
})
|
||||
|
||||
export { getFileServerMap, publishFileServer }
|
@ -169,72 +169,47 @@ export const uploadMetaToFileStorage = async (
|
||||
|
||||
// Create the encrypted json file from array buffer and hash
|
||||
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||
const urls = await uploadToFileStorage(file)
|
||||
const url = await uploadToFileStorage(file)
|
||||
|
||||
return urls
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the meta from one or more file storages, one by one, and it will take the first one, which has matching hash
|
||||
* @param urls urls of meta files
|
||||
* @param encryptionKey
|
||||
*/
|
||||
export const fetchMetaFromFileStorage = async (
|
||||
urls: string[],
|
||||
url: string,
|
||||
encryptionKey: string | undefined
|
||||
): Promise<Meta> => {
|
||||
) => {
|
||||
if (!encryptionKey) {
|
||||
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
|
||||
}
|
||||
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i]
|
||||
const isLastUrl = i === urls.length - 1
|
||||
const encryptedArrayBuffer = await axios.get(url, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
const encryptedArrayBuffer = await axios.get(url, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
|
||||
// Verify hash
|
||||
const parts = url.split('/')
|
||||
const urlHash = parts[parts.length - 1]
|
||||
const hash = await getHash(encryptedArrayBuffer.data)
|
||||
if (hash !== urlHash) {
|
||||
// If no more urls left to try and hash check failed, throw an error
|
||||
if (isLastUrl)
|
||||
throw new MetaStorageError(
|
||||
MetaStorageErrorType.HASH_VERIFICATION_FAILED
|
||||
)
|
||||
// Otherwise, skip to the next url to fetch
|
||||
continue
|
||||
}
|
||||
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer.data,
|
||||
encryptionKey
|
||||
).catch((err) => {
|
||||
if (isLastUrl) {
|
||||
throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
|
||||
cause: err
|
||||
})
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
if (arrayBuffer) {
|
||||
// Decode meta.json and parse
|
||||
const decoder = new TextDecoder()
|
||||
const json = decoder.decode(arrayBuffer)
|
||||
const meta = await parseJson<Meta>(json)
|
||||
return meta
|
||||
} else if (!isLastUrl) {
|
||||
continue
|
||||
}
|
||||
|
||||
throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
|
||||
// Verify hash
|
||||
const parts = url.split('/')
|
||||
const urlHash = parts[parts.length - 1]
|
||||
const hash = await getHash(encryptedArrayBuffer.data)
|
||||
if (hash !== urlHash) {
|
||||
throw new MetaStorageError(MetaStorageErrorType.HASH_VERIFICATION_FAILED)
|
||||
}
|
||||
|
||||
throw new MetaStorageError(
|
||||
MetaStorageErrorType.NO_URLS_PROCESSED_SUCCESSFULLY
|
||||
)
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer.data,
|
||||
encryptionKey
|
||||
).catch((err) => {
|
||||
throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
|
||||
cause: err
|
||||
})
|
||||
})
|
||||
|
||||
if (arrayBuffer) {
|
||||
// Decode meta.json and parse
|
||||
const decoder = new TextDecoder()
|
||||
const json = decoder.decode(arrayBuffer)
|
||||
const meta = await parseJson<Meta>(json)
|
||||
return meta
|
||||
}
|
||||
|
||||
throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Event,
|
||||
EventTemplate,
|
||||
@ -11,11 +11,7 @@ import {
|
||||
import { toast } from 'react-toastify'
|
||||
import { NostrController } from '../controllers'
|
||||
import store from '../store/store'
|
||||
import {
|
||||
CreateSignatureEventContent,
|
||||
FileServerPutResponse,
|
||||
Meta
|
||||
} from '../types'
|
||||
import { CreateSignatureEventContent, Meta } from '../types'
|
||||
import { hexToNpub, unixNow } from './nostr'
|
||||
import { parseJson } from './string'
|
||||
import { hexToBytes } from '@noble/hashes/utils'
|
||||
@ -23,23 +19,18 @@ import { getHash } from './hash.ts'
|
||||
import { SIGIT_BLOSSOM } from './const.ts'
|
||||
|
||||
/**
|
||||
* Uploads a file to one or more file storage services.
|
||||
* Uploads a file to a file storage service.
|
||||
* @param blob The Blob object representing the file to upload.
|
||||
* @param nostrController The NostrController instance for handling authentication.
|
||||
* @returns The array of URLs of the uploaded file.
|
||||
* @returns The URL of the uploaded file.
|
||||
*/
|
||||
export const uploadToFileStorage = async (file: File): Promise<string[]> => {
|
||||
export const uploadToFileStorage = async (file: File) => {
|
||||
// Define event metadata for authorization
|
||||
const hash = await getHash(await file.arrayBuffer())
|
||||
if (!hash) {
|
||||
throw new Error("Can't get file hash.")
|
||||
}
|
||||
|
||||
const preferredServersMap = store.getState().servers.map || {}
|
||||
const preferredServers = Object.keys(preferredServersMap)
|
||||
// If no servers found, use SIGIT as fallback
|
||||
if (!preferredServers.length) preferredServers.push(SIGIT_BLOSSOM)
|
||||
|
||||
const event: EventTemplate = {
|
||||
kind: 24242,
|
||||
content: 'Authorize Upload',
|
||||
@ -63,28 +54,16 @@ export const uploadToFileStorage = async (file: File): Promise<string[]> => {
|
||||
// Sign the authorization event using the dedicated key stored in user app data
|
||||
const authEvent = finalizeEvent(event, hexToBytes(key))
|
||||
|
||||
const uploadPromises: Promise<AxiosResponse<FileServerPutResponse>>[] = []
|
||||
// Upload the file to the file storage service using Axios
|
||||
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
|
||||
headers: {
|
||||
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
|
||||
'Content-Type': 'application/sigit' // Set content type header
|
||||
}
|
||||
})
|
||||
|
||||
// Upload the file to the file storage services using Axios
|
||||
for (const preferredServer of preferredServers) {
|
||||
const uploadPromise = axios.put<FileServerPutResponse>(
|
||||
`${preferredServer}/upload`,
|
||||
file,
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
|
||||
'Content-Type': 'application/sigit' // Set content type header
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
uploadPromises.push(uploadPromise)
|
||||
}
|
||||
|
||||
const responses = await Promise.all(uploadPromises)
|
||||
|
||||
// Return the URLs of the uploaded files
|
||||
return responses.map((response) => response.data.url) as string[]
|
||||
// Return the URL of the uploaded file
|
||||
return response.data.url as string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -249,7 +228,7 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
|
||||
if (!createSignatureContent) return null
|
||||
|
||||
// Extract the ZIP URL from the create signature content
|
||||
const zipUrls = createSignatureContent.zipUrls
|
||||
const zipUrl = createSignatureContent.zipUrl
|
||||
|
||||
// Retrieve the user's public key from the state
|
||||
const usersPubkey = store.getState().auth.usersPubkey!
|
||||
@ -280,7 +259,7 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
|
||||
return {
|
||||
createSignatureEvent,
|
||||
createSignatureContent,
|
||||
zipUrls: zipUrls,
|
||||
zipUrl,
|
||||
encryptionKey: decrypted
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk'
|
||||
import { hexToBytes } from '@noble/hashes/utils'
|
||||
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk'
|
||||
import axios from 'axios'
|
||||
import { truncate } from 'lodash'
|
||||
import {
|
||||
@ -16,18 +16,13 @@ import {
|
||||
nip44,
|
||||
verifyEvent
|
||||
} from 'nostr-tools'
|
||||
import { toast } from 'react-toastify'
|
||||
import { NIP05_REGEX } from '../constants'
|
||||
import store from '../store/store'
|
||||
import {
|
||||
Meta,
|
||||
SignedEvent,
|
||||
FileServerPutResponse,
|
||||
BlossomVersion
|
||||
} from '../types'
|
||||
import { Meta, SignedEvent } from '../types'
|
||||
import { SIGIT_BLOSSOM } from './const.ts'
|
||||
import { getHash } from './hash'
|
||||
import { parseJson, removeLeadingSlash } from './string'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
/**
|
||||
* Generates a `d` tag for userAppData
|
||||
@ -359,7 +354,7 @@ export const deleteBlossomFile = async (url: string, privateKey: string) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to upload user application data to the user preferred File (Blossom) servers.
|
||||
* Function to upload user application data to the Blossom server.
|
||||
* @param sigits - An object containing metadata for the user application data.
|
||||
* @param processedGiftWraps - An array of processed gift wrap IDs.
|
||||
* @param privateKey - The private key used for encryption.
|
||||
@ -370,11 +365,6 @@ export const uploadUserAppDataToBlossom = async (
|
||||
processedGiftWraps: string[],
|
||||
privateKey: string
|
||||
) => {
|
||||
const preferredServersMap = store.getState().servers.map || {}
|
||||
const preferredServers = Object.keys(preferredServersMap)
|
||||
// If no servers found, use SIGIT as fallback
|
||||
if (!preferredServers.length) preferredServers.push(SIGIT_BLOSSOM)
|
||||
|
||||
// Create an object containing the sigits and processed gift wraps
|
||||
const obj = {
|
||||
sigits,
|
||||
@ -421,44 +411,33 @@ export const uploadUserAppDataToBlossom = async (
|
||||
// Finalize the event with the private key
|
||||
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
||||
|
||||
// Upload the file to the file storage services using Axios
|
||||
const responses = await Promise.all(
|
||||
preferredServers.map((preferredServer) => {
|
||||
return axios.put<FileServerPutResponse>(
|
||||
`${preferredServer}/upload`,
|
||||
file,
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
// Upload the file to the file storage service using Axios
|
||||
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
|
||||
headers: {
|
||||
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
|
||||
}
|
||||
})
|
||||
|
||||
// Return the URLs of the uploaded files
|
||||
return responses.map((response) => response.data.url) as string[]
|
||||
// Return the URL of the uploaded file
|
||||
return response.data.url as string
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to retrieve and decrypt user application data from file (Blossom) servers.
|
||||
* Since we pull from multiple servers, we will take the first one
|
||||
* @param blossomVersion - The URL to fetch the encrypted data from.
|
||||
* Function to retrieve and decrypt user application data from Blossom server.
|
||||
* @param url - The URL to fetch the encrypted data from.
|
||||
* @param privateKey - The private key used for decryption.
|
||||
* @returns A promise that resolves to the decrypted and parsed user application data.
|
||||
*/
|
||||
export const getUserAppDataFromBlossom = async (
|
||||
blossomVersion: BlossomVersion,
|
||||
url: string,
|
||||
privateKey: string
|
||||
) => {
|
||||
// Initialize errorCode to track HTTP error codes
|
||||
let errorCode = 0
|
||||
|
||||
const blossomUrl = blossomVersion.urls[0]
|
||||
|
||||
// Fetch the encrypted data from the provided URL
|
||||
const encrypted = await axios
|
||||
.get(blossomUrl, {
|
||||
.get(url, {
|
||||
responseType: 'blob' // Expect a blob response
|
||||
})
|
||||
.then(async (res) => {
|
||||
@ -470,13 +449,8 @@ export const getUserAppDataFromBlossom = async (
|
||||
})
|
||||
.catch((err) => {
|
||||
// Log and display an error message if the request fails
|
||||
console.error(
|
||||
`error occurred in getting file from ${blossomVersion}`,
|
||||
err
|
||||
)
|
||||
toast.error(
|
||||
err.message || `error occurred in getting file from ${blossomVersion}`
|
||||
)
|
||||
console.error(`error occurred in getting file from ${url}`, err)
|
||||
toast.error(err.message || `error occurred in getting file from ${url}`)
|
||||
|
||||
// Set errorCode to the HTTP status code if available
|
||||
if (err.request) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { TimeoutError } from '../types/errors/TimeoutError.ts'
|
||||
import { CurrentUserFile } from '../types/file.ts'
|
||||
import { SigitFile } from './file.ts'
|
||||
import { NIP05_REGEX } from '../constants.ts'
|
||||
|
||||
export const debounceCustom = <T extends (...args: never[]) => void>(
|
||||
fn: T,
|
||||
@ -144,25 +143,3 @@ export const isPromiseRejected = <T>(
|
||||
): result is PromiseRejectedResult => {
|
||||
return result.status === 'rejected'
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if it's valid {protocol}{domain}
|
||||
* @param url
|
||||
*/
|
||||
export const isValidUrl = (url: string) => {
|
||||
return /^(https?:\/\/)(?!-)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}$/gim.test(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if it's a valid domain or nip05 format
|
||||
* @param value
|
||||
*/
|
||||
export const isValidNip05 = (value: string) => {
|
||||
return NIP05_REGEX.test(value)
|
||||
}
|
||||
|
||||
export const isValidRelayUri = (value: string) => {
|
||||
return /^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
||||
value
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user