diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..4365b923 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,112 @@ +# Git files +.git +.gitignore + +# Environment and configuration files +.env +.env.* +*.env +apimanager/apimanager/local_settings.py + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store +API-Manager.iml + +# Python cache and build artifacts +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ + +# Testing and coverage +.coverage +.pytest_cache/ +htmlcov/ +.tox/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Logs +*.log +logs/ +*.log.* + +# Database files +*.db +*.sqlite3 +db/ + +# Temporary files +*.tmp +*.temp +tmp/ +temp/ + +# OS files +Thumbs.db +.DS_Store + +# Documentation build +docs/_build/ + +# Jupyter Notebook +.ipynb_checkpoints + +# Node modules (if any) +node_modules/ + +# Rope project settings +.ropeproject/ + +# Development and deployment files +docker-compose*.yml +Dockerfile* +.dockerignore +nginx*.conf +supervisor*.conf +*.service + +# Backup files +*.bak +*.backup + +# Security and certificate files +*.pem +*.key +*.crt +*.cert +*.p12 +*.pfx + +# Local development files +cookies.txt \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..83f9e4af --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,5 @@ +{ + "format_on_save": "off", + "remove_trailing_whitespace_on_save": false, + "ensure_final_newline_on_save": false +} diff --git a/Dockerfile b/Dockerfile index dcc2d0f9..7559b9b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,22 @@ FROM python:3.10 -COPY . /app + +# Create non-root user +RUN groupadd --gid 1000 appuser \ + && useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser + +COPY requirements.txt /app/ +COPY apimanager/ /app/apimanager/ +COPY static/ /app/static/ +COPY gunicorn.conf.py /app/gunicorn.conf.py COPY .github/local_settings_container.py /app/apimanager/apimanager/local_settings.py -COPY .github/gunicorn.conf.py /app/gunicorn.conf.py RUN pip install -r /app/requirements.txt WORKDIR /app RUN ./apimanager/manage.py migrate + +# Set proper ownership and switch to non-root user +RUN chown -R appuser:appuser /app +USER appuser + WORKDIR /app/apimanager EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--config", "../gunicorn.conf.py", "apimanager.wsgi"] \ No newline at end of file diff --git a/apimanager/apimanager/settings.py b/apimanager/apimanager/settings.py index b5b7f077..724639ee 100644 --- a/apimanager/apimanager/settings.py +++ b/apimanager/apimanager/settings.py @@ -332,11 +332,13 @@ # DO NOT TRY TO DO SO YOU WILL BE IGNORED! OBPv500 = API_HOST + '/obp/v5.0.0' OBPv510 = API_HOST + '/obp/v5.1.0' +OBPv600 = API_HOST + '/obp/v6.0.0' # API Versions API_VERSION = { "v500": OBPv500, - "v510": OBPv510 + "v510": OBPv510, + "v600": OBPv600 } # For some reason, swagger is not available at the latest API version #API_URL_SWAGGER = API_HOST + '/obp/v1.4.0/resource-docs/v' + 5.1.0 + '/swagger' # noqa diff --git a/apimanager/base/context_processors.py b/apimanager/base/context_processors.py index d3dff76d..7325b6ff 100644 --- a/apimanager/base/context_processors.py +++ b/apimanager/base/context_processors.py @@ -13,7 +13,7 @@ def api_version_processor(request): """Returns the configured API_VERSION""" - return {'API_VERSION': settings.API_VERSION['v500']} + return {'API_VERSION': settings.API_VERSION['v510']} def portal_page(request): @@ -82,7 +82,7 @@ def api_user_id(request): """Returns the API user id of the logged-in user""" user_id = 'not authenticated' get_current_user_api_url = USER_CURRENT - #Here we can not get the user from obp-api side, so we use the django auth user id here. + #Here we can not get the user from obp-api side, so we use the django auth user id here. cache_key_django_user_id = request.session._session.get('_auth_user_id') cache_key = '{},{},{}'.format('api_user_id',get_current_user_api_url, cache_key_django_user_id) apicaches=None @@ -112,4 +112,3 @@ def api_tester_url(request): """Returns the URL to the API Tester for the API instance""" url = getattr(settings, 'API_TESTER_URL', None) return {'API_TESTER_URL': url} - diff --git a/apimanager/base/templates/home.html b/apimanager/base/templates/home.html index a9351eb5..4d2d54b9 100644 --- a/apimanager/base/templates/home.html +++ b/apimanager/base/templates/home.html @@ -1,73 +1,95 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block content %} +{% extends 'base.html' %} {% load i18n %} {% block content %}
-

