diff --git a/core/cli_api.py b/core/cli_api.py index 5df4125..976bf2f 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -14,6 +14,7 @@ CONFIG_FILE = '/etc/hysteria/config.json' CONFIG_ENV_FILE = '/etc/hysteria/.configs.env' WEBPANEL_ENV_FILE = '/etc/hysteria/core/scripts/webpanel/.env' NORMALSUB_ENV_FILE = '/etc/hysteria/core/scripts/normalsub/.env' +NODES_JSON_PATH = "/etc/hysteria/nodes.json" class Command(Enum): diff --git a/core/scripts/webpanel/routers/api/v1/config/ip.py b/core/scripts/webpanel/routers/api/v1/config/ip.py index 211e03d..bcd8398 100644 --- a/core/scripts/webpanel/routers/api/v1/config/ip.py +++ b/core/scripts/webpanel/routers/api/v1/config/ip.py @@ -1,44 +1,43 @@ from fastapi import APIRouter, HTTPException from ..schema.response import DetailResponse +import json +import os - -from ..schema.config.ip import EditInputBody, StatusResponse +from ..schema.config.ip import ( + EditInputBody, + StatusResponse, + AddNodeBody, + DeleteNodeBody, + NodeListResponse +) import cli_api router = APIRouter() -@router.get('/get', response_model=StatusResponse, summary='Get IP Status') +@router.get('/get', response_model=StatusResponse, summary='Get Local Server IP Status') async def get_ip_api(): """ - Retrieves the current status of IP addresses. + Retrieves the current status of the main server's IP addresses. Returns: StatusResponse: A response model containing the current IP address details. - - Raises: - HTTPException: If the IP status is not available (404) or if there is an error processing the request (400). """ try: ipv4, ipv6 = cli_api.get_ip_address() - if ipv4 or ipv6: - return StatusResponse(ipv4=ipv4, ipv6=ipv6) # type: ignore - raise HTTPException(status_code=404, detail='IP status not available.') + return StatusResponse(ipv4=ipv4, ipv6=ipv6) except Exception as e: raise HTTPException(status_code=400, detail=f'Error: {str(e)}') -@router.get('/add', response_model=DetailResponse, summary='Add IP') +@router.get('/add', response_model=DetailResponse, summary='Detect and Add Local Server IP') async def add_ip_api(): """ Adds the auto-detected IP addresses to the .configs.env file. Returns: A DetailResponse with a message indicating the IP addresses were added successfully. - - Raises: - HTTPException: if an error occurs while adding the IP addresses. """ try: cli_api.add_ip_address() @@ -47,19 +46,13 @@ async def add_ip_api(): raise HTTPException(status_code=400, detail=f'Error: {str(e)}') -@router.post('/edit', response_model=DetailResponse, summary='Edit IP') +@router.post('/edit', response_model=DetailResponse, summary='Edit Local Server IP') async def edit_ip_api(body: EditInputBody): """ - Edits the IP addresses in the .configs.env file. + Edits the main server's IP addresses in the .configs.env file. Args: body: An instance of EditInputBody containing the new IPv4 and/or IPv6 addresses. - - Returns: - A DetailResponse with a message indicating the IP addresses were edited successfully. - - Raises: - HTTPException: if an error occurs while editing the IP addresses. """ try: if not body.ipv4 and not body.ipv6: @@ -69,3 +62,54 @@ async def edit_ip_api(body: EditInputBody): return DetailResponse(detail='IP address edited successfully.') except Exception as e: raise HTTPException(status_code=400, detail=f'Error: {str(e)}') + + +@router.get('/nodes', response_model=NodeListResponse, summary='Get All External Nodes') +async def get_all_nodes(): + """ + Retrieves the list of all configured external nodes. + + Returns: + A list of node objects, each containing a name and an IP. + """ + if not os.path.exists(cli_api.NODES_JSON_PATH): + return [] + try: + with open(cli_api.NODES_JSON_PATH, 'r') as f: + content = f.read() + if not content: + return [] + return json.loads(content) + except (json.JSONDecodeError, IOError) as e: + raise HTTPException(status_code=500, detail=f"Failed to read or parse nodes file: {e}") + + +@router.post('/nodes/add', response_model=DetailResponse, summary='Add External Node') +async def add_node(body: AddNodeBody): + """ + Adds a new external node to the configuration. + + Args: + body: Request body containing the name and IP of the node. + """ + try: + cli_api.add_node(body.name, body.ip) + return DetailResponse(detail=f"Node '{body.name}' added successfully.") + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post('/nodes/delete', response_model=DetailResponse, summary='Delete External Node') +async def delete_node(body: DeleteNodeBody): + """ + + Deletes an external node from the configuration by its name. + + Args: + body: Request body containing the name of the node to delete. + """ + try: + cli_api.delete_node(body.name) + return DetailResponse(detail=f"Node '{body.name}' deleted successfully.") + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/ip.py b/core/scripts/webpanel/routers/api/v1/schema/config/ip.py index adfa1ad..31589f9 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/config/ip.py +++ b/core/scripts/webpanel/routers/api/v1/schema/config/ip.py @@ -21,4 +21,26 @@ class StatusResponse(BaseModel): raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address or domain name") class EditInputBody(StatusResponse): - pass \ No newline at end of file + pass + +class Node(BaseModel): + name: str + ip: str + + @field_validator('ip', mode='before') + def check_ip(cls, v: str, info: ValidationInfo): + if v is None: + raise ValueError("IP cannot be None") + try: + ip_address(v) + return v + except ValueError: + raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address") + +class AddNodeBody(Node): + pass + +class DeleteNodeBody(BaseModel): + name: str + +NodeListResponse = list[Node] \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/schema/user.py b/core/scripts/webpanel/routers/api/v1/schema/user.py index 7cc2629..25c9f50 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/user.py +++ b/core/scripts/webpanel/routers/api/v1/schema/user.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List from pydantic import BaseModel, RootModel @@ -38,8 +38,14 @@ class EditUserInputBody(BaseModel): renew_creation_date: bool = False blocked: bool = False +class NodeUri(BaseModel): + name: str + uri: str + class UserUriResponse(BaseModel): username: str - ipv4: str | None = None - ipv6: str | None = None - normal_sub: str | None = None \ No newline at end of file + ipv4: Optional[str] = None + ipv6: Optional[str] = None + nodes: Optional[List[NodeUri]] = [] + normal_sub: Optional[str] = None + error: Optional[str] = None \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/user.py b/core/scripts/webpanel/routers/api/v1/user.py index 550ed42..e55e595 100644 --- a/core/scripts/webpanel/routers/api/v1/user.py +++ b/core/scripts/webpanel/routers/api/v1/user.py @@ -178,10 +178,12 @@ async def show_user_uri_api(username: str): uri_data_list = cli_api.show_user_uri_json([username]) if not uri_data_list: raise HTTPException(status_code=404, detail=f'URI for user {username} not found.') + uri_data = uri_data_list[0] if uri_data.get('error'): raise HTTPException(status_code=404, detail=f"{uri_data['error']}") - return uri_data + + return UserUriResponse(**uri_data) except cli_api.ScriptNotFoundError as e: raise HTTPException(status_code=500, detail=f'Server script error: {str(e)}') except cli_api.CommandExecutionError as e: