Added libs
This commit is contained in:
2
noSys/.gitignore
vendored
Normal file
2
noSys/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
noSys.zip
|
||||
1
noSys/.mtimes.json
Normal file
1
noSys/.mtimes.json
Normal file
@@ -0,0 +1 @@
|
||||
{".gitignore": 1748256258.9985344, "api.http": 1750139941.0187607, "connections.py": 1757738624.594908, "dataManager.py": 1766792003.8798084, "dispatcher.py": 1757802996.961887, "events.py": 1757056638.2672198, "index.html": 1752913927.847083, "modules.py": 1757507552.9273007, "networks.py": 1757748986.1507368, "networksApiBlueprint.py": 1757583651.2527168, "noSys.py": 1757749429.9569714, "noSys.zip.bkp": 1755913936.6262422, "noSysApiBlueprint.py": 1757743519.0319538, "noSysCore.py": 1757838163.5684075, "noSysModule.py": 1757065213.3453174, "noSysModuleServer.py": 1756720592.4303212, "noSysSocketio.py": 1756798784.7306013, "peers.py": 1757802573.2807248, "servers.py": 1756114998.854464, "users.py": 1757585178.0426896, "vue\\router.js": 1753274355.3049414, "vue\\api\\noSysApi.js": 1757586249.0933578, "vue\\api\\socketEvents.js": 1753577297.9669068, "vue\\components\\ConnectionsTab.vue": 1757496047.4013255, "vue\\components\\NetworksTabs.vue": 1757748327.563559, "vue\\stores\\noSysStore.js": 1757498732.9446464, "vue\\views\\HomeView.vue": 1757495074.6380558}
|
||||
24
noSys/api.http
Normal file
24
noSys/api.http
Normal file
@@ -0,0 +1,24 @@
|
||||
@hostname = http://127.0.0.1
|
||||
@port = 5001
|
||||
@hostServer = {{hostname}}:{{port}}
|
||||
|
||||
###
|
||||
POST {{hostServer}}/users HTTP/1.1
|
||||
content-type: application/json
|
||||
|
||||
{"password":"MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MTI=", "data":{"proof_of_work":"eYnU*@"}}
|
||||
###{"password":"I0x1Y2FzR2FicmllbFZhekRvc1NhbnRvc0luYWNpbyE=", "data":{"proof_of_work":"eYnU*@"}}
|
||||
### A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj
|
||||
|
||||
|
||||
###
|
||||
GET {{hostServer}}/users
|
||||
|
||||
###
|
||||
GET {{hostServer}}/users/A14%2FRek5z78U1rYC%2BWLvU%2FifnsX43o0tjnexmYdlXsjY
|
||||
|
||||
###
|
||||
POST {{hostServer}}/users/A14%2FRek5z78U1rYC%2BWLvU%2FifnsX43o0tjnexmYdlXsjY/sign HTTP/1.1
|
||||
content-type: application/json
|
||||
|
||||
{"data":"Test"}
|
||||
70
noSys/connections.py
Normal file
70
noSys/connections.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from .peers import Peer, ConnectionState
|
||||
from .events import Events, DynamicEvents
|
||||
|
||||
from libs.fspn.utils.observable import Event
|
||||
from libs.fspn.protocol.connection import Connection, EVENTS as CONNECTION_EVENTS
|
||||
|
||||
from libs.app.common.logging import get_logger
|
||||
|
||||
logger = get_logger("connections")
|
||||
|
||||
class ConnectionManager:
|
||||
"""Handles peer connections"""
|
||||
|
||||
# TODO Maybe peerManager receveid events and change status, reconnections, disconnections, etc. This class can just manage limits of connections, idk!
|
||||
|
||||
def __init__(self, nosys_core):
|
||||
from .noSysCore import NoSysCore
|
||||
self.nosys_core:NoSysCore = nosys_core
|
||||
|
||||
def connect(self, address, user_id, bind_address=("0.0.0.0", 0)):
|
||||
peer, connect = self.create_connection(address, user_id, bind_address)
|
||||
connect()
|
||||
return peer
|
||||
|
||||
def create_connection(self, address, user_id, bind_address=("0.0.0.0", 0)):
|
||||
user = self.nosys_core.users.get_user(user_id)
|
||||
if not user:
|
||||
raise Exception("Cannot connect, user missing")
|
||||
|
||||
connection = Connection(user.public_key, self.nosys_core.modules.pmc)
|
||||
connection.subscribe_event(CONNECTION_EVENTS.ON_CONNECTION, self._on_connection)
|
||||
connection.subscribe_event(CONNECTION_EVENTS.ON_CONNECTION_ERROR, self._on_connection_error)
|
||||
connection.subscribe_event(CONNECTION_EVENTS.ON_DISCONNECTION, self._on_disconnection)
|
||||
connection.subscribe_event(CONNECTION_EVENTS.ON_MESSAGE, self.nosys_core.dispatcher.on_message)
|
||||
|
||||
peer = self.nosys_core.peers.create_peer(connection)
|
||||
self.nosys_core.peers.add_peer(peer)
|
||||
|
||||
def connect():
|
||||
connection.connect(address, bind_address)
|
||||
|
||||
return peer, connect
|
||||
|
||||
def disconnect(self, peer_id: str):
|
||||
self.nosys_core.peers.remove_peer(peer_id)
|
||||
|
||||
def _on_connection(self, event:Event):
|
||||
connection:Connection = event.source
|
||||
logger.debug(f'New connection {connection.address} ID: {connection.id}')
|
||||
peer = self.nosys_core.peers.get_peer(connection.id)
|
||||
peer.state = ConnectionState.CONNECTED
|
||||
self.nosys_core.fire_event(Events.PEER_CONNECTED, peer=peer)
|
||||
self.nosys_core.fire_event(DynamicEvents.peer_connection(peer.id), peer=peer)
|
||||
logger.debug(f'Peer connected: {peer.get_my_user()} -> {peer.get_peer_user()}')
|
||||
|
||||
def _on_connection_error(self, event:Event):
|
||||
connection:Connection = event.source
|
||||
error = event.error
|
||||
peer = self.nosys_core.peers.get_peer(connection.id)
|
||||
peer.state = ConnectionState.DISCONNECTED
|
||||
self.nosys_core.fire_event(Events.PEER_CONNECTION_ERROR, peer=peer, error=error)
|
||||
self.nosys_core.fire_event(DynamicEvents.peer_connection_error(peer.id), peer=peer, error=error)
|
||||
|
||||
def _on_disconnection(self, event:Event):
|
||||
connection:Connection = event.source
|
||||
logger.debug('Disconnection', event.__dict__)
|
||||
peer = self.nosys_core.peers.get_peer(connection.id)
|
||||
peer.state = ConnectionState.DISCONNECTED
|
||||
self.nosys_core.fire_event(Events.PEER_DISCONNECTED, peer=peer)
|
||||
self.nosys_core.fire_event(DynamicEvents.peer_disconnection(peer.id), peer=peer)
|
||||
145
noSys/dataManager.py
Normal file
145
noSys/dataManager.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from typing import Dict, List, Optional, Any
|
||||
from libs.app.common.store import DataStore
|
||||
from libs.fspn.utils.sha256_util import hash_string
|
||||
|
||||
class DataManager():
|
||||
def __init__(self, nosys_core):
|
||||
from .noSysCore import NoSysCore
|
||||
self.nosys_core:NoSysCore = nosys_core
|
||||
|
||||
self.store = DataStore(path="noSys/data.json", default_data={"users":[], "rendezvous":[], "networks":[]})
|
||||
|
||||
# ------------------- USER CRUD -------------------
|
||||
def add_user(self, user_id: str):
|
||||
if self.get_user(user_id):
|
||||
raise ValueError(f"User {user_id} already exists")
|
||||
user = {"id": user_id, "networks":[]}
|
||||
self.store.add_item("users", user, unique=True, id_field="id", id=user_id)
|
||||
return user
|
||||
|
||||
def get_user(self, user_id: str):
|
||||
return self.store.get_item("users", "id", user_id)
|
||||
|
||||
def list_users(self):
|
||||
return self.store.list_items("users")
|
||||
|
||||
def delete_user(self, user_id: str) -> bool:
|
||||
return self.store.remove_item("users", "id", user_id)
|
||||
|
||||
def update_user(self, user_id: str, updates: Dict[str, Any]) -> bool:
|
||||
return self.store.update_item("users", "id", user_id, updates)
|
||||
|
||||
# ------------------- USER NETWORK MANAGEMENT -------------------
|
||||
def user_add_network(self, user_id: str, network_id: str) -> bool:
|
||||
user = self.get_user(user_id)
|
||||
if not user:
|
||||
return False
|
||||
if network_id not in user.get("networks"):
|
||||
user.setdefault("networks").append(network_id)
|
||||
return self.update_user(user_id, {"networks": user["networks"]})
|
||||
return False
|
||||
|
||||
def user_remove_network(self, user_id: str, network_id: str) -> bool:
|
||||
user = self.get_user(user_id)
|
||||
if not user:
|
||||
return False
|
||||
if network_id not in user.get("networks"):
|
||||
new_networks = [n for n in user["networks"] if n != network_id]
|
||||
return self.update_user(user_id, {"networks": new_networks})
|
||||
return False
|
||||
|
||||
def user_list_networks(self, user_id: str) -> List[str]:
|
||||
user = self.get_user(user_id)
|
||||
return user["networks", []] if user else []
|
||||
|
||||
# ------------------- RENDEZVOUS CRUD -------------------
|
||||
def add_rendezvous(self, rv_id: str, address: str):
|
||||
if self.get_rendezvous(rv_id):
|
||||
raise ValueError(f"Rendezvous {rv_id} already exists")
|
||||
rv = {"id": rv_id, "address": address}
|
||||
self.store.add_item("rendezvous", rv, unique=True, id_field="id", id=rv_id)
|
||||
|
||||
def get_rendezvous(self, rv_id: str):
|
||||
return self.store.get_item("rendezvous", "id", rv_id)
|
||||
|
||||
def list_rendezvous(self):
|
||||
return self.store.list_items("rendezvous")
|
||||
|
||||
def delete_rendezvous(self, rv_id: str) -> bool:
|
||||
return self.store.remove_item("rendezvous", "id", rv_id)
|
||||
|
||||
def update_rendezvous(self, rv_id: str, updates: Dict[str, Any]) -> bool:
|
||||
return self.store.update_item("rendezvous", "id", rv_id, updates)
|
||||
|
||||
# ------------------- NETWORK CRUD -------------------
|
||||
def _default_config(self, net_type: str) -> Dict[str, Any]:
|
||||
defaults = {
|
||||
"test_network": {
|
||||
"auto_connect": True,
|
||||
"min_connections": 10,
|
||||
"max_connections": 20,
|
||||
"max_store_size": 1000,
|
||||
"max_message_size": 64_000,
|
||||
"message_ttl": 86400,
|
||||
"ack_required": True,
|
||||
"min_pow": 4
|
||||
}
|
||||
}
|
||||
return defaults.get(net_type, {})
|
||||
|
||||
def _generate_id(self, name, net_type):
|
||||
payload = f"{name}:{net_type}".encode()
|
||||
return hash_string(payload)
|
||||
|
||||
def create_network(
|
||||
self,
|
||||
name: str,
|
||||
description: str,
|
||||
net_type: str,
|
||||
modules: Optional[List[str]] = None,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
|
||||
network_id = self._generate_id(name, net_type)
|
||||
if self.get_network(network_id):
|
||||
raise ValueError(f"Network {network_id} already exists")
|
||||
|
||||
final_config = {**self._default_config(net_type), **(config or {})}
|
||||
network = {
|
||||
"id": network_id,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"type": net_type,
|
||||
"modules": modules or [],
|
||||
"peers": [],
|
||||
"rendezvous": [],
|
||||
"config": final_config
|
||||
}
|
||||
self.store.add_item("networks", network, unique=True, id_field="id", id=network_id)
|
||||
return network
|
||||
|
||||
def get_network(self, network_id: str) -> Optional[Dict[str, Any]]:
|
||||
return self.store.get_item("networks", "id", network_id)
|
||||
|
||||
def list_networks(self) -> List[Dict[str, Any]]:
|
||||
return self.store.list_items("networks")
|
||||
|
||||
def delete_network(self, network_id: str) -> bool:
|
||||
return self.store.remove_item("networks", "id", network_id)
|
||||
|
||||
def update_network(self, network_id: str, updates: Dict[str, Any]) -> bool:
|
||||
return self.store.update_item("networks", "id", network_id, updates)
|
||||
|
||||
def network_assign_rendezvous(self, network_id: str, rv_id: str):
|
||||
network = self.store.get_item("networks", "id", network_id)
|
||||
if not network:
|
||||
return False
|
||||
if rv_id not in network.get("rendezvous", []):
|
||||
network.setdefault("rendezvous", []).append(rv_id)
|
||||
return self.store.update_item("networks", "id", network_id, {"rendezvous": network["rendezvous"]})
|
||||
return False
|
||||
|
||||
# ------------------- NETWORK QUERY -------------------
|
||||
def network_find_by_module(self, module_name: str) -> List[Dict[str, Any]]:
|
||||
"""Return all networks that use a given module"""
|
||||
return [n for n in self.list_networks() if module_name in n.get("modules", [])]
|
||||
73
noSys/dispatcher.py
Normal file
73
noSys/dispatcher.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import json
|
||||
from libs.app.common.logging import get_logger
|
||||
from .events import DynamicEvents
|
||||
|
||||
logger = get_logger("dispatcher")
|
||||
|
||||
class Dispatcher:
|
||||
"""Responsible for sending messages/binary, broadcasting and dispatching received messages for dynamic events."""
|
||||
|
||||
def __init__(self, nosys_core):
|
||||
from .noSysCore import NoSysCore
|
||||
self.nosys_core: NoSysCore = nosys_core
|
||||
|
||||
# -------- Sending --------
|
||||
|
||||
def send_binary(self, data: bytes, peer_id, to_module: tuple[str, str], meta=None, encrypted: bool = True):
|
||||
meta = meta or {}
|
||||
peer = self.nosys_core.peers.get_peer(peer_id)
|
||||
if peer and peer.has_module(to_module):
|
||||
app_body = {
|
||||
"app": {"lib": to_module[0], "module": to_module[1]},
|
||||
"meta": meta,
|
||||
}
|
||||
peer.connection.send_binary(data=data, meta=app_body, encrypted=encrypted)
|
||||
|
||||
def send_message(self, message, peer_id, to_module: tuple[str, str], encrypted: bool = True):
|
||||
peer = self.nosys_core.peers.get_peer(peer_id)
|
||||
if peer:
|
||||
if not peer.is_connected():
|
||||
raise Exception(f"Cannot send message to a disconnected peer: {peer_id}")
|
||||
app_body = {
|
||||
"app": {"lib": to_module[0], "module": to_module[1]},
|
||||
"data": message,
|
||||
}
|
||||
peer.connection.send_message(message=json.dumps(app_body), encrypted=encrypted)
|
||||
|
||||
def broadcast_message(self, message, to_module: tuple[str, str], exclude: list[str] | None = None,
|
||||
encrypted: bool = True):
|
||||
exclude = exclude or []
|
||||
for peer in self.nosys_core.peers.get_online_peers():
|
||||
if peer.peer_id not in exclude:
|
||||
self.send_message(message, peer.peer_id, to_module, encrypted)
|
||||
|
||||
# -------- Receiving --------
|
||||
|
||||
def on_message(self, event):
|
||||
"""Receives event from Connection and fires dynamic event `module_message_`"""
|
||||
try:
|
||||
if "meta" in event.message:
|
||||
app_body = event.message["meta"]
|
||||
module_meta = app_body.get("meta", {})
|
||||
data = event.message["data"]
|
||||
else:
|
||||
app_body = json.loads(event.message["data"])
|
||||
module_meta = {}
|
||||
data = app_body["data"]
|
||||
|
||||
module_key = (app_body["app"]["lib"], app_body["app"]["module"])
|
||||
logger.debug(
|
||||
"%s receiving from %s\n Module: %s\n Meta: %s\n Data: %s",
|
||||
event.source.bind_address,
|
||||
event.source.address,
|
||||
module_key,
|
||||
module_meta,
|
||||
data,
|
||||
)
|
||||
|
||||
evt_name = DynamicEvents.module_message(module_key[0], module_key[1])
|
||||
peer = self.nosys_core.peers.get_peer(event.source.id)
|
||||
self.nosys_core.fire_event(evt_name, peer=peer, module=module_key, data=data, meta=module_meta)
|
||||
|
||||
except Exception:
|
||||
logger.exception("ERROR while dispatching incoming message")
|
||||
45
noSys/events.py
Normal file
45
noSys/events.py
Normal file
@@ -0,0 +1,45 @@
|
||||
class Events:
|
||||
STARTED = "started"
|
||||
START_ERROR = "error_start"
|
||||
READY = "ready"
|
||||
USER_ADDED = "user_added"
|
||||
USER_REMOVED = "user_removed"
|
||||
PEER_CONNECTED = "peer_connected"
|
||||
PEER_CONNECTION_ERROR = "peer_connection_error"
|
||||
PEER_DISCONNECTED = "peer_disconnected"
|
||||
|
||||
class DynamicEvents:
|
||||
@staticmethod
|
||||
def peer_connection(id:str) -> str:
|
||||
return f"peer_connection_{id}"
|
||||
|
||||
@staticmethod
|
||||
def peer_connection_error(id:str) -> str:
|
||||
return f"peer_connection_error_{id}"
|
||||
|
||||
@staticmethod
|
||||
def peer_disconnection(id:str) -> str:
|
||||
return f"peer_disconnection_{id}"
|
||||
|
||||
@staticmethod
|
||||
def module_message(lib: str, module: str) -> str:
|
||||
return f"module_message_{lib}_{module}"
|
||||
|
||||
@staticmethod
|
||||
def module_connection(lib: str, module: str) -> str:
|
||||
return f"module_connection_{lib}_{module}"
|
||||
|
||||
@staticmethod
|
||||
def module_disconnection(lib: str, module: str) -> str:
|
||||
return f"module_disconnection_{lib}_{module}"
|
||||
|
||||
@staticmethod
|
||||
def network_connection(network: str) -> str:
|
||||
return f"network_connection_{network}"
|
||||
|
||||
@staticmethod
|
||||
def network_disconnection(network: str) -> str:
|
||||
return f"network_disconnection_{network}"
|
||||
|
||||
class ServerEvents:
|
||||
TEST = "test"
|
||||
179
noSys/index.html
Normal file
179
noSys/index.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>NoSys</title>
|
||||
|
||||
<style>
|
||||
body{
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const noSysApiUrl = "http://127.0.0.1:5050/noSys";
|
||||
var connectionClicked = null;
|
||||
|
||||
function connectAddress(){
|
||||
const ip = document.getElementById("ip").value;
|
||||
const port = document.getElementById("port").value;
|
||||
|
||||
const data = {"user": "A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj", "address": {"ip": ip,"port": port}};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json',},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(noSysApiUrl+"/peers", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
listConnections();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(){
|
||||
messageElement = document.getElementById("message")
|
||||
message = messageElement.value;
|
||||
if (connectionClicked && message){
|
||||
const data = {"message":{"action":"test", "data":message},"toModule": {"package": "noSys","module": "noSys"},"encrypted": true};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(noSysApiUrl+"/peers/"+connectionClicked, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
addMessage(connectionClicked, message, false)
|
||||
messageElement.value = ""
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setConnectionClicked(id, address, bindAddress){
|
||||
connectionClicked = id;
|
||||
document.getElementById("messageTo").value = address;
|
||||
document.getElementById("messageFrom").value = bindAddress;
|
||||
}
|
||||
|
||||
function listConnections(){
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: {}
|
||||
};
|
||||
fetch(noSysApiUrl+"/peers", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok){
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
uList = document.getElementById("ulConnections");
|
||||
uList.replaceChildren();
|
||||
data.forEach((element, index) => {
|
||||
console.log(element);
|
||||
var li = document.createElement("li");
|
||||
li.setAttribute("id", element.id);
|
||||
li.addEventListener("click", function(){
|
||||
setConnectionClicked(this.id, element.address, element.bindAddress);
|
||||
});
|
||||
li.appendChild(document.createTextNode("ID:"+element.id+" | Connected to:"+element.address+" | Address bound:"+element.bindAddress+" | User:"+element.user+" | Status:"+element.status));
|
||||
uList.appendChild(li);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function addMessage(connectionId, message, from){
|
||||
uList = document.getElementById("ulMessages");
|
||||
var li = document.createElement("li");
|
||||
const direction = from ? "FROM" : "TO";
|
||||
text = " "+direction+" ConnectionId: "+connectionId+"\n"+message+"\n\n";
|
||||
li.appendChild(document.createTextNode(text));
|
||||
uList.appendChild(li);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var socket = io(noSysApiUrl);
|
||||
socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
});
|
||||
socket.on('message', function(msg) {
|
||||
console.log('Message received: ' + msg);
|
||||
});
|
||||
|
||||
socket.on('test', function(msg) {
|
||||
console.log(msg);
|
||||
addMessage(msg.connectionId, msg.message, true)
|
||||
});
|
||||
socket.on('disconnect', function() {
|
||||
console.log('Disconnected from server');
|
||||
});
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<input id="ip" type="text" value="n0sys.duckdns.org">
|
||||
<input id="port" type="text" value="30331">
|
||||
<button onclick="connectAddress()">Connect</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input id="message" type="text" value="">
|
||||
<button onclick="sendMessage()">Send message</button>
|
||||
<label>TO</label>
|
||||
<input id="messageTo" type="text" value="">
|
||||
<label>FROM</label>
|
||||
<input id="messageFrom" type="text" value="">
|
||||
</div>
|
||||
|
||||
<pre>
|
||||
<ul id="ulMessages">
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
<div>
|
||||
<ul id="ulConnections">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
<script>
|
||||
listConnections()
|
||||
</script>
|
||||
</html>
|
||||
23
noSys/info.json
Normal file
23
noSys/info.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "noSys",
|
||||
"version": 0.14,
|
||||
"modules": [
|
||||
{
|
||||
"id": "noSys",
|
||||
"version": 0
|
||||
},
|
||||
{
|
||||
"id": "networks",
|
||||
"version": 0
|
||||
}
|
||||
],
|
||||
"frontend": "vue",
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "fspn"
|
||||
},
|
||||
{
|
||||
"id": "api"
|
||||
}
|
||||
]
|
||||
}
|
||||
97
noSys/modules.py
Normal file
97
noSys/modules.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .noSysCore import NoSysCore
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
import logging
|
||||
from typing import Dict, Tuple, Any
|
||||
|
||||
from .events import Events
|
||||
from .noSysModule import NoSysModule
|
||||
from libs.app.common.logging import get_logger
|
||||
from libs.fspn.protocol.security import Pmc
|
||||
|
||||
logger = get_logger("moduleManager", buffer=True)
|
||||
|
||||
class ModuleManager:
|
||||
"""Loads, unloads, and manages modules dynamically."""
|
||||
|
||||
def __init__(self, nosys_core: "NoSysCore"):
|
||||
self.nosys_core = nosys_core
|
||||
self.modules: Dict[str, NoSysModule] = {}
|
||||
from libs.api.api import Api
|
||||
self.api:Api = None
|
||||
self.pmc:Pmc = None
|
||||
|
||||
self.load_from_config()
|
||||
|
||||
def get(self, package_id, module_id) -> NoSysModule:
|
||||
return self.modules.get((package_id, module_id))
|
||||
|
||||
def load_from_config(self):
|
||||
libs = self.nosys_core.config.get("app", "libs")
|
||||
for lib in libs:
|
||||
if lib.get("enabled", True):
|
||||
for module in self.nosys_core.config.get(lib.get("id"), "info", "modules"):
|
||||
self.load(lib.get("id"), module.get("id"))
|
||||
|
||||
def load(self, package_id: str, module_id: str):
|
||||
"""Dynamically load a module and initialize it."""
|
||||
logger.debug(f"Importing module: {package_id}.{module_id}")
|
||||
|
||||
key = (package_id, module_id)
|
||||
if key in self.modules:
|
||||
return self.modules[key]
|
||||
|
||||
path = f"libs.{package_id}.{module_id}"
|
||||
|
||||
try:
|
||||
if path in sys.modules:
|
||||
module = importlib.reload(sys.modules[path])
|
||||
else:
|
||||
module = importlib.import_module(path)
|
||||
|
||||
# Prefer explicit "get_module_class" function if provided
|
||||
if hasattr(module, "get_module_class"):
|
||||
clss = module.get_module_class()
|
||||
else:
|
||||
# Fallback to naming convention: Uppercase the first letter of the module
|
||||
class_name = module_id[:1].upper()+module_id[1:]
|
||||
clss = getattr(module, class_name)
|
||||
|
||||
if not issubclass(clss, NoSysModule):
|
||||
raise TypeError(f"{clss} must inherit from NoSysModule")
|
||||
|
||||
instance = clss(self.nosys_core)
|
||||
self.modules[key] = instance
|
||||
|
||||
logger.debug(f"Module loaded: {path} ({clss.__name__})")
|
||||
return instance
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to load module {path}: {e}")
|
||||
return None
|
||||
|
||||
def unload(self, package_id: str, module_id: str):
|
||||
"""Unload a module and call its teardown hook if available."""
|
||||
key = (package_id, module_id)
|
||||
if key not in self.modules:
|
||||
return
|
||||
|
||||
module:NoSysModule = self.modules.pop(key)
|
||||
|
||||
try:
|
||||
module.teardown_module()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error during teardown of {key}: {e}")
|
||||
|
||||
logger.debug(f"Module unloaded: {key}")
|
||||
|
||||
def reload(self, package_id: str, module_id: str):
|
||||
"""Reload a module (unload + load)."""
|
||||
self.unload(package_id, module_id)
|
||||
return self.load(package_id, module_id)
|
||||
|
||||
def setup(self):
|
||||
for module in self.modules.values():
|
||||
module.setup()
|
||||
217
noSys/networks.py
Normal file
217
noSys/networks.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from typing import Dict, List, Optional, Any
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
|
||||
from libs.fspn.utils.wrapper_util import threaded
|
||||
from .noSysModule import NoSysModule
|
||||
from libs.app.common.store import DataStore
|
||||
from libs.app.common.logging import get_logger
|
||||
from libs.fspn.utils.sha256_util import hash_string
|
||||
from libs.rendezvous.rendezvousClient import RendezvousClient, RendezvousClientEvents as RendezvousEvents
|
||||
from .peers import Peer
|
||||
from .events import Events, DynamicEvents
|
||||
from .networksApiBlueprint import Blueprint
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class NetworkStatus(Enum):
|
||||
IDLE = "idle"
|
||||
CONNECTING = "connecting"
|
||||
HEALTHY = "healthy"
|
||||
DEGRADED = "degraded"
|
||||
FAILED = "failed"
|
||||
|
||||
logger = get_logger("networks")
|
||||
|
||||
class Networks(NoSysModule):
|
||||
""" SOMETHING HERE."""
|
||||
|
||||
def __init__(self, nosys_core):
|
||||
super().__init__(nosys_core)
|
||||
|
||||
self.nosys_core.subscribe_event(Events.USER_ADDED, self.on_user_added)
|
||||
# self.nosys_core.subscribe_event(Events.PEER_CONNECTED, self.on_peer_connected)
|
||||
# self.nosys_core.subscribe_event(Events.PEER_DISCONNECTED, self.on_peer_disconnected)
|
||||
# self.nosys_core.subscribe_event(Events.PEER_CONNECTION_ERROR, self.on_peer_connection_error)
|
||||
|
||||
self.network_states = {}
|
||||
|
||||
def setup(self):
|
||||
self.rendezvous_client:RendezvousClient = self.nosys_core.modules.get("rendezvous", "rendezvousClient")
|
||||
self.rendezvous_client.subscribe_event(RendezvousEvents.SERVER_CONNECTED, self.on_server_connected)
|
||||
self.rendezvous_client.subscribe_event(RendezvousEvents.SERVER_DISCONNECTED, self.on_server_disconnected)
|
||||
self.rendezvous_client.subscribe_event(RendezvousEvents.SERVER_CONNECTION_ERROR, self.on_server_connection_error)
|
||||
self.nosys_core.modules.api.register_blueprint(Blueprint(self).blueprint)
|
||||
# self.socketio = HandlerSocketio(self)
|
||||
# self.nosys_core.modules.api.register_socketio(self.socketio)
|
||||
|
||||
def user_add_network(self, user_id, network_id):
|
||||
self.nosys_core.data.user_add_network(user_id, network_id)
|
||||
|
||||
def user_remove_network(self, user_id, network_id):
|
||||
self.nosys_core.data.user_remove_network(user_id, network_id)
|
||||
|
||||
def on_user_added(self, event):
|
||||
user_id:str = event.user_id
|
||||
user_data = self.nosys_core.data.get_user(user_id)
|
||||
for network_id in user_data.get("networks", []):
|
||||
self.network_states.get(network_id)["users"].append(user_id)
|
||||
|
||||
def on_nosys_ready(self, event):
|
||||
self.manage_networks()
|
||||
|
||||
def set_network(self, network):
|
||||
network_id = network["id"]
|
||||
if not network:
|
||||
raise ValueError(f"Network {network_id} not found")
|
||||
|
||||
if network_id not in self.network_states:
|
||||
self.nosys_core.subscribe_event(DynamicEvents.network_connection(network_id), self.on_network_connection)
|
||||
self.nosys_core.subscribe_event(DynamicEvents.network_disconnection(network_id), self.on_network_disconnection)
|
||||
|
||||
self.network_states[network_id] = {
|
||||
"id": network_id,
|
||||
"users" : [],
|
||||
"peers": [],
|
||||
"rendezvous": [],
|
||||
"status": NetworkStatus.IDLE.value,
|
||||
"managed": self.is_network_auto_start(network_id),
|
||||
"tries": 0
|
||||
}
|
||||
return self.network_states[network_id]
|
||||
|
||||
def is_network_auto_start(self, network_id):
|
||||
network = self.nosys_core.data.get_network(network_id)
|
||||
return network.get("config", {}).get("auto_connect", False)
|
||||
|
||||
def set_managed(self, network_id, value:bool):
|
||||
state = self.network_states[network_id]
|
||||
state["managed"] = value
|
||||
|
||||
@threaded
|
||||
def manage_networks(self):
|
||||
for network in self.networks:
|
||||
self.set_network(network)
|
||||
|
||||
while True:
|
||||
for network_id, state in self.network_states.items():
|
||||
if state["managed"]:
|
||||
self.manage_network(network_id)
|
||||
time.sleep(30)
|
||||
|
||||
def manage_network(self, network_id):
|
||||
state = self.network_states[network_id]
|
||||
network = self.nosys_core.data.get_network(network_id)
|
||||
|
||||
if state["status"] == NetworkStatus.IDLE.value:
|
||||
print(NetworkStatus.IDLE.value)
|
||||
self.connect_network_rendezvous_servers(network)
|
||||
|
||||
elif state["status"] == NetworkStatus.CONNECTING.value:
|
||||
print(NetworkStatus.CONNECTING.value)
|
||||
|
||||
elif state["status"] == NetworkStatus.DEGRADED.value:
|
||||
print(NetworkStatus.DEGRADED.value)
|
||||
self.request_new_peer(network_id=network_id)
|
||||
|
||||
elif state["status"] == NetworkStatus.HEALTHY.value:
|
||||
print(NetworkStatus.HEALTHY.value)
|
||||
|
||||
elif state["status"] == NetworkStatus.FAILED.value:
|
||||
print(NetworkStatus.FAILED.value)
|
||||
self.set_managed(network_id, False)
|
||||
|
||||
def connect_network_rendezvous_servers(self, network):
|
||||
network_id = network.get("id")
|
||||
state = self.network_states.get(network_id)
|
||||
if state["users"]:
|
||||
for rendezvous_id in network.get("rendezvous"):
|
||||
rendezvous = self.nosys_core.data.get_rendezvous(rendezvous_id)
|
||||
host_port = rendezvous.get("address").split(':')
|
||||
address = (host_port[0], int(host_port[1])) if len(host_port) > 1 else (host_port[0], 0)
|
||||
|
||||
peer = self.rendezvous_client.connect_to_server(address, random.choice(state["users"]))
|
||||
state["rendezvous"].append(peer.id)
|
||||
else:
|
||||
logger.debug(f"User missing to connect to the rendezvous servers of network {network_id}")
|
||||
|
||||
def on_server_connected(self, event):
|
||||
peer:Peer = event.peer
|
||||
peer_id = peer.id
|
||||
for network_id, state in self.network_states.items():
|
||||
if peer_id in state["rendezvous"]:
|
||||
print(f"Rendezvous {peer_id} connected for network {network_id}")
|
||||
|
||||
def on_server_connection_error(self, event):
|
||||
peer:Peer = event.peer
|
||||
peer_id = peer.id
|
||||
for network_id, state in self.network_states.items():
|
||||
if peer_id in state["rendezvous"]:
|
||||
state["rendezvous"].remove(peer_id)
|
||||
print(f"Rendezvous {peer_id} error for network {network_id}")
|
||||
|
||||
if not state["rendezvous"]:
|
||||
print(f"All rendezvous failed")
|
||||
state["tries"]+=1
|
||||
|
||||
self._recalc_network_status(network_id)
|
||||
|
||||
def on_server_disconnected(self, event):
|
||||
peer:Peer = event.peer
|
||||
peer_id = peer.id
|
||||
for network_id, state in self.network_states.items():
|
||||
if peer_id in state["rendezvous"]:
|
||||
state["rendezvous"].remove(peer_id)
|
||||
print(f"Rendezvous {peer_id} disconnected for network {network_id}")
|
||||
|
||||
def on_network_connection(self, event):
|
||||
network_id:str = event.network_id
|
||||
peer:Peer = event.peer
|
||||
|
||||
self.network_states[network_id]["peers"].append(peer.id)
|
||||
self._recalc_network_status(network_id)
|
||||
|
||||
def on_network_disconnection(self, event):
|
||||
network_id:str = event.network_id
|
||||
peer:Peer = event.peer
|
||||
|
||||
network_states = self.network_states.get(network_id)
|
||||
network_states["peers"].remove(peer.id)
|
||||
self._recalc_network_status(network_id)
|
||||
|
||||
def request_new_peer(self, network_id, peer_id=None):
|
||||
network_states = self.network_states[network_id]
|
||||
if not peer_id:
|
||||
peer_id = random.choice(network_states["peers"])
|
||||
|
||||
self.rendezvous_client.send_get_random_peer(peer_id, network_id)
|
||||
|
||||
def _set_status(self, network_id, status: NetworkStatus, details=None):
|
||||
state = self.network_states.get(network_id, {})
|
||||
state["status"] = status.value
|
||||
if details:
|
||||
state.update(details)
|
||||
|
||||
def _recalc_network_status(self, network_id):
|
||||
state = self.network_states.get(network_id)
|
||||
peers = state.get("peers", [])
|
||||
network = self.nosys_core.data.get_network(network_id)
|
||||
min_conn = network["config"]["min_connections"]
|
||||
|
||||
if len(peers) >= min_conn:
|
||||
self._set_status(network_id, NetworkStatus.HEALTHY)
|
||||
elif peers:
|
||||
self._set_status(network_id, NetworkStatus.DEGRADED)
|
||||
elif state["rendezvous"]:
|
||||
self._set_status(network_id, NetworkStatus.CONNECTING)
|
||||
elif state["tries"]>=3:
|
||||
self._set_status(network_id, NetworkStatus.FAILED)
|
||||
else:
|
||||
self._set_status(network_id, NetworkStatus.IDLE)
|
||||
|
||||
if state["managed"] == True:
|
||||
self.manage_network(network_id)
|
||||
|
||||
def is_network_healthy(self, network_id):
|
||||
return self.network_states.get(network_id, {}).get("status") == NetworkStatus.HEALTHY.value
|
||||
51
noSys/networksApiBlueprint.py
Normal file
51
noSys/networksApiBlueprint.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from flask import Blueprint, make_response, request, jsonify, abort
|
||||
from flask_socketio import SocketIO, emit, join_room, leave_room
|
||||
|
||||
import os, signal, json, time
|
||||
import logging
|
||||
from threading import Thread
|
||||
|
||||
from libs.api.apiBlueprint import ApiBlueprint
|
||||
|
||||
class Blueprint(ApiBlueprint):
|
||||
def routes(self):
|
||||
from .networks import Networks
|
||||
self.module:Networks = self.module
|
||||
|
||||
@self.blueprint.route('/')
|
||||
def show():
|
||||
return self.module.name
|
||||
|
||||
@self.blueprint.route('/networks')
|
||||
def networks():
|
||||
if request.method == "GET":
|
||||
networks = []
|
||||
for network_id, state in self.module.network_states.items():
|
||||
obj = {"data":self.module.nosys_core.data.get_network(network_id), "state":state}
|
||||
networks.append(obj)
|
||||
return jsonify(networks)
|
||||
|
||||
elif request.method == "POST":
|
||||
content:dict = request.json
|
||||
user_id = content["user_id"]
|
||||
network_id = content["network_id"]
|
||||
self.module.user_add_network(user_id, network_id)
|
||||
|
||||
return jsonify({"network_id":network_id})
|
||||
|
||||
@self.blueprint.route("/networks/<path:network_id>", methods=["GET", "PUT", "DELETE"])
|
||||
def network(network_id):
|
||||
state = self.module.network_states.get(network_id)
|
||||
if request.method == "GET":
|
||||
return jsonify(state)
|
||||
|
||||
elif request.method == "PUT":
|
||||
# TODO Edit network config
|
||||
return jsonify({"networkId":network_id})
|
||||
|
||||
elif request.method == "DELETE":
|
||||
content:dict = request.json
|
||||
user_id = content["user_id"]
|
||||
self.module.user_remove_network(user_id, network_id)
|
||||
return jsonify({"networkId":network_id})
|
||||
|
||||
70
noSys/noSys.py
Normal file
70
noSys/noSys.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import time
|
||||
|
||||
from .noSysModule import NoSysModule
|
||||
from .peers import Peer
|
||||
from .events import Events, DynamicEvents
|
||||
|
||||
from libs.noSys.noSysApiBlueprint import Blueprint
|
||||
from libs.noSys.noSysSocketio import HandlerSocketio
|
||||
|
||||
class NoSys(NoSysModule):
|
||||
"""Core module of NoSys that handles system-level messages."""
|
||||
|
||||
def __init__(self, nosys_core):
|
||||
super().__init__(nosys_core)
|
||||
|
||||
self.nosys_core.subscribe_event(Events.USER_ADDED, self.on_user_added)
|
||||
self.nosys_core.subscribe_event(Events.PEER_CONNECTED, self.on_peer_connected)
|
||||
|
||||
def setup(self):
|
||||
self.nosys_core.modules.api.register_blueprint(Blueprint(self).blueprint)
|
||||
|
||||
self.socketio = HandlerSocketio(self)
|
||||
self.nosys_core.modules.api.register_socketio(self.socketio)
|
||||
|
||||
def on_nosys_ready(self, event):
|
||||
pass
|
||||
|
||||
def on_user_added(self, event):
|
||||
user_id:str = event.user_id
|
||||
user_data = self.nosys_core.data.get_user(user_id)
|
||||
if not user_data:
|
||||
user_data = self.nosys_core.data.add_user(user_id)
|
||||
|
||||
def on_peer_connected(self, event):
|
||||
peer:Peer = event.peer
|
||||
self.send_info(peer.id)
|
||||
|
||||
def on_module_message(self, event):
|
||||
handler_action = getattr(self, 'on_'+event.data.get("action"))
|
||||
handler_action(event)
|
||||
|
||||
def send_info(self, peer_id):
|
||||
modules = []
|
||||
for key, module in self.nosys_core.modules.modules.items():
|
||||
modules.append({"package":module.package_id, "module":module.module_id})
|
||||
networks = []
|
||||
for network in self.nosys_core.data.list_networks():
|
||||
networks.append({"id":network.get("id")})
|
||||
self.nosys_core.dispatcher.send_message({'action':'info','modules':modules, 'networks':networks}, peer_id, self.id)
|
||||
|
||||
def on_info(self, event):
|
||||
peer: Peer = event.peer
|
||||
modules = event.data['modules']
|
||||
for module in modules:
|
||||
lib = module.get("package")
|
||||
module = module.get("module")
|
||||
peer.modules[(lib, module)] = module
|
||||
self.nosys_core.fire_event(DynamicEvents.module_connection(lib, module), peer=event.peer)
|
||||
networks = event.data['networks']
|
||||
for network in networks:
|
||||
network_id = network.get("id")
|
||||
peer.networks[network_id] = network
|
||||
my_net = self.nosys_core.data.get_network(network_id)
|
||||
if my_net:
|
||||
self.nosys_core.fire_event(DynamicEvents.network_connection(network_id), network_id=network_id, peer=event.peer)
|
||||
|
||||
def on_module_connection(self, event):
|
||||
pass
|
||||
|
||||
|
||||
BIN
noSys/noSys.zip.bkp
Normal file
BIN
noSys/noSys.zip.bkp
Normal file
Binary file not shown.
103
noSys/noSysApiBlueprint.py
Normal file
103
noSys/noSysApiBlueprint.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from flask import Blueprint, make_response, request, jsonify, abort
|
||||
from flask_socketio import SocketIO, emit, join_room, leave_room
|
||||
|
||||
import os, signal, json, time
|
||||
import logging
|
||||
from threading import Thread
|
||||
|
||||
from libs.api.apiBlueprint import ApiBlueprint
|
||||
|
||||
class Blueprint(ApiBlueprint):
|
||||
def routes(self):
|
||||
from .noSys import NoSys
|
||||
self.nosys:NoSys = self.module
|
||||
from .noSysCore import NoSysCore
|
||||
self.nosys_core = self.nosys.nosys_core
|
||||
|
||||
@self.blueprint.route('/')
|
||||
def show():
|
||||
return self.nosys.name
|
||||
|
||||
@self.blueprint.route('/users')
|
||||
def users():
|
||||
if request.method == "GET":
|
||||
users = []
|
||||
for user in self.nosys_core.users.users.values():
|
||||
user_data = self.nosys.nosys_core.data.get_user(user.id)
|
||||
users.append(user_data)
|
||||
return jsonify(users)
|
||||
|
||||
@self.blueprint.route("/users/<path:user_id>", methods=["GET", "POST", "DELETE"])
|
||||
def user(user_id):
|
||||
if request.method == "GET":
|
||||
user_data = self.nosys.nosys_core.data.get_user(user_id)
|
||||
return jsonify(user_data)
|
||||
|
||||
@self.blueprint.route("/users/<path:user_id>/networks", methods=["GET", "POST", "DELETE"])
|
||||
def user_networks(user_id):
|
||||
if request.method == "GET":
|
||||
user_networks = self.nosys.nosys_core.data.get_user(user_id)["networks"]
|
||||
return jsonify(user_networks)
|
||||
|
||||
elif request.method == "POST":
|
||||
content:dict = request.json
|
||||
return jsonify()
|
||||
|
||||
@self.blueprint.route("/peers", methods=["GET", "POST"])
|
||||
def peers():
|
||||
if request.method == "GET":
|
||||
response = []
|
||||
for peer in self.nosys_core.peers.peers.values():
|
||||
response.append({
|
||||
"id":peer.id, "address":f"{peer.connection.address[0]}:{peer.connection.address[1]}",
|
||||
"bindAddress":f"{peer.connection.bind_address[0]}:{peer.connection.bind_address[1]}",
|
||||
"user":peer.connection.security.user, "status":peer.connection.status.name
|
||||
})
|
||||
return jsonify(response)
|
||||
|
||||
elif request.method == "POST":
|
||||
content:dict = request.json
|
||||
address = (content["address"]["ip"], int(content["address"]["port"]))
|
||||
bind_address = ("0.0.0.0", 0)
|
||||
if content.get("bindAddress"):
|
||||
bind_address = (content["bindAddress"]["ip"], int(content["bindAddress"]["port"]))
|
||||
user_pk = content["user"]
|
||||
connection_id = self.nosys_core.connections.connect(address=address, user_id=user_pk, bind_address=bind_address)
|
||||
return jsonify({"connectionId":connection_id})
|
||||
|
||||
@self.blueprint.route("/peers/<path:connection_id>", methods=["GET", "POST", "DELETE"])
|
||||
def peer(connection_id):
|
||||
peer = self.nosys_core.peers.peers.get(connection_id)
|
||||
if not peer:
|
||||
return jsonify({})
|
||||
|
||||
if request.method == "GET":
|
||||
return jsonify({
|
||||
"id":peer.id, "address":f"{peer.connection.address[0]}:{peer.connection.address[1]}",
|
||||
"bindAddress":f"{peer.connection.bind_address[0]}:{peer.connection.bind_address[1]}",
|
||||
"user":peer.connection.security.user, "status":peer.connection.status.name
|
||||
})
|
||||
|
||||
elif request.method == "POST":
|
||||
content:dict = request.json
|
||||
message = content.get("message")
|
||||
to_module = (content.get("toModule")["package"],content.get("toModule")["module"])
|
||||
encrypted = content.get("encrypted")
|
||||
self.nosys_core.dispatcher.send_message(message, peer.id, to_module, encrypted)
|
||||
return jsonify({"connectionId":connection_id})
|
||||
|
||||
elif request.method == "DELETE":
|
||||
peer.connection.close_connection()
|
||||
return jsonify({"connectionId":connection_id})
|
||||
|
||||
@self.blueprint.route('/restart')
|
||||
def restart():
|
||||
if request.method == "GET":
|
||||
self.nosys_core.restart_app()
|
||||
return jsonify({})
|
||||
|
||||
@self.blueprint.route('/test')
|
||||
def test():
|
||||
if request.method == "GET":
|
||||
self.nosys_core.test()
|
||||
return jsonify({})
|
||||
142
noSys/noSysCore.py
Normal file
142
noSys/noSysCore.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from libs.fspn.utils.wrapper_util import *
|
||||
from libs.fspn.utils.observable import Observable, Event
|
||||
from libs.fspn.protocol.server import Server
|
||||
from libs.fspn.protocol.connection import Connection, EVENTS as CONNECTION_EVENTS, STATUS as CONNECTION_STATUS
|
||||
from libs.fspn.protocol.security import Security, Pmc
|
||||
from libs.app.updater import Updater
|
||||
|
||||
# from libs.api.api import Api
|
||||
# from .zechoSocketio import HandlerSocketio
|
||||
|
||||
import pathlib
|
||||
from enum import Enum, auto
|
||||
import importlib
|
||||
import logging, traceback
|
||||
import json
|
||||
import random, time
|
||||
import secrets
|
||||
import os
|
||||
import sys
|
||||
|
||||
from libs.app.common.logging import get_logger, get_logger_buffer
|
||||
from libs.app.common.paths import ROOT_DIR
|
||||
from .peers import PeerManager
|
||||
from .users import UserManager
|
||||
from .modules import ModuleManager
|
||||
from libs.app.common.config import Config
|
||||
from .events import Events
|
||||
from .connections import ConnectionManager
|
||||
from .dispatcher import Dispatcher
|
||||
from .servers import ServerManager
|
||||
from .dataManager import DataManager
|
||||
from .networks import Networks
|
||||
|
||||
logger = get_logger(buffer=True)
|
||||
|
||||
class NoSysCore(Observable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def start(self):
|
||||
logger.info(f"--------------- Starting NoSys ---------------")
|
||||
self.peers = PeerManager(self)
|
||||
self.users = UserManager(self)
|
||||
|
||||
self.config = Config()
|
||||
self.data = DataManager(self)
|
||||
self.connections = ConnectionManager(self)
|
||||
self.dispatcher = Dispatcher(self)
|
||||
self.servers = ServerManager(self)
|
||||
|
||||
self.modules = ModuleManager(self)
|
||||
self.modules.setup()
|
||||
|
||||
self.fire_event(Events.READY)
|
||||
# self.test()
|
||||
|
||||
def restart_app(self):
|
||||
logger.warning("Restarting...")
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
|
||||
# ------------------------------------------------------------------------------------------
|
||||
|
||||
def wait_user(self):
|
||||
while True:
|
||||
if self.users.users:
|
||||
user_id = random.choice(list(self.users.users.keys()))
|
||||
break
|
||||
time.sleep(1)
|
||||
return user_id
|
||||
|
||||
def test_connection(self, user_id):
|
||||
p1= random.randint(46600, 49000)
|
||||
p2 = random.randint(46600, 49000)
|
||||
c1 = self.connections.connect(("127.0.0.1", p1), user_id, ("127.0.0.1", p2))
|
||||
c2 = self.connections.connect(("127.0.0.1", p2), user_id, ("127.0.0.1", p1))
|
||||
|
||||
p1 = self.peers.get_peer(c1)
|
||||
p2 = self.peers.get_peer(c2)
|
||||
|
||||
return p1,p2
|
||||
|
||||
def test_rendezvous(self, user_id):
|
||||
from libs.rendezvous.rendezvousServer import RendezvousServer
|
||||
rendezvous_server : RendezvousServer = self.modules.get("rendezvous", "rendezvousServer")
|
||||
rendezvous_server.run_server(user_id)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
from libs.rendezvous.rendezvousClient import RendezvousClient
|
||||
rendezvous_client : RendezvousClient = self.modules.get("rendezvous", "rendezvousClient")
|
||||
rendezvous_client.connect_to_server(("localhost", 40441), user_id)
|
||||
rendezvous_client.connect_to_server(("localhost", 40441), user_id)
|
||||
|
||||
def test_nosys(self):
|
||||
pass
|
||||
|
||||
def test(self):
|
||||
from .noSys import NoSys
|
||||
nosys : NoSys = self.modules.get("noSys", "noSys")
|
||||
|
||||
# try:
|
||||
# self.data.add_rendezvous("r1", "127.0.0.1:40441")
|
||||
# except:
|
||||
# pass
|
||||
# try:
|
||||
# net = self.data.create_network("test", "network for tests", "open_forum", [nosys.name])
|
||||
# except:
|
||||
# pass
|
||||
# self.data.network_assign_rendezvous(net.get("id"), "r1")
|
||||
|
||||
user_id = self.wait_user()
|
||||
|
||||
from libs.rendezvous.rendezvousServer import RendezvousServer
|
||||
rendezvous_server : RendezvousServer = self.modules.get("rendezvous", "rendezvousServer")
|
||||
rendezvous_server.run_server(user_id)
|
||||
# time.sleep(2)
|
||||
# self.data.user_add_network(user_id, "629985fd999a675072a2e3f22f74e60c62e8c5b8efb21478c9b04e7ff2fe9779")
|
||||
|
||||
# peer1, peer2 = self.test_connection(user_id)
|
||||
|
||||
|
||||
# from libs.fileTransfer.fileTransfer import FileTransfer
|
||||
# file_transfer:FileTransfer = self.modules[("fileTransfer", "fileTransfer")]
|
||||
# file_transfer.subscribe_module_file_events(self.module_id, self.on_file_event)
|
||||
|
||||
# file_transfer.send_file(r"C:\Users\lucas\Videos\Captures\Call of Duty® 2025-07-25 10-23-55.mp4", c2, self.module_id)
|
||||
|
||||
# def on_file_event(self, event):
|
||||
# from libs.fileTransfer.fileTransfer import File, FileEvents
|
||||
# file:File = event.file
|
||||
# file.subscribe_event(FileEvents.ON_FILE_COMPLETED.name, self.on_file_done)
|
||||
# file.subscribe_event(FileEvents.ON_FILE_ERROR.name, self.on_file_done)
|
||||
# file.subscribe_event(FileEvents.ON_FILE_UPDATE.name, self.on_file_update)
|
||||
# file.approve_transfer(True)
|
||||
|
||||
# def on_file_done(self, event):
|
||||
# logger.warning(f"FILE COMPLETED, DO SOMETHING {event.source.__dict__}")
|
||||
|
||||
# def on_file_update(self, event):
|
||||
# file = event.source
|
||||
# total_true = sum(bool(x) for x in file.parts)
|
||||
# logger.warning(f"FILE UPDATE, {total_true}|{len(file.parts)}")
|
||||
64
noSys/noSysModule.py
Normal file
64
noSys/noSysModule.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .noSysCore import NoSysCore
|
||||
|
||||
from libs.fspn.utils.observable import Observable
|
||||
from .events import Events, DynamicEvents
|
||||
|
||||
|
||||
class NoSysModule(Observable):
|
||||
"""Base class for all modules in NoSys."""
|
||||
def __init__(self, nosys_core:"NoSysCore"):
|
||||
super().__init__()
|
||||
self.nosys_core:NoSysCore = nosys_core
|
||||
self.id = self._get_module_id()
|
||||
self.config = self.nosys_core.config.get(self.package_id)
|
||||
self.networks = self._get_module_networks()
|
||||
|
||||
self.nosys_core.subscribe_event(Events.READY, self.on_nosys_ready)
|
||||
self.nosys_core.subscribe_event(DynamicEvents.module_connection(self.package_id, self.module_id), self.on_module_connection)
|
||||
self.nosys_core.subscribe_event(DynamicEvents.module_disconnection(self.package_id, self.module_id), self.on_module_disconnection)
|
||||
self.nosys_core.subscribe_event(DynamicEvents.module_message(self.package_id, self.module_id), self.on_module_message)
|
||||
|
||||
for network in self.networks:
|
||||
self.nosys_core.subscribe_event(DynamicEvents.network_connection(network.get("id")), self.on_network_connection)
|
||||
self.nosys_core.subscribe_event(DynamicEvents.network_disconnection(network.get("id")), self.on_network_disconnection)
|
||||
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def on_nosys_ready(self, event):
|
||||
pass
|
||||
|
||||
def on_module_connection(self, event):
|
||||
pass
|
||||
|
||||
def on_module_disconnection(self, event):
|
||||
pass
|
||||
|
||||
def on_module_message(self, event):
|
||||
pass
|
||||
|
||||
def on_network_connection(self, event):
|
||||
pass
|
||||
|
||||
def on_network_disconnection(self, event):
|
||||
pass
|
||||
|
||||
def teardown_module(self):
|
||||
pass
|
||||
|
||||
|
||||
def _get_module_id(self):
|
||||
module_path = self.__class__.__module__
|
||||
parts = module_path.split(".")
|
||||
|
||||
self.package_id = parts[1]
|
||||
self.module_id = parts[2]
|
||||
self.name = self.package_id+"_"+self.module_id
|
||||
|
||||
return (self.package_id, self.module_id)
|
||||
|
||||
def _get_module_networks(self):
|
||||
return self.nosys_core.data.network_find_by_module(self.name)
|
||||
57
noSys/noSysModuleServer.py
Normal file
57
noSys/noSysModuleServer.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from .noSysModule import NoSysModule
|
||||
from .events import Events
|
||||
from .peers import Peer
|
||||
from libs.fspn.protocol.server import Server
|
||||
from libs.fspn.protocol.connection import Connection
|
||||
|
||||
class NoSysModuleServer(NoSysModule, Server):
|
||||
def __init__(self, nosys_core):
|
||||
NoSysModule.__init__(self, nosys_core)
|
||||
Server.__init__(self)
|
||||
|
||||
self.host = "0.0.0.0"
|
||||
self.port = 0
|
||||
|
||||
self.clients : dict[str, Peer] = {}
|
||||
|
||||
def setup(self):
|
||||
self.nosys_core.subscribe_event(Events.PEER_CONNECTED, self._on_peer_connected)
|
||||
self.nosys_core.subscribe_event(Events.PEER_DISCONNECTED, self._on_peer_disconnected)
|
||||
|
||||
def run_server(self, user_id):
|
||||
self.nosys_core.servers.run_server(self, user_id, (self.host, self.port))
|
||||
|
||||
def on_server_connection(self, event):
|
||||
print("CONNECTION RAW")
|
||||
connection: Connection = event.source
|
||||
peer = Peer(connection.id, connection)
|
||||
self.nosys_core.peers.add_peer(peer)
|
||||
self.clients[peer.id] = peer
|
||||
self.nosys_core.connections._on_connection(event)
|
||||
|
||||
def on_server_disconnection(self, event):
|
||||
print("DISCONNECTION RAW")
|
||||
self.nosys_core.connections._on_disconnection(event)
|
||||
|
||||
def on_server_message(self, event):
|
||||
print("MESSAGE RAW")
|
||||
self.nosys_core.dispatcher.on_message(event)
|
||||
|
||||
def _on_peer_connected(self, event):
|
||||
peer:Peer = event.peer
|
||||
print(peer.id, self.clients)
|
||||
if peer.id in self.clients:
|
||||
print("PEER CONNECTED")
|
||||
self.on_connection(peer)
|
||||
|
||||
def _on_peer_disconnected(self, event):
|
||||
peer:Peer = event.peer
|
||||
if peer.id in self.clients:
|
||||
print("PEER DISCONNECTED")
|
||||
self.on_disconnection(peer)
|
||||
|
||||
def on_connection(self, peer:Peer):
|
||||
pass
|
||||
|
||||
def on_disconnection(self, peer:Peer):
|
||||
pass
|
||||
30
noSys/noSysSocketio.py
Normal file
30
noSys/noSysSocketio.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from flask import Blueprint, make_response, request, jsonify, session, request
|
||||
from flask_socketio import SocketIO, emit, join_room, leave_room
|
||||
|
||||
from libs.api.eventsSocketio import EventsSocketio
|
||||
|
||||
import os, json
|
||||
import logging
|
||||
|
||||
class HandlerSocketio(EventsSocketio):
|
||||
def events(self):
|
||||
@self.socketio.on("message", namespace=self.namespace)
|
||||
def on_message(*args, **kwargs):
|
||||
print('NoSys Message',args, kwargs)
|
||||
self.emit("message", f"Message from Nosys {args[0]}")
|
||||
|
||||
@self.socketio.on("ola", namespace=self.namespace)
|
||||
def on_test(*args, **kwargs):
|
||||
print('Zecho ola',args, kwargs)
|
||||
self.emit("ola",f"Ola from Nosys {args[0]}")
|
||||
|
||||
|
||||
|
||||
def send_test(self, message):
|
||||
self.emit("test", message)
|
||||
|
||||
def emit_new_user(self, user):
|
||||
self.emit("newUser", user)
|
||||
|
||||
def emit_new_peer(self, peer):
|
||||
self.emit("newPeer", peer.id)
|
||||
84
noSys/peers.py
Normal file
84
noSys/peers.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import enum
|
||||
class ConnectionState(enum.Enum):
|
||||
DISCONNECTED = 0
|
||||
CONNECTING = 1
|
||||
CONNECTED = 2
|
||||
|
||||
from libs.fspn.protocol.connection import Connection
|
||||
|
||||
class Peer:
|
||||
"""Represents a peer in the network."""
|
||||
|
||||
def __init__(self, id: str, connection: Connection):
|
||||
self.id = id
|
||||
self.connection:Connection = connection
|
||||
self.state = ConnectionState.DISCONNECTED
|
||||
self.modules = {}
|
||||
self.networks = {}
|
||||
|
||||
def has_module(self, module_id:str):
|
||||
return module_id in self.modules
|
||||
|
||||
def has_network(self, network_id:str):
|
||||
return network_id in self.networks
|
||||
|
||||
def is_connected(self):
|
||||
return self.state == ConnectionState.CONNECTED
|
||||
|
||||
def get_my_user(self):
|
||||
return self.connection.security.user
|
||||
|
||||
def get_peer_user(self):
|
||||
return self.connection.security.peer_user
|
||||
|
||||
|
||||
class PeerManager:
|
||||
"""Handles connection and storage of peers."""
|
||||
|
||||
def __init__(self, nosys_core):
|
||||
from .noSysCore import NoSysCore
|
||||
self.nosys_core:NoSysCore = nosys_core
|
||||
self.peers:dict[str, Peer] = {}
|
||||
|
||||
def create_peer(self, connection:Connection):
|
||||
return Peer(connection.id, connection)
|
||||
|
||||
def add_peer(self, peer: Peer):
|
||||
self.peers[peer.id] = peer
|
||||
|
||||
def get_peer(self, peer_id:str) -> Peer:
|
||||
return self.peers.get(peer_id, None)
|
||||
|
||||
def remove_peer(self, peer_id: str):
|
||||
self.peers.pop(peer_id, None)
|
||||
|
||||
def get_online_peers(self) -> list[Peer]:
|
||||
return self.get_peers_in_state(ConnectionState.CONNECTED)
|
||||
|
||||
def get_connecting_peers(self) -> list[Peer]:
|
||||
return self.get_peers_in_state(ConnectionState.CONNECTING)
|
||||
|
||||
def get_offline_peers(self) -> list[Peer]:
|
||||
return self.get_peers_in_state(ConnectionState.DISCONNECTED)
|
||||
|
||||
def get_peers_in_state(self, state):
|
||||
peers:list[Peer] = []
|
||||
for id, peer in self.peers.items():
|
||||
if peer.state == state:
|
||||
peers.append(peer)
|
||||
return peers
|
||||
|
||||
def get_by_peer_user_id(self, user_id):
|
||||
peers:list[Peer] = []
|
||||
for id, peer in self.peers.items():
|
||||
if peer.get_peer_user() == user_id and peer.is_connected():
|
||||
peers.append(peer)
|
||||
return peers
|
||||
|
||||
def get_by_network_id(self, networks):
|
||||
peers:list[Peer] = []
|
||||
for id, peer in self.peers.items():
|
||||
for peer_network_id in peer.networks:
|
||||
if peer_network_id in networks:
|
||||
peers.append(peer)
|
||||
return peers
|
||||
18
noSys/servers.py
Normal file
18
noSys/servers.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from .noSysModuleServer import NoSysModuleServer
|
||||
|
||||
class ServerManager:
|
||||
"""Handles running Server modules."""
|
||||
|
||||
def __init__(self, nosys_core):
|
||||
from .noSysCore import NoSysCore
|
||||
self.nosys_core:NoSysCore = nosys_core
|
||||
|
||||
self.servers: dict[tuple[str, int], NoSysModuleServer] = {}
|
||||
|
||||
def run_server(self, module:NoSysModuleServer, user_id=None, bind_address=None):
|
||||
user = self.nosys_core.users.get_user(user_id)
|
||||
if not user:
|
||||
raise Exception("Cannot run server, user missing")
|
||||
|
||||
module.run(user.public_key, self.nosys_core.modules.pmc, bind_address)
|
||||
self.servers[module.bind_address] = module
|
||||
26
noSys/users.py
Normal file
26
noSys/users.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from .events import Events
|
||||
|
||||
class User:
|
||||
"""Represents a single user in the system."""
|
||||
|
||||
def __init__(self, public_key, networks=[]):
|
||||
self.id = public_key
|
||||
self.public_key = public_key
|
||||
self.networks = networks
|
||||
|
||||
|
||||
class UserManager:
|
||||
"""Handles user creation, storage, and retrieval."""
|
||||
|
||||
def __init__(self, nosys_core):
|
||||
from .noSysCore import NoSysCore
|
||||
self.nosys_core: NoSysCore = nosys_core
|
||||
self.users: dict[str, User] = {}
|
||||
|
||||
def add_user(self, user: User):
|
||||
if user.public_key not in self.users:
|
||||
self.users[user.public_key] = user
|
||||
self.nosys_core.fire_event(Events.USER_ADDED, user_id = user.id)
|
||||
|
||||
def get_user(self, user_id: str):
|
||||
return self.users.get(user_id)
|
||||
126
noSys/vue/api/noSysApi.js
Normal file
126
noSys/vue/api/noSysApi.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useNoSysStore } from '../stores/noSysStore'
|
||||
|
||||
const noSysApiUrl = "/api/noSys";
|
||||
const noSysStore = useNoSysStore()
|
||||
|
||||
export const noSysApi = {
|
||||
|
||||
async getUsers(){
|
||||
let users = {}
|
||||
try {
|
||||
const response = await fetch(noSysApiUrl+'/users')
|
||||
if (!response.ok) throw new Error("Error fetching users:")
|
||||
users = await response.json()
|
||||
return users;
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getUser(userId){
|
||||
let user = {}
|
||||
try {
|
||||
const response = await fetch(noSysApiUrl+'/users/'+userId)
|
||||
if (!response.ok) throw new Error("Error fetching users:")
|
||||
user = await response.json()
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getConfigs(){
|
||||
try {
|
||||
const response = await fetch(noSysApiUrl+'/configs')
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Error fetching configs: ${response.status} - ${errorText}`);
|
||||
}
|
||||
const result = await response.json()
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error fetching configs:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async connectAddress(user, ip, port){
|
||||
const data = {"user": user, "address": {"ip": ip,"port": port}};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json',},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
try {
|
||||
const response = await fetch(noSysApiUrl+"/peers", requestOptions);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Error fetching users: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async listPeers(){
|
||||
try {
|
||||
const response = await fetch(noSysApiUrl+'/peers')
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Error fetching peers: ${response.status} - ${errorText}`);
|
||||
}
|
||||
const result = await response.json()
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error fetching peers:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async disconnectPeer(connectionId){
|
||||
const data = {};
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
headers: {'Content-Type': 'application/json',},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
try {
|
||||
const response = await fetch(noSysApiUrl+"/peers/"+connectionId, requestOptions);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Error fetching users: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async listNetworks(){
|
||||
try {
|
||||
const response = await fetch(noSysApiUrl+'/networks')
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Error fetching networks: ${response.status} - ${errorText}`);
|
||||
}
|
||||
const result = await response.json()
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error fetching networks:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}
|
||||
15
noSys/vue/api/socketEvents.js
Normal file
15
noSys/vue/api/socketEvents.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useNoSysStore } from '../stores/noSysStore'
|
||||
|
||||
const noSysStore = useNoSysStore()
|
||||
|
||||
export default function registerSocketEvents(socket) {
|
||||
socket.on('connect', (data) => {
|
||||
console.log('Connected noSys')
|
||||
}),
|
||||
|
||||
socket.on('newUser', (data) => {
|
||||
noSysStore.addUser({publicKey:data})
|
||||
console.log('New user', data)
|
||||
})
|
||||
|
||||
}
|
||||
282
noSys/vue/components/ConnectionsTab.vue
Normal file
282
noSys/vue/components/ConnectionsTab.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<script setup>
|
||||
import Button from '@/components/buttons/Button.vue';
|
||||
import InputText from '@/components/inputs/InputText.vue';
|
||||
import Label from '@/components/labels/Label.vue';
|
||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue';
|
||||
import { noSysApi } from '../api/noSysApi';
|
||||
import InputComboBox from '@/components/inputs/InputComboBox.vue';
|
||||
import { useNoSysStore } from '../stores/noSysStore'
|
||||
import { getSocket } from '@/plugins/socketioManager'
|
||||
import Card from '@/components/cards/Card.vue';
|
||||
import { ArrowPathIcon, EllipsisHorizontalIcon, GlobeAltIcon, LinkIcon, MapIcon, WifiIcon, XMarkIcon } from '@heroicons/vue/24/solid';
|
||||
import { TabPanel} from '@headlessui/vue'
|
||||
|
||||
var socket = null
|
||||
const noSysStore = useNoSysStore()
|
||||
|
||||
const ip = ref("n0sys.duckdns.org")
|
||||
const port = ref("30331")
|
||||
const user = ref()
|
||||
const peers = ref([])
|
||||
|
||||
async function listUsers(){
|
||||
noSysStore.users.value = await noSysApi.getUsers()
|
||||
}
|
||||
|
||||
async function connect(){
|
||||
const response = await noSysApi.connectAddress(user.value.user, ip.value, port.value)
|
||||
}
|
||||
|
||||
async function reconnect(connectionId){
|
||||
// const response = await noSysApi.connectAddress(user.value.user, ip.value, port.value)
|
||||
}
|
||||
|
||||
async function disconnect(connectionId){
|
||||
const response = await noSysApi.disconnectPeer(connectionId)
|
||||
}
|
||||
|
||||
async function listPeers(){
|
||||
peers.value = await noSysApi.listPeers()
|
||||
}
|
||||
|
||||
onActivated(async () => {
|
||||
socket = await getSocket("noSys_noSys")
|
||||
socket.on('tempEvent', onTemporaryEvent)
|
||||
|
||||
listUsers()
|
||||
listPeers()
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
socket.off('tempEvent')
|
||||
});
|
||||
|
||||
function onTemporaryEvent(data) {
|
||||
console.log('Received temporary event in HomeView:', data)
|
||||
}
|
||||
|
||||
// function sendMessage(){
|
||||
// messageElement = document.getElementById("message")
|
||||
// message = messageElement.value;
|
||||
// if (connectionClicked && message){
|
||||
// const data = {"message":{"action":"test", "data":message},"toModule": {"package": "noSys","module": "noSys"},"encrypted": true};
|
||||
// const requestOptions = {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify(data),
|
||||
// };
|
||||
|
||||
// fetch(noSysApiUrl+"/peers/"+connectionClicked, requestOptions)
|
||||
// .then(response => {
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Network response was not ok');
|
||||
// }
|
||||
// return response.json();
|
||||
// })
|
||||
// .then(data => {
|
||||
// console.log(data);
|
||||
// addMessage(connectionClicked, message, false)
|
||||
// messageElement.value = ""
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error
|
||||
|
||||
// ('Error:', error);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel>
|
||||
<div class="container mx-auto px-6 py-1 space-y-8">
|
||||
<div class="grid md:grid-cols-4 gap-6">
|
||||
<Card class="border-yellow-400/20 p-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center">
|
||||
<WifiIcon class="text-green-400 w-6 h-6"/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{{peers.filter((p) => p.status === "CONNECTED").length}}
|
||||
</p>
|
||||
<p class="text-gray-400 text-sm">Connected</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class=" border-yellow-400/20 p-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-12 h-12 bg-yellow-500/20 rounded-lg flex items-center justify-center">
|
||||
<EllipsisHorizontalIcon class="text-yellow-400 h-6 w-6" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{{peers.filter((p) => p.status === "CONNECTING").length}}
|
||||
</p>
|
||||
<p class="text-gray-400 text-sm">Connecting</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class=" border-yellow-400/20 p-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-12 h-12 bg-red-500/20 rounded-lg flex items-center justify-center">
|
||||
<XMarkIcon class="text-red-400 h-6 w-6"/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{{peers.filter((p) => p.status === "ERROR").length}}
|
||||
</p>
|
||||
<p class="text-gray-400 text-sm">Failed</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class=" border-yellow-400/20 p-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center">
|
||||
<LinkIcon class="text-blue-400 h-6 w-6"/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-white">{{peers.length}}</p>
|
||||
<p class="text-gray-400 text-sm">Total</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card class="bg-black border-yellow-400/20 p-8">
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<LinkIcon class="text-yellow-400 h-6 w-6" />
|
||||
<h2 class="text-2xl font-bold text-yellow-400">Connect to Peer</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<Label class="text-gray-300 mb-2 block">IP Address</Label>
|
||||
<InputText id="ip" type="text" v-model="ip" placeholder="" class="border-yellow-400/30 w-full"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-300 mb-2 block">Port</Label>
|
||||
<InputText id="port" type="text" v-model="port" placeholder="" class="border-yellow-400/30 w-full"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-300 mb-2 block">User</Label>
|
||||
<InputComboBox v-model="user" :items="noSysStore.users.value" labelKey="user" keyField="user" class="border-yellow-400/30"></InputComboBox>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button @click="connect()" class="w-full bg-yellow-400 text-black hover:bg-yellow-300 font-bold">Connect</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class=" border-yellow-400/20 p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<GlobeAltIcon class="text-yellow-400 h-6 w-6" />
|
||||
<h2 class="text-2xl font-bold text-yellow-400">Active Connections</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="peers.length === 0" class="text-center py-12">
|
||||
<WifiIcon class="text-gray-600 mx-auto mb-4 h-7 w-7" />
|
||||
<p class="text-gray-400 text-lg">No connections established</p>
|
||||
<p class="text-gray-500 text-sm">Connect to peers to start building the decentralized network</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="peer in peers" :key="peer.id" class="bg-black border border-yellow-400/20 rounded-lg p-6">
|
||||
<div class="grid lg:grid-cols-7 gap-4 items-center">
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Connection ID</Label>
|
||||
<p class="text-white font-mono text-sm">{{ peer.id }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Address</Label>
|
||||
<p class="text-white text-sm">
|
||||
{{peer.address}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Bind Address</Label>
|
||||
<p class="text-white text-sm">
|
||||
{{peer.bindAddress}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Status</Label>
|
||||
<div class="flex items-center space-x-2"
|
||||
:class="{
|
||||
'text-green-400': peer.status === 'CONNECTED',
|
||||
'text-blue-400': peer.status === 'CONNECTING',
|
||||
'text-yellow-400': peer.status === 'HANDSHAKING',
|
||||
'text-red-400': peer.status === 'DISCONNECTED',
|
||||
'text-gray-300': !['CONNECTED', 'CONNECTING', 'HANDSHAKING', 'DISCONNECTED'].includes(peer.status)
|
||||
}">
|
||||
{{peer.status}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Users</Label>
|
||||
<p class="text-sm">
|
||||
<span class="text-yellow-400">{{peer.user}}</span>
|
||||
<!-- TODO MyUser, PeerUser/ProofOfWork -->
|
||||
<span class="text-gray-400"> ↔ </span>
|
||||
<span class="text-blue-400">{{peer.user}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Last Activity</Label>
|
||||
<!-- TODO Last Activity -->
|
||||
<p class="text-white text-sm">{{ new Date().toLocaleTimeString() }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="peer.status === 'CONNECTED'"
|
||||
@click="disconnect(peer.id)"
|
||||
class="bg-red-500 hover:bg-red-600 text-white"
|
||||
>
|
||||
<XMarkIcon class="mr-1 h-4 w-4" />
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button v-if="peer.status === 'DISCONNECTED'"
|
||||
@click="reconnect(peer.id)"
|
||||
class="bg-green-500 hover:bg-green-600 text-white"
|
||||
>
|
||||
<ArrowPathIcon class="mr-1 h-4 w-4" />
|
||||
Reconnect
|
||||
</Button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-yellow-400/10">
|
||||
<div class="flex justify-between text-xs text-gray-400">
|
||||
<!-- Connected At -->
|
||||
<span>Connected: {{new Date(peer.connectedAt).toLocaleString()}}</span>
|
||||
<span>
|
||||
Duration: {{Math.floor((Date.now() - new Date(peer.connectedAt).getTime()) / 60000)}}{" "}
|
||||
minutes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</TabPanel>
|
||||
</template>
|
||||
183
noSys/vue/components/NetworksTabs.vue
Normal file
183
noSys/vue/components/NetworksTabs.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script setup>
|
||||
import Button from '@/components/buttons/Button.vue';
|
||||
import Card from '@/components/cards/Card.vue';
|
||||
import InputComboBox from '@/components/inputs/InputComboBox.vue';
|
||||
import InputText from '@/components/inputs/InputText.vue';
|
||||
import Label from '@/components/labels/Label.vue';
|
||||
import { TabPanel} from '@headlessui/vue'
|
||||
import { CogIcon, MagnifyingGlassIcon, PlusIcon, TrashIcon } from '@heroicons/vue/24/solid';
|
||||
import { noSysApi } from '../api/noSysApi';
|
||||
import { useNoSysStore } from '../stores/noSysStore';
|
||||
import { onActivated, onDeactivated } from 'vue';
|
||||
|
||||
const noSysStore = useNoSysStore()
|
||||
|
||||
async function listNetworks(){
|
||||
noSysStore.networks.value = await noSysApi.listNetworks()
|
||||
}
|
||||
|
||||
onActivated(()=>{
|
||||
listNetworks()
|
||||
})
|
||||
|
||||
onDeactivated(()=>{
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel>
|
||||
<Card class="border-yellow-400/20 p-8 mb-8">
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<PlusIcon class="text-yellow-400 w-6 h-6"/>
|
||||
<h2 class="text-2xl font-bold text-yellow-400">Add Network</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label class="text-gray-300 mb-2 block">Network ID</Label>
|
||||
<InputText
|
||||
class="bg-black border-yellow-400/30 text-white"
|
||||
placeholder="ID"
|
||||
/>
|
||||
</div>
|
||||
<Button class="mt-6 bg-yellow-400 text-black hover:bg-yellow-300 font-bold w-full">
|
||||
<MagnifyingGlassIcon class="w-4 h-4 mr-2 text-black" />
|
||||
<p class="text-black">Search Network</p>
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-300 mb-2 block">Network Name</Label>
|
||||
<InputText
|
||||
class="bg-black border-yellow-400/30 text-white"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-300 mb-2 block">Network Description</Label>
|
||||
<InputText
|
||||
class="bg-black border-yellow-400/30 text-white"
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Label class="text-gray-300 mb-2 block">Modules</Label>
|
||||
<InputComboBox
|
||||
class="bg-black border-yellow-400/30 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Label class="text-gray-300 mb-2 block">Network Type</Label>
|
||||
<InputComboBox
|
||||
class="bg-black border-yellow-400/30 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-3">
|
||||
<Button class="mt-6 bg-yellow-400 text-black hover:bg-yellow-300 font-bold w-full">
|
||||
<PlusIcon class="w-4 h-4 mr-2 text-black" />
|
||||
<p class="text-black">Add Network</p>
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="border-yellow-400/20 p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- <Network class="text-yellow-400" size={24} /> -->
|
||||
<h2 class="text-2xl font-bold text-yellow-400">Networks</h2>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400">{networks.length} networks</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="!noSysStore.networks.value" class="text-center py-12">
|
||||
<Network size={48} class="text-gray-600 mx-auto mb-4" />
|
||||
<p class="text-gray-400 text-lg">No networks created</p>
|
||||
<p class="text-gray-500 text-sm">Create a network to start connecting with peers</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="network in noSysStore.networks.value" key={network.id} class="bg-black border border-yellow-400/20 rounded-lg p-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3 mb-3">
|
||||
<h3 class="text-xl font-bold text-white">{{network.data.name}}</h3>
|
||||
<span class="bg-yellow-400/20 text-yellow-400 px-2 py-1 rounded text-sm">
|
||||
{{network.data.id}}
|
||||
</span>
|
||||
<span
|
||||
class="px-2 py-1 rounded text-sm"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-4 gap-4 mb-4">
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Total Connections</Label>
|
||||
<p class="text-2xl font-bold text-green-400">{{network.state.peers.length}}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Status</Label>
|
||||
<p
|
||||
class=""
|
||||
>
|
||||
{{network.state.status}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Auto Fill</Label>
|
||||
<p class="">
|
||||
{{network.state.managed}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-gray-400 text-xs">Users</Label>
|
||||
<p class="">
|
||||
{{network.state.users.length}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<Label class="text-gray-400 text-xs">Modules</Label>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div v-for="module in network.data.modules" class="flex flex-wrap mt-1">
|
||||
<span class="bg-blue-500/20 text-blue-400 px-2 py-1 rounded text-sm">
|
||||
{{module}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<Button
|
||||
class="border-yellow-400 text-yellow-400 hover:bg-yellow-400 hover:text-black bg-transparent"
|
||||
>
|
||||
<CogIcon class="h-4 w-4 mr-1" />
|
||||
Configure
|
||||
</Button>
|
||||
<Button
|
||||
class="bg-red-500 hover:bg-red-600 text-white"
|
||||
>
|
||||
<TrashIcon class="h-4 w-4mr-1" />
|
||||
Quit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</template>
|
||||
7
noSys/vue/router.js
Normal file
7
noSys/vue/router.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import HomeView from "./views/HomeView.vue";
|
||||
|
||||
const routes = [
|
||||
{path: '/', name:'noSys', component: HomeView},
|
||||
]
|
||||
|
||||
export {routes};
|
||||
57
noSys/vue/stores/noSysStore.js
Normal file
57
noSys/vue/stores/noSysStore.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useNoSysStore = defineStore('noSysStore', {
|
||||
state: () => ({
|
||||
users: [], // [{ publicKey }]
|
||||
connections: [], // [{ publicKey, connectedAt, network }]
|
||||
networks: [],
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getUserByKey: (state) => (key) => {
|
||||
return state.users.find(u => u.publicKey === key)
|
||||
},
|
||||
getConnectionsByNetwork: (state) => (network) => {
|
||||
return state.connections.filter(c => c.network === network)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
addUser(user) {
|
||||
if (!this.users.find(u => u.publicKey === user.publicKey)) {
|
||||
this.users.push(user)
|
||||
}
|
||||
},
|
||||
|
||||
removeUser(publicKey) {
|
||||
this.users = this.users.filter(u => u.publicKey !== publicKey)
|
||||
},
|
||||
|
||||
updateUser(updated) {
|
||||
const index = this.users.findIndex(u => u.publicKey === updated.publicKey)
|
||||
if (index !== -1) this.users[index] = { ...this.users[index], ...updated }
|
||||
},
|
||||
|
||||
addConnection(conn) {
|
||||
const exists = this.connections.find(
|
||||
c => c.publicKey === conn.publicKey && c.network === conn.network
|
||||
)
|
||||
if (!exists) {
|
||||
this.connections.push({
|
||||
...conn,
|
||||
connectedAt: conn.connectedAt || new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
removeConnection(publicKey, network = null) {
|
||||
this.connections = this.connections.filter(
|
||||
c => c.publicKey !== publicKey || (network && c.network !== network)
|
||||
)
|
||||
},
|
||||
|
||||
clearConnections() {
|
||||
this.connections = []
|
||||
}
|
||||
}
|
||||
})
|
||||
59
noSys/vue/views/HomeView.vue
Normal file
59
noSys/vue/views/HomeView.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import Button from '@/components/buttons/Button.vue';
|
||||
import InputText from '@/components/inputs/InputText.vue';
|
||||
import Label from '@/components/labels/Label.vue';
|
||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue';
|
||||
import { noSysApi } from '../api/noSysApi';
|
||||
import InputComboBox from '@/components/inputs/InputComboBox.vue';
|
||||
import { useNoSysStore } from '../stores/noSysStore'
|
||||
import { getSocket } from '@/plugins/socketioManager'
|
||||
import Card from '@/components/cards/Card.vue';
|
||||
import { ArrowPathIcon, EllipsisHorizontalIcon, GlobeAltIcon, LinkIcon, MapIcon, WifiIcon, XMarkIcon } from '@heroicons/vue/24/solid';
|
||||
import ConnectionsTab from '../components/ConnectionsTab.vue';
|
||||
import NetworksTabs from '../components/NetworksTabs.vue';
|
||||
import { TabGroup, TabList, Tab, TabPanels} from '@headlessui/vue'
|
||||
|
||||
|
||||
const tabItems = [
|
||||
{label:"Connections", icon:LinkIcon, tabComponent:ConnectionsTab},
|
||||
{label:"Networks", icon:MapIcon, tabComponent:NetworksTabs},
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-black text-yellow-400">
|
||||
<header class="border-b border-yellow-400/20 py-6">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-3xl font-black">
|
||||
<span class="text-yellow-400">NO</span>
|
||||
<span class="text-white">SYS</span>
|
||||
</div>
|
||||
<div class="w-1 h-8 bg-yellow-400"></div>
|
||||
<span class="text-gray-400">Peer-to-Peer Network Connection</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-6">
|
||||
<TabGroup>
|
||||
<TabList class="flex flex-wrap gap-2 border-b border-yellow-400/20">
|
||||
<Tab v-for="tab in tabItems" as="template" :key="tab" v-slot="{ selected }">
|
||||
<button class="flex items-center space-x-2 px-6 py-3 font-semibold transition-all duration-300 border-b-2"
|
||||
:class="{ 'text-yellow-400 border-yellow-400': selected, 'text-gray-400 border-transparent hover:text-yellow-400 hover:border-yellow-400/50': !selected }">
|
||||
<component :is="tab.icon" class="h-4 w-4"></component>
|
||||
<span>{{ tab.label }}</span>
|
||||
</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="mt-6 space-y-6" v-for="tab in tabItems" :key="tab.label">
|
||||
<component :is="tab.tabComponent"></component>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
Reference in New Issue
Block a user