{% trans "Welcome to API Manager" %}

-
- {% if not user.is_authenticated %} -

- {% trans "API Manager allows you to manage some aspects of the OBP instance at " %} {{ API_HOST }}. {% trans "You have to " %} {% trans "login" %} {% trans "or" %} {% trans "register" %} {% trans "an account before being able to proceed" %}.{% trans "Your access is limited by the Entitlements you have." %} -

- {% else %} -

- {% trans "API Manager allows you to manage some aspects of the OBP instance at " %} {{ API_HOST }}. -

- {% endif %} -
- {% if not user.is_authenticated %} -
- -
-
- -
- -
- -
-
- {% csrf_token %} -
- - {{ directlogin_form.username }} -
-
- - {{ directlogin_form.password }} -
- -
-
-
-
- {% csrf_token %} -
- - {{ gatewaylogin_form.username }} -
-
- - {{ gatewaylogin_form.secret }} -
- -
- -
-
-
-
- {% endif %} +

{% trans "Welcome to API Manager" %}

+
+ {% if not user.is_authenticated %} +

+ {% trans "API Manager allows you to manage some aspects of the OBP + instance at " %} {{ API_HOST }}. {% + trans "You have to " %} + + {% trans "login" %} + + {% trans "or" %} + + {% trans "register" %} + + {% trans "an account before being able to proceed" %}.{% trans "Your + access is limited by the Entitlements you have." %} +

+ {% else %} +

+ {% trans "API Manager allows you to manage some aspects of the OBP + instance at " %} {{ API_HOST }}. +

