issue-274 #278
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);
|
color: rgba(0, 0, 0, 0.25);
|
||||||
font-size: 14px;
|
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 ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||||
import CachedIcon from '@mui/icons-material/Cached'
|
import CachedIcon from '@mui/icons-material/Cached'
|
||||||
import RouterIcon from '@mui/icons-material/Router'
|
import RouterIcon from '@mui/icons-material/Router'
|
||||||
|
import StorageIcon from '@mui/icons-material/Storage'
|
||||||
import { ListItem, useTheme } from '@mui/material'
|
import { ListItem, useTheme } from '@mui/material'
|
||||||
import List from '@mui/material/List'
|
import List from '@mui/material/List'
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||||
@ -74,6 +75,12 @@ export const SettingsPage = () => {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
{listItem('Relays')}
|
{listItem('Relays')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem component={Link} to={appPrivateRoutes.servers}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<StorageIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
{listItem('Servers')}
|
||||||
|
</ListItem>
|
||||||
<ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
|
<ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<CachedIcon />
|
<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(
|
||||||
m marked this conversation as resolved
Outdated
|
|||||||
|
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',
|
profileSettings: '/settings/profile/:npub',
|
||||||
cacheSettings: '/settings/cache',
|
cacheSettings: '/settings/cache',
|
||||||
relays: '/settings/relays',
|
relays: '/settings/relays',
|
||||||
|
servers: '/settings/servers',
|
||||||
nostrLogin: '/settings/nostrLogin'
|
nostrLogin: '/settings/nostrLogin'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import { RelaysPage } from '../pages/settings/relays'
|
|||||||
import { SettingsPage } from '../pages/settings/Settings'
|
import { SettingsPage } from '../pages/settings/Settings'
|
||||||
import { SignPage } from '../pages/sign'
|
import { SignPage } from '../pages/sign'
|
||||||
import { VerifyPage } from '../pages/verify'
|
import { VerifyPage } from '../pages/verify'
|
||||||
|
import { ServersPage } from '../pages/settings/servers'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
||||||
@ -96,6 +97,10 @@ export const privateRoutes = [
|
|||||||
path: appPrivateRoutes.relays,
|
path: appPrivateRoutes.relays,
|
||||||
element: <RelaysPage />
|
element: <RelaysPage />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.servers,
|
||||||
|
element: <ServersPage />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: appPrivateRoutes.nostrLogin,
|
path: appPrivateRoutes.nostrLogin,
|
||||||
element: <NostrLoginPage />
|
element: <NostrLoginPage />
|
||||||
|
@ -19,6 +19,7 @@ export interface Meta {
|
|||||||
exportSignature?: string
|
exportSignature?: string
|
||||||
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||||
timestamps?: OpenTimestamp[]
|
timestamps?: OpenTimestamp[]
|
||||||
|
// TODO Add field: fileServers
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSignatureEventContent {
|
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 './relay'
|
||||||
export * from './zip'
|
export * from './zip'
|
||||||
export * from './event'
|
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 sigits - An object containing metadata for the user application data.
|
||||||
* @param processedGiftWraps - An array of processed gift wrap IDs.
|
* @param processedGiftWraps - An array of processed gift wrap IDs.
|
||||||
* @param privateKey - The private key used for encryption.
|
* @param privateKey - The private key used for encryption.
|
||||||
@ -744,7 +744,7 @@ const uploadUserAppDataToBlossom = async (
|
|||||||
|
|
||||||
// Finalize the event with the private key
|
// Finalize the event with the private key
|
||||||
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
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
|
// Upload the file to the file storage service using Axios
|
||||||
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
|
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
|
||||||
headers: {
|
headers: {
|
||||||
|
Loading…
Reference in New Issue
Block a user
should be a utility