From f2dc9942d91d87e59e7a864adc1444fc283654a8 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 11 Feb 2026 16:49:32 -0300 Subject: [PATCH] Add API endpoints for listing peers and retrieving peer details --- api_v2/urls_manage.py | 3 +- api_v2/views.py | 29 ++++++++ api_v2/views_api.py | 6 +- templates/api_v2/api_documentation.html | 89 +++++++++++++++++++++++++ templates/api_v2/list.html | 3 + 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 templates/api_v2/api_documentation.html diff --git a/api_v2/urls_manage.py b/api_v2/urls_manage.py index 31f6a80..59fd55d 100644 --- a/api_v2/urls_manage.py +++ b/api_v2/urls_manage.py @@ -1,9 +1,10 @@ from django.urls import path -from api_v2.views import view_api_key_list, view_manage_api_key, view_delete_api_key +from api_v2.views import view_api_key_list, view_manage_api_key, view_delete_api_key, view_api_docs urlpatterns = [ path('list/', view_api_key_list, name='api_v2_list'), path('manage/', view_manage_api_key, name='api_v2_manage'), path('delete//', view_delete_api_key, name='api_v2_delete'), + path('docs/', view_api_docs, name='api_v2_docs'), ] \ No newline at end of file diff --git a/api_v2/views.py b/api_v2/views.py index e5ee093..e9234b5 100644 --- a/api_v2/views.py +++ b/api_v2/views.py @@ -94,3 +94,32 @@ def view_delete_api_key(request, uuid): 'text': _('Are you sure you want to delete the API Key "%(name)s"?') % {'name': api_key.name} } return render(request, 'generic_delete_confirmation.html', context) + + +@login_required +def view_api_docs(request): + from django.urls.resolvers import URLPattern + from api_v2 import urls_api + + if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=50).exists(): + return render(request, 'access_denied.html', {'page_title': _('Access Denied')}) + + docs = [] + # We iterate over the urlpatterns from the api_v2.urls_api module + for pattern in urls_api.urlpatterns: + if isinstance(pattern, URLPattern): + view_func = pattern.callback + # The view might be wrapped (e.g. by @csrf_exempt), but @api_doc metadata + # should have been preserved on the wrapper or be available on the original function. + # verify_api_doc checks handled this. + if hasattr(view_func, 'api_doc'): + doc_data = view_func.api_doc.copy() + doc_data['url_pattern'] = str(pattern.pattern) + doc_data['name'] = pattern.name + docs.append(doc_data) + + context = { + 'page_title': _('API Documentation'), + 'docs': docs + } + return render(request, 'api_v2/api_documentation.html', context) diff --git a/api_v2/views_api.py b/api_v2/views_api.py index dc878f9..d09eeef 100644 --- a/api_v2/views_api.py +++ b/api_v2/views_api.py @@ -15,13 +15,14 @@ from wireguard_tools.views import export_wireguard_configuration from .models import ApiKey -def api_doc(*, summary: str, auth: str, params: list, returns: list, examples: Optional[dict] = None): +def api_doc(*, summary: str, auth: str, params: list, returns: list, methods: Optional[List[str]] = None, examples: Optional[dict] = None): def decorator(view_func): view_func.api_doc = { "summary": summary, "auth": auth, "params": params, "returns": returns, + "methods": methods or ["POST"], "examples": examples or {}, } @@ -166,6 +167,7 @@ def _get_wireguard_instance(instance_name: str) -> Optional[WireGuardInstance]: @api_doc( summary="Create / Update / Delete a WireGuard peer (and optionally reload the interface)", auth="Header token: ", + methods=["POST", "PUT", "DELETE"], params=[ {"name": "instance", "in": "json", "type": "string", "required": True, "example": "wg0", "description": "Target instance name in the format wg{instance_id} (e.g. wg0, wg1)."}, @@ -450,6 +452,7 @@ def api_v2_manage_peer(request): @api_doc( summary="List peers for a specific instance (required)", auth="Header token: ", + methods=["POST", "GET"], params=[ {"name": "instance", "in": "json", "type": "string", "required": True, "example": "wg2", "description": "Required. Target instance name in the format wg{instance_id} (e.g. wg0, wg1)."}, @@ -523,6 +526,7 @@ def api_v2_peer_list(request): @api_doc( summary="Peer details for a specific instance (required) by peer_uuid or peer_public_key", auth="Header token: ", + methods=["POST", "GET"], params=[ {"name": "instance", "in": "json", "type": "string", "required": True, "example": "wg2", "description": "Required. Target instance name in the format wg{instance_id} (e.g. wg0, wg1)."}, diff --git a/templates/api_v2/api_documentation.html b/templates/api_v2/api_documentation.html new file mode 100644 index 0000000..103f137 --- /dev/null +++ b/templates/api_v2/api_documentation.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{% trans 'API Documentation' %}

+ + {% for doc in docs %} +
+
+
+ {% for method in doc.methods %} + {{ method }} + {% endfor %} + /api/v2/{{ doc.url_pattern }} +
+
+
+

{{ doc.summary }}

+ +

{% trans 'Authentication' %}: {{ doc.auth }}

+ + {% if doc.params %} +
{% trans 'Parameters' %}
+ + + + + + + + + + + + {% for param in doc.params %} + + + + + + + + {% endfor %} + +
{% trans 'Name' %}{% trans 'In' %}{% trans 'Type' %}{% trans 'Required' %}{% trans 'Description' %}
{{ param.name }}{{ param.in }}{{ param.type }} + {% if param.required %} + {% trans 'Yes' %} + {% else %} + {% trans 'No' %} + {% endif %} + + {{ param.description }} + {% if param.example %} +
{% trans 'Example' %}: {{ param.example }} + {% endif %} +
+ {% endif %} + + {% if doc.returns %} +
{% trans 'Returns' %}
+
    + {% for ret in doc.returns %} +
  • + {{ ret.status }} +
    {{ ret.body|pprint }}
    +
  • + {% endfor %} +
+ {% endif %} + + {% if doc.examples %} +
{% trans 'Examples' %}
+ {% for key, example in doc.examples.items %} +
+
+ {{ key }} +
{{ example|pprint }}
+
+
+ {% endfor %} + {% endif %} +
+
+ {% endfor %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/api_v2/list.html b/templates/api_v2/list.html index 0543369..b7d9421 100644 --- a/templates/api_v2/list.html +++ b/templates/api_v2/list.html @@ -79,6 +79,9 @@ {% trans 'Add API Key' %} + + {% trans 'API Documentation' %} +