+ {% endif %} +
+ {% if not user.is_authenticated %} +
+ +
+
+ +
+
+ +
+
+ {% csrf_token %} +
+ + {{ directlogin_form.username }} +
+
+ + {{ directlogin_form.password }} +
+ +
+
+
+
+ {% csrf_token %} +
+ + {{ gatewaylogin_form.username }} +
+
+ + {{ gatewaylogin_form.secret }} +
+ +
+
+
+
+
+ {% endif %}
{% endblock %} diff --git a/apimanager/consumers/forms.py b/apimanager/consumers/forms.py index 79f673b8..0d5832cb 100644 --- a/apimanager/consumers/forms.py +++ b/apimanager/consumers/forms.py @@ -12,8 +12,45 @@ class ApiConsumersForm(forms.Form): required=True, ) + from_date = forms.DateTimeField( + label='From Date', + widget=forms.DateTimeInput( + attrs={ + 'class': 'form-control', + 'type': 'datetime-local', + 'value': '2024-01-01T00:00', + } + ), + required=False, + initial='2024-01-01T00:00:00', + ) + + to_date = forms.DateTimeField( + label='To Date', + widget=forms.DateTimeInput( + attrs={ + 'class': 'form-control', + 'type': 'datetime-local', + 'value': '2026-01-01T00:00', + } + ), + required=False, + initial='2026-01-01T00:00:00', + ) + + per_second_call_limit = forms.IntegerField( + label='Per Second Call Limit', + widget=forms.NumberInput( + attrs={ + 'class': 'form-control', + } + ), + initial=-1, + required=False, + ) + per_minute_call_limit = forms.IntegerField( - label='per_minute_call_limit', + label='Per Minute Call Limit', widget=forms.NumberInput( attrs={ 'class': 'form-control', @@ -24,7 +61,7 @@ class ApiConsumersForm(forms.Form): ) per_hour_call_limit = forms.IntegerField( - label='per_hour_call_limit', + label='Per Hour Call Limit', widget=forms.NumberInput( attrs={ 'class': 'form-control', @@ -33,8 +70,9 @@ class ApiConsumersForm(forms.Form): initial=-1, required=False, ) + per_day_call_limit = forms.IntegerField( - label='per_day_call_limit', + label='Per Day Call Limit', widget=forms.NumberInput( attrs={ 'class': 'form-control', @@ -43,8 +81,9 @@ class ApiConsumersForm(forms.Form): initial=-1, required=False, ) + per_week_call_limit = forms.IntegerField( - label='per_week_call_limit', + label='Per Week Call Limit', widget=forms.NumberInput( attrs={ 'class': 'form-control', @@ -55,7 +94,7 @@ class ApiConsumersForm(forms.Form): ) per_month_call_limit = forms.IntegerField( - label='per_month_call_limit', + label='Per Month Call Limit', widget=forms.NumberInput( attrs={ 'class': 'form-control', diff --git a/apimanager/consumers/static/consumers/css/consumers.css b/apimanager/consumers/static/consumers/css/consumers.css index 7c502fcc..4c21ca3c 100644 --- a/apimanager/consumers/static/consumers/css/consumers.css +++ b/apimanager/consumers/static/consumers/css/consumers.css @@ -1,20 +1,350 @@ -.consumers #consumer-list { - margin-top: 20px; +.consumers #consumer-list { + margin-top: 20px; } #consumers .btn-group-vertical.filter-enabled, #consumers .btn-group-vertical.filter-apptype { - margin-top: 10px; + margin-top: 10px; } #consumers-detail div { - margin: 5px 0; + margin: 5px 0; } #consumers .filter a { - font-size: 12px; + font-size: 12px; } #consumers .actions .btn { - margin-bottom: 2px; + margin-bottom: 2px; +} + +/* Rate Limiting Styles */ +#consumers-detail h2 { + color: #333; + border-bottom: 2px solid #007bff; + padding-bottom: 10px; + margin-bottom: 20px; +} + +#consumers-detail .panel-info { + border-color: #bee5eb; + background-color: #d1ecf1; +} + +#consumers-detail .panel-info .panel-body { + background-color: #f8f9fa; + border-radius: 5px; + padding: 15px; +} + +#consumers-detail .text-info { + color: #0c5460 !important; + font-size: 16px; + font-weight: bold; +} + +#consumers-detail .text-muted { + color: #6c757d !important; + font-size: 12px; +} + +#consumers-detail .form-group label { + font-weight: bold; + color: #495057; +} + +#consumers-detail .btn-primary { + background-color: #007bff; + border-color: #007bff; + padding: 10px 20px; + font-weight: bold; +} + +#consumers-detail .btn-primary:hover { + background-color: #0056b3; + border-color: #0056b3; +} + +/* Usage statistics 6-column layout */ +#consumers-detail .panel-info .col-sm-2 { + min-height: 80px; + padding: 10px 5px; + transition: all 0.3s ease; +} + +/* Readonly fields styling */ +#consumers-detail input[readonly] { + background-color: #f8f9fa; + color: #6c757d; + cursor: not-allowed; + border: 1px solid #dee2e6; +} + +/* Refresh button styling */ +#refreshUsageBtn { + transition: all 0.3s ease; + margin-left: 10px; +} + +#refreshUsageBtn:hover { + background-color: #138496; + border-color: #117a8b; +} + +#refreshUsageBtn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Progress bar styling */ +#refreshProgress { + height: 10px; + background-color: #f5f5f5; + border-radius: 5px; + overflow: hidden; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +#refreshProgress .progress-bar { + transition: width 0.3s ease; + background-color: #17a2b8; +} + +/* Usage update animation */ +.usage-calls { + transition: background-color 0.5s ease; +} + +.usage-calls.updating { + background-color: #d4edda !important; + padding: 2px 6px; + border-radius: 3px; +} + +/* Spinning animation for refresh icon */ +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.glyphicon-spin { + animation: spin 1s infinite linear; +} + +/* Panel pulse effect during refresh */ +.panel-refreshing { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(23, 162, 184, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(23, 162, 184, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(23, 162, 184, 0); + } +} + +/* Updated data highlight */ +.data-updated { + background-color: #d4edda; + border-left: 3px solid #28a745; + padding-left: 10px; + transition: all 0.5s ease; +} + +/* Responsive adjustments for usage stats */ +@media (max-width: 768px) { + #consumers-detail .panel-info .col-xs-6 { + margin-bottom: 15px; + } + + #consumers-detail .panel-info .col-sm-2 { + min-height: auto; + } + + #refreshUsageBtn { + font-size: 12px; + padding: 4px 8px; + } +} + +/* Timestamp fields in configuration section */ +#consumers-detail .form-group input[readonly] { + font-size: 12px; + padding: 6px 12px; +} + +/* Rate Limiting CRUD Interface Styles */ +#rateLimitForm { + margin-bottom: 30px; +} + +#rateLimitForm .panel-heading { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +#rateLimitForm .panel-title { + color: #495057; + font-weight: bold; + font-size: 16px; +} + +#addRateLimitBtn { + background-color: #28a745; + border-color: #28a745; + font-weight: bold; +} + +#addRateLimitBtn:hover { + background-color: #218838; + border-color: #1e7e34; +} + +/* Rate limits table styling */ +#rateLimitsList .table { + margin-bottom: 0; +} + +#rateLimitsList .table th { + background-color: #f8f9fa; + color: #495057; + font-weight: bold; + font-size: 12px; + text-transform: none; + border-top: none; +} + +#rateLimitsList .table td { + font-size: 13px; + vertical-align: middle; +} + +/* Action buttons styling */ +#rateLimitsList .btn-sm { + font-size: 11px; + padding: 4px 8px; + margin-right: 5px; +} + +#rateLimitsList .btn-primary { + background-color: #007bff; + border-color: #007bff; +} + +#rateLimitsList .btn-primary:hover { + background-color: #0056b3; + border-color: #0056b3; +} + +#rateLimitsList .btn-danger { + background-color: #dc3545; + border-color: #dc3545; +} + +#rateLimitsList .btn-danger:hover { + background-color: #c82333; + border-color: #bd2130; +} + +/* Form styling */ +#rateLimitFormElement .form-group label { + font-weight: bold; + color: #495057; + font-size: 13px; +} + +#rateLimitFormElement .form-control { + font-size: 13px; + border-radius: 3px; +} + +#rateLimitFormElement .btn { + margin-right: 10px; +} + +/* Empty state styling */ +.alert-info { + background-color: #d1ecf1; + border-color: #bee5eb; + color: #0c5460; +} + +.alert-info .glyphicon { + margin-right: 8px; +} + +/* Responsive table */ +@media (max-width: 1200px) { + #rateLimitsList .table-responsive { + font-size: 12px; + } + + #rateLimitsList .table th, + #rateLimitsList .table td { + padding: 8px 4px; + } + + #rateLimitsList .btn-sm { + font-size: 10px; + padding: 3px 6px; + margin: 1px; + display: block; + width: 100%; + margin-bottom: 2px; + } +} + +@media (max-width: 768px) { + #rateLimitForm .col-xs-6 { + margin-bottom: 10px; + } + + #rateLimitsList .table { + font-size: 11px; + } + + #addRateLimitBtn { + width: 100%; + margin-bottom: 15px; + } +} + +/* Animation for form show/hide */ +#rateLimitForm { + transition: all 0.3s ease-in-out; +} + +/* Highlight new/updated rows */ +.table tr.highlight { + background-color: #d4edda; + transition: background-color 2s ease-out; +} + +/* Loading states */ +.btn[disabled] { + opacity: 0.6; + cursor: not-allowed; +} + +/* Panel improvements */ +.panel-default > .panel-heading { + background-image: none; + background-color: #f8f9fa; +} + +.panel-default { + border-color: #dee2e6; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); } diff --git a/apimanager/consumers/static/consumers/js/consumers.js b/apimanager/consumers/static/consumers/js/consumers.js index 8da04639..17e09658 100644 --- a/apimanager/consumers/static/consumers/js/consumers.js +++ b/apimanager/consumers/static/consumers/js/consumers.js @@ -1,2 +1,156 @@ -$(document).ready(function($) { +$(document).ready(function ($) { + // Handle datetime-local inputs for rate limiting + function initializeDateTimeFields() { + // Set default values for datetime fields if they're empty + var fromDateField = $("#from_date"); + var toDateField = $("#to_date"); + + // If fields are empty, set default values + if (!fromDateField.val()) { + fromDateField.val("2024-01-01T00:00"); + } + if (!toDateField.val()) { + toDateField.val("2100-01-01T00:00"); + } + } + + // Convert ISO datetime strings to datetime-local format for form inputs + function convertISOToLocalDateTime(isoString) { + if (!isoString) return ""; + // Remove the 'Z' and convert to local datetime format + return isoString.replace("Z", "").substring(0, 16); + } + + // Initialize datetime fields with existing values if they exist + function setExistingDateTimeValues() { + var fromDate = $("[data-from-date]").data("from-date"); + var toDate = $("[data-to-date]").data("to-date"); + + if (fromDate && fromDate !== "1099-12-31T23:00:00Z") { + $("#from_date").val(convertISOToLocalDateTime(fromDate)); + } + if (toDate && toDate !== "1099-12-31T23:00:00Z") { + $("#to_date").val(convertISOToLocalDateTime(toDate)); + } + } + + // Form validation + function validateRateLimitingForm() { + $("#rateLimitFormElement").on("submit", function (e) { + var hasError = false; + var errorMessage = ""; + + // Check if any limit values are negative (except -1 which means unlimited) + $(this) + .find('input[type="number"]') + .each(function () { + var value = parseInt($(this).val()); + if (isNaN(value) || value < -1) { + hasError = true; + errorMessage += + "Rate limit values must be -1 (unlimited) or positive numbers.\n"; + return false; + } + }); + + // Check date range + var fromDate = new Date($("#from_date").val()); + var toDate = new Date($("#to_date").val()); + + if (fromDate && toDate && fromDate > toDate) { + hasError = true; + errorMessage += "From Date must be before To Date.\n"; + } + + if (hasError) { + alert(errorMessage); + e.preventDefault(); + return false; + } + + // Handle form submission via AJAX + e.preventDefault(); + submitRateLimitForm(); + }); + } + + // Submit rate limit form via AJAX + function submitRateLimitForm() { + var form = $("#rateLimitFormElement"); + var formData = new FormData(form[0]); + var submitBtn = $("#submitBtn"); + var originalText = submitBtn.text(); + + // Disable submit button and show loading + submitBtn.prop("disabled", true).text("Saving..."); + + $.ajax({ + url: window.location.pathname, + type: "POST", + data: formData, + processData: false, + contentType: false, + headers: { + "X-CSRFToken": $("[name=csrfmiddlewaretoken]").val(), + }, + success: function (response) { + if (response.success) { + // Hide form and reload page to show updated data + hideRateLimitForm(); + window.location.reload(); + } else { + alert("Error: " + (response.error || "Unknown error occurred")); + } + }, + error: function (xhr, status, error) { + var errorMessage = "Error saving rate limit"; + if (xhr.responseJSON && xhr.responseJSON.error) { + errorMessage = xhr.responseJSON.error; + } + alert(errorMessage); + }, + complete: function () { + // Re-enable submit button + submitBtn.prop("disabled", false).text(originalText); + }, + }); + } + + // Add visual feedback for current usage status + function enhanceUsageDisplay() { + $(".text-info").each(function () { + var callsMade = parseInt($(this).text().match(/\d+/)); + var parentDiv = $(this).closest(".col-xs-6, .col-sm-3"); + var limitText = parentDiv.find("strong").text().toLowerCase(); + + // You could add logic here to highlight usage that's approaching limits + // For now, we'll just ensure consistent styling + $(this).addClass("usage-indicator"); + }); + } + + // Initialize all functionality + initializeDateTimeFields(); + setExistingDateTimeValues(); + validateRateLimitingForm(); + enhanceUsageDisplay(); + + // Add tooltips for better UX + $('[data-toggle="tooltip"]').tooltip(); + + // Add help text for rate limiting fields + $('input[name*="call_limit"]').each(function () { + $(this).attr( + "title", + "Use -1 for unlimited, or enter a positive number for the limit", + ); + }); }); + +// Global functions are now defined inline in the template +// This file now only contains form validation and initialization + +// Refresh rate limits list +function refreshRateLimits() { + window.location.reload(); +} diff --git a/apimanager/consumers/templates/consumers/detail.html b/apimanager/consumers/templates/consumers/detail.html index 0569ed9d..d4859b72 100644 --- a/apimanager/consumers/templates/consumers/detail.html +++ b/apimanager/consumers/templates/consumers/detail.html @@ -1,6 +1,7 @@ {% extends 'base.html' %} {% load humanize static %} {% load i18n %} +{% load consumer_extras %} {% block page_title %}{{ block.super }} / Consumer {{ consumer.app_name }}{% endblock page_title %} @@ -12,57 +13,222 @@

{% trans "Consumer" %} {{ consumer.app_name }}

-

{% trans "Params" %}

-
- {% csrf_token %} - {{ form.consumer_id }} - {% if form.non_field_errors %} -
- {{ form.non_field_errors }} +

{% trans "Rate Limiting Configuration" %}

+ + +
+
+ +
+
+ + +