feat: publish and read from relays a list of preferred file servers, run validation while adding new server
This commit is contained in:
parent
092bb98670
commit
31d1630ab1
106
docs/blossom-flow.drawio
Normal file
106
docs/blossom-flow.drawio
Normal file
@ -0,0 +1,106 @@
|
||||
<mxfile host="drawio-plugin" modified="2024-12-16T12:18:54.992Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" etag="AV-hpNJbeaKzNFoRwzLd" version="22.1.22" type="embed">
|
||||
<diagram id="ADjf0_COJFV7FXKRQUJh" name="Page-1">
|
||||
<mxGraphModel dx="942" dy="687" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" 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;" edge="1" parent="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;" edge="1" parent="1" source="34" target="38">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="34" value="User 1" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="510" 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;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="460" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="36" target="41">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="36" value="User 2" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="510" 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;" vertex="1" parent="1">
|
||||
<mxGeometry x="350" y="460" 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;" edge="1" parent="1" source="38" target="36">
|
||||
<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;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="660" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="44" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="41" target="43">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="41" value="Reads the files From A, B, C" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="660" 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;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="810" width="170" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
15
src/App.scss
15
src/App.scss
@ -169,3 +169,18 @@ 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;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle'
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||
import CachedIcon from '@mui/icons-material/Cached'
|
||||
import RouterIcon from '@mui/icons-material/Router'
|
||||
import StorageIcon from '@mui/icons-material/Storage'
|
||||
import { ListItem, useTheme } from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
@ -74,6 +75,12 @@ export const SettingsPage = () => {
|
||||
</ListItemIcon>
|
||||
{listItem('Relays')}
|
||||
</ListItem>
|
||||
<ListItem component={Link} to={appPrivateRoutes.servers}>
|
||||
<ListItemIcon>
|
||||
<StorageIcon />
|
||||
</ListItemIcon>
|
||||
{listItem('Servers')}
|
||||
</ListItem>
|
||||
<ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
|
||||
<ListItemIcon>
|
||||
<CachedIcon />
|
||||
|
256
src/pages/settings/servers/index.tsx
Normal file
256
src/pages/settings/servers/index.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
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 {
|
||||
getFileServers,
|
||||
publishFileServer
|
||||
} from '../../../utils/file-servers.ts'
|
||||
import { useAppSelector } from '../../../hooks/store.ts'
|
||||
import { useDidMount } from '../../../hooks'
|
||||
import { SIGIT_BLOSSOM } 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>({})
|
||||
|
||||
useDidMount(() => {
|
||||
fetchFileServers()
|
||||
})
|
||||
|
||||
const fetchFileServers = async () => {
|
||||
if (usersPubkey) {
|
||||
await getFileServers(usersPubkey).then((res) => {
|
||||
if (res.map) {
|
||||
if (Object.keys(res.map).length === 0) {
|
||||
serverRequirementWarning()
|
||||
}
|
||||
|
||||
setBlossomServersMap(res.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
|
||||
}
|
||||
|
||||
const serverURL = `${protocol}${newServerURL?.trim().replace(protocol, '')}`
|
||||
if (!serverURL) return
|
||||
|
||||
// Check if new server is a valid URL
|
||||
if (
|
||||
!/^(https?:\/\/)(?!-)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}$/gim.test(
|
||||
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).catch(() => null)
|
||||
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 (
|
||||
serverURL === SIGIT_BLOSSOM &&
|
||||
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)
|
||||
.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, reject) => {
|
||||
axios
|
||||
.get(serverURL)
|
||||
.then((res) => {
|
||||
if (res && res.data?.toLowerCase().includes('blossom server')) {
|
||||
resolve(true)
|
||||
} else {
|
||||
reject(false)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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}
|
||||
handleDeleteServer={handleDeleteServer}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Footer />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
interface ServerItemProps {
|
||||
serverURL: string
|
||||
handleDeleteServer?: (serverURL: string) => void
|
||||
}
|
||||
|
||||
const ServerItem = ({ serverURL, handleDeleteServer }: 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} ${serverURL === SIGIT_BLOSSOM ? styles.disabled : ''}`}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<span>Remove</span>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
111
src/pages/settings/servers/style.module.scss
Normal file
111
src/pages/settings/servers/style.module.scss
Normal file
@ -0,0 +1,111 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ export const appPrivateRoutes = {
|
||||
profileSettings: '/settings/profile/:npub',
|
||||
cacheSettings: '/settings/cache',
|
||||
relays: '/settings/relays',
|
||||
servers: '/settings/servers',
|
||||
nostrLogin: '/settings/nostrLogin'
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { RelaysPage } from '../pages/settings/relays'
|
||||
import { SettingsPage } from '../pages/settings/Settings'
|
||||
import { SignPage } from '../pages/sign'
|
||||
import { VerifyPage } from '../pages/verify'
|
||||
import { ServersPage } from '../pages/settings/servers'
|
||||
|
||||
/**
|
||||
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
||||
@ -96,6 +97,10 @@ export const privateRoutes = [
|
||||
path: appPrivateRoutes.relays,
|
||||
element: <RelaysPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.servers,
|
||||
element: <ServersPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.nostrLogin,
|
||||
element: <NostrLoginPage />
|
||||
|
@ -19,6 +19,7 @@ export interface Meta {
|
||||
exportSignature?: string
|
||||
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||
timestamps?: OpenTimestamp[]
|
||||
// TODO Add field: fileServers
|
||||
}
|
||||
|
||||
export interface CreateSignatureEventContent {
|
||||
|
6
src/types/file-server.ts
Normal file
6
src/types/file-server.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type FileServerMap = {
|
||||
[key: string]: {
|
||||
read: boolean
|
||||
write: boolean
|
||||
}
|
||||
}
|
@ -5,3 +5,4 @@ export * from './profile'
|
||||
export * from './relay'
|
||||
export * from './zip'
|
||||
export * from './event'
|
||||
export * from './file-server.ts'
|
||||
|
115
src/utils/file-servers.ts
Normal file
115
src/utils/file-servers.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { FileServerMap } from '../types'
|
||||
import { NostrController, relayController } from '../controllers'
|
||||
import { DEFAULT_LOOK_UP_RELAY_LIST, SIGIT_BLOSSOM } from './const.ts'
|
||||
import { unixNow } from './nostr.ts'
|
||||
import { Filter, UnsignedEvent, kinds } from 'nostr-tools'
|
||||
|
||||
/**
|
||||
* Fetches the relays to get preferred file servers for the given npub
|
||||
* @param npub hex pubkey
|
||||
*/
|
||||
const getFileServers = async (
|
||||
npub: string
|
||||
): Promise<{ map: FileServerMap; mapUpdated?: number }> => {
|
||||
// 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 relayController
|
||||
.fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST)
|
||||
.catch((err) => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
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() })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes a preferred file servers list for the given npub
|
||||
* @param serverMap list of preferred servers
|
||||
* @param npub hex pubkey
|
||||
* @param extraRelaysToPublish additional relay on which to publish
|
||||
*/
|
||||
const publishFileServer = async (
|
||||
serverMap: FileServerMap,
|
||||
npub: string,
|
||||
extraRelaysToPublish?: 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)
|
||||
|
||||
let relaysToPublish = serverURLs
|
||||
|
||||
// Add extra relays if provided
|
||||
if (extraRelaysToPublish) {
|
||||
relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
|
||||
}
|
||||
|
||||
// If a relay map is empty, use the most popular relay URIs
|
||||
if (!relaysToPublish.length) {
|
||||
relaysToPublish = DEFAULT_LOOK_UP_RELAY_LIST
|
||||
}
|
||||
const publishResult = await relayController.publish(
|
||||
signedEvent,
|
||||
relaysToPublish
|
||||
)
|
||||
|
||||
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 { getFileServers, publishFileServer }
|
@ -688,7 +688,7 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to upload user application data to the Blossom server.
|
||||
* Function to upload user application data to the SIGit 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.
|
||||
@ -744,7 +744,7 @@ const uploadUserAppDataToBlossom = async (
|
||||
|
||||
// Finalize the event with the private key
|
||||
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
||||
|
||||
// TODO send to all added preferred blossom/file servers
|
||||
// Upload the file to the file storage service using Axios
|
||||
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
|
||||
headers: {
|
||||
|
Loading…
Reference in New Issue
Block a user