This commit is contained in:
Marko Milić 2025-11-14 12:14:09 +00:00 committed by GitHub
commit 4756a61b99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2627 additions and 266 deletions

112
.dockerignore Normal file
View File

@ -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

5
.zed/settings.json Normal file
View File

@ -0,0 +1,5 @@
{
"format_on_save": "off",
"remove_trailing_whitespace_on_save": false,
"ensure_final_newline_on_save": false
}

View File

@ -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"]

View File

@ -332,11 +332,13 @@ except ImportError:
# 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

View File

@ -13,7 +13,7 @@ USER_CURRENT = "/users/current"
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}

View File

@ -1,73 +1,95 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
{% extends 'base.html' %} {% load i18n %} {% block content %}
<div class="home">
<h1>{% trans "Welcome to API Manager" %}</h1>
<div class="well" id="intro">
{% if not user.is_authenticated %}
<p>
{% trans "API Manager allows you to manage some aspects of the OBP instance at " %} <a href="{{ API_HOST }}">{{ API_HOST }}</a>. {% trans "You have to " %} <a href="{{ API_HOST }}" title="Login at {{ API_HOST }}"> {% trans "login" %} </a> {% trans "or" %} <a href="{{ API_HOST }}/user_mgt/sign_up" title="Register at {{ API_HOST }}"> {% trans "register" %} </a> {% trans "an account before being able to proceed" %}.{% trans "Your access is limited by the Entitlements you have." %}
</p>
{% else %}
<p>
{% trans "API Manager allows you to manage some aspects of the OBP instance at " %} <a href="{{ API_HOST }}">{{ API_HOST }}</a>.
</p>
{% endif %}
</div>
{% if not user.is_authenticated %}
<div id="login">
<label for="authentication-select"><h2>{% trans "Authenticate" %}</h2></label>
<div class="row">
<div class="col-xs-12 col-sm-3">
<select class="form-control" id="authentication-select">
<option value="">{% trans "Choose ..." %}</option>
<option value="oauth">OAuth 1/OpenID Connect</option>
{% if ALLOW_DIRECT_LOGIN %}
<option value="directlogin" >DirectLogin</option>
{% endif %}
{% if ALLOW_GATEWAY_LOGIN %}
<option value="gatewaylogin" >GatewayLogin</option>
{% endif %}
</select>
</div>
<div class="col-xs-12 col-sm-9">
<div class="authentication-method" id="authenticate-oauth">
<a class="btn btn-primary" href="{% url 'oauth-initiate' %}{% if request.GET.next %}?next={{ request.GET.next }}{% endif %}">{% trans "Proceed to authentication server" %}</a>
</div>
<div class="authentication-method" id="authenticate-directlogin">
<form action="{% url 'directlogin' %}" method="post">
{% csrf_token %}
<div class="form-group">
<label for="username">Username:</label>
{{ directlogin_form.username }}
</div>
<div class="form-group">
<label for="password">Password:</label>
{{ directlogin_form.password }}
</div>
<button class="btn btn-primary">Login</button>
</form>
</div>
<div class="authentication-method" id="authenticate-gatewaylogin">
<form action="{% url 'gatewaylogin' %}" method="post">
{% csrf_token %}
<div class="form-group">
<label for="username">Username:</label>
{{ gatewaylogin_form.username }}
</div>
<div class="form-group">
<label for="secret">Secret:</label>
{{ gatewaylogin_form.secret }}
</div>
<button class="btn btn-primary">Login</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
<h1>{% trans "Welcome to API Manager" %}</h1>
<div class="well" id="intro">
{% if not user.is_authenticated %}
<p>
{% trans "API Manager allows you to manage some aspects of the OBP
instance at " %} <a href="{{ API_HOST }}">{{ API_HOST }}</a>. {%
trans "You have to " %}
<a href="{{ API_HOST }}" title="Login at {{ API_HOST }}">
{% trans "login" %}
</a>
{% trans "or" %}
<a
href="{{ API_HOST }}/user_mgt/sign_up"
title="Register at {{ API_HOST }}"
>
{% trans "register" %}
</a>
{% trans "an account before being able to proceed" %}.{% trans "Your
access is limited by the Entitlements you have." %}
</p>
{% else %}
<p>
{% trans "API Manager allows you to manage some aspects of the OBP
instance at " %} <a href="{{ API_HOST }}">{{ API_HOST }}</a>.
</p>
{% endif %}
</div>
{% if not user.is_authenticated %}
<div id="login">
<label for="authentication-select"
><h2>{% trans "Authenticate" %}</h2></label
>
<div class="row">
<div class="col-xs-12 col-sm-3">
<select class="form-control" id="authentication-select">
<option value="">{% trans "Choose ..." %}</option>
<option value="oauth">OAuth 1/OpenID Connect</option>
{% if ALLOW_DIRECT_LOGIN %}
<option value="directlogin">DirectLogin</option>
{% endif %} {% if ALLOW_GATEWAY_LOGIN %}
<option value="gatewaylogin">GatewayLogin</option>
{% endif %}
</select>
</div>
<div class="col-xs-12 col-sm-9">
<div class="authentication-method" id="authenticate-oauth">
<a
class="btn btn-primary"
href="{% url 'oauth-initiate' %}{% if request.GET.next %}?next={{ request.GET.next }}{% endif %}"
>{% trans "Proceed to authentication server" %}</a
>
</div>
<div
class="authentication-method"
id="authenticate-directlogin"
>
<form action="{% url 'directlogin' %}" method="post">
{% csrf_token %}
<div class="form-group">
<label for="username">Username:</label>
{{ directlogin_form.username }}
</div>
<div class="form-group">
<label for="password">Password:</label>
{{ directlogin_form.password }}
</div>
<button class="btn btn-primary">Login</button>
</form>
</div>
<div
class="authentication-method"
id="authenticate-gatewaylogin"
>
<form action="{% url 'gatewaylogin' %}" method="post">
{% csrf_token %}
<div class="form-group">
<label for="username">Username:</label>
{{ gatewaylogin_form.username }}
</div>
<div class="form-group">
<label for="secret">Secret:</label>
{{ gatewaylogin_form.secret }}
</div>
<button class="btn btn-primary">Login</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -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',

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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 @@
<div class="row">
<div class="col-xs-12">
<h2>{% trans "Params" %}</h2>
<form action="" method="post">
{% csrf_token %}
{{ form.consumer_id }}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<h2>{% trans "Rate Limiting Configuration" %}</h2>
<div class="row">
<div class="col-xs-2 col-sm-2">
{% if form.per_minute_call_limit.errors %}<div class="alert alert-danger">{{ form.per_minute_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_minute_call_limit.label_tag }}
{{ form.per_minute_call_limit }}
</div>
<!-- Add New Rate Limit Button -->
<div class="row" style="margin-bottom: 20px;">
<div class="col-xs-12">
<button type="button" class="btn btn-success" id="addRateLimitBtn" onclick="showAddRateLimitForm()">
<span class="glyphicon glyphicon-plus"></span> {% trans "Add New Rate Limit" %}
</button>
</div>
</div>
<!-- Add/Edit Rate Limit Form (Initially Hidden) -->
<div id="rateLimitForm" style="display: none; margin-bottom: 30px;">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title" id="formTitle">{% trans "Add New Rate Limit" %}</h4>
</div>
<div class="col-xs-2 col-sm-2">
{% if form.per_hour_call_limit.errors %}<div class="alert alert-danger">{{ form.per_hour_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_hour_call_limit.label_tag }}
{{ form.per_hour_call_limit }}
</div>
<div class="panel-body">
<form id="rateLimitFormElement" method="post">
{% csrf_token %}
<input type="hidden" id="rateLimitId" name="rate_limit_id" value="">
<input type="hidden" name="action" id="formAction" value="create">
<div class="row">
<div class="col-xs-12 col-sm-6">
<div class="form-group">
<label for="from_date">{% trans "From Date" %}</label>
<input type="datetime-local" class="form-control" id="from_date" name="from_date" required>
</div>
</div>
<div class="col-xs-12 col-sm-6">
<div class="form-group">
<label for="to_date">{% trans "To Date" %}</label>
<input type="datetime-local" class="form-control" id="to_date" name="to_date" required>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_second_call_limit">{% trans "Per Second" %}</label>
<input type="number" class="form-control" id="per_second_call_limit" name="per_second_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_minute_call_limit">{% trans "Per Minute" %}</label>
<input type="number" class="form-control" id="per_minute_call_limit" name="per_minute_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_hour_call_limit">{% trans "Per Hour" %}</label>
<input type="number" class="form-control" id="per_hour_call_limit" name="per_hour_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_day_call_limit">{% trans "Per Day" %}</label>
<input type="number" class="form-control" id="per_day_call_limit" name="per_day_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_week_call_limit">{% trans "Per Week" %}</label>
<input type="number" class="form-control" id="per_week_call_limit" name="per_week_call_limit" value="-1" min="-1">
</div>
</div>
<div class="col-xs-6 col-sm-4 col-md-2">
<div class="form-group">
<label for="per_month_call_limit">{% trans "Per Month" %}</label>
<input type="number" class="form-control" id="per_month_call_limit" name="per_month_call_limit" value="-1" min="-1">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button type="submit" class="btn btn-primary" id="submitBtn">{% trans "Save Rate Limit" %}</button>
<button type="button" class="btn btn-default" onclick="hideRateLimitForm()">{% trans "Cancel" %}</button>
</div>
</div>
</form>
</div>
<div class="col-xs-2 col-sm-2">
{% if form.per_day_call_limit.errors %}<div class="alert alert-danger">{{ form.per_day_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_day_call_limit.label_tag }}
{{ form.per_day_call_limit }}
</div>
</div>
<!-- Rate Limits List -->
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">{% trans "Existing Rate Limits" %}</h4>
</div>
</div>
<div class="col-xs-2 col-sm-2">
{% if form.per_week_call_limit.errors %}<div class="alert alert-danger">{{ form.per_week_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_week_call_limit.label_tag }}
{{ form.per_week_call_limit }}
</div>
</div>
<div class="col-xs-2 col-sm-2">
{% if form.per_month_call_limit.errors %}<div class="alert alert-danger">{{ form.per_month_call_limit.errors }}</div>{% endif %}
<div class="form-group">
{{ form.per_month_call_limit.label_tag }}
{{ form.per_month_call_limit }}
<div class="panel-body">
<div id="rateLimitsList">
{% if call_limits and call_limits.limits %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>{% trans "Rate Limiting ID" %}</th>
<th>{% trans "From Date" %}</th>
<th>{% trans "To Date" %}</th>
<th>{% trans "Per Second" %}</th>
<th>{% trans "Per Minute" %}</th>
<th>{% trans "Per Hour" %}</th>
<th>{% trans "Per Day" %}</th>
<th>{% trans "Per Week" %}</th>
<th>{% trans "Per Month" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Updated" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for limit in call_limits.limits %}
<tr>
<td><code>{{ limit.rate_limiting_id|default:"N/A" }}</code></td>
<td>{{ limit.from_date|parse_iso_date:"Y-m-d H:i" }}</td>
<td>{{ limit.to_date|parse_iso_date:"Y-m-d H:i" }}</td>
<td>{{ limit.per_second_call_limit|default:"-1" }}</td>
<td>{{ limit.per_minute_call_limit|default:"-1" }}</td>
<td>{{ limit.per_hour_call_limit|default:"-1" }}</td>
<td>{{ limit.per_day_call_limit|default:"-1" }}</td>
<td>{{ limit.per_week_call_limit|default:"-1" }}</td>
<td>{{ limit.per_month_call_limit|default:"-1" }}</td>
<td>{{ limit.created_at|parse_iso_date:"Y-m-d H:i" }}</td>
<td>{{ limit.updated_at|parse_iso_date:"Y-m-d H:i" }}</td>
<td>
{% if limit.rate_limiting_id %}
<button type="button" class="btn btn-sm btn-primary"
onclick="editRateLimit('{{ limit.rate_limiting_id }}', '{{ limit.from_date|escapejs }}', '{{ limit.to_date|escapejs }}', '{{ limit.per_second_call_limit|default:"-1" }}', '{{ limit.per_minute_call_limit|default:"-1" }}', '{{ limit.per_hour_call_limit|default:"-1" }}', '{{ limit.per_day_call_limit|default:"-1" }}', '{{ limit.per_week_call_limit|default:"-1" }}', '{{ limit.per_month_call_limit|default:"-1" }}')">
<span class="glyphicon glyphicon-edit"></span> {% trans "Edit" %}
</button>
<button type="button" class="btn btn-sm btn-danger"
onclick="deleteRateLimit('{{ limit.rate_limiting_id|escapejs }}')">
<span class="glyphicon glyphicon-trash"></span> {% trans "Delete" %}
</button>
{% else %}
<span class="text-muted">{% trans "No ID" %}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<span class="glyphicon glyphicon-info-sign"></span>
{% trans "No rate limits configured for this consumer. Click 'Add New Rate Limit' to create one." %}
<!-- Debug: Show raw API response for troubleshooting -->
{% if call_limits %}
<br><small><strong>Debug - Raw call_limits data:</strong><br>
<pre style="font-size: 10px; background: #f5f5f5; padding: 10px; margin-top: 10px;">{{ call_limits }}</pre>
</small>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<h2>{% trans "Current Usage" %}
<button type="button" id="refreshUsageBtn" class="btn btn-sm btn-info pull-right" onclick="refreshUsageStats()">
<span class="glyphicon glyphicon-refresh"></span> {% trans "Auto Refresh (5s)" %}
</button>
</h2>
<div class="panel panel-info" id="usageStatsPanel">
<div class="panel-body" id="usageStatsContent">
<div class="row" id="usageStatsRow">
<div class="col-xs-6 col-sm-2" data-period="per_second">
<strong>{% trans "Per Second" %}</strong><br>
<span class="text-info usage-calls">{% if not current_usage.per_second %}Unlimited{% elif current_usage.per_second.calls_made|add:"0" == -1 %}Not tracked{% else %}{{ current_usage.per_second.calls_made }} calls made{% endif %}</span><br>
{% if current_usage.per_second %}<small class="text-muted usage-reset">{% if current_usage.per_second.reset_in_seconds|add:"0" == -1 %}Not tracked{% else %}Resets in {{ current_usage.per_second.reset_in_seconds }} seconds{% endif %}</small>{% endif %}
</div>
<div class="col-xs-6 col-sm-2" data-period="per_minute">
<strong>{% trans "Per Minute" %}</strong><br>
<span class="text-info usage-calls">{% if not current_usage.per_minute %}Unlimited{% elif current_usage.per_minute.calls_made|add:"0" == -1 %}Not tracked{% else %}{{ current_usage.per_minute.calls_made }} calls made{% endif %}</span><br>
{% if current_usage.per_minute %}<small class="text-muted usage-reset">{% if current_usage.per_minute.reset_in_seconds|add:"0" == -1 %}Not tracked{% else %}Resets in {{ current_usage.per_minute.reset_in_seconds }} seconds{% endif %}</small>{% endif %}
</div>
<div class="col-xs-6 col-sm-2" data-period="per_hour">
<strong>{% trans "Per Hour" %}</strong><br>
<span class="text-info usage-calls">{% if not current_usage.per_hour %}Unlimited{% elif current_usage.per_hour.calls_made|add:"0" == -1 %}Not tracked{% else %}{{ current_usage.per_hour.calls_made }} calls made{% endif %}</span><br>
{% if current_usage.per_hour %}<small class="text-muted usage-reset">{% if current_usage.per_hour.reset_in_seconds|add:"0" == -1 %}Not tracked{% else %}Resets in {{ current_usage.per_hour.reset_in_seconds }} seconds{% endif %}</small>{% endif %}
</div>
<div class="col-xs-6 col-sm-2" data-period="per_day">
<strong>{% trans "Per Day" %}</strong><br>
<span class="text-info usage-calls">{% if not current_usage.per_day %}Unlimited{% elif current_usage.per_day.calls_made|add:"0" == -1 %}Not tracked{% else %}{{ current_usage.per_day.calls_made }} calls made{% endif %}</span><br>
{% if current_usage.per_day %}<small class="text-muted usage-reset">{% if current_usage.per_day.reset_in_seconds|add:"0" == -1 %}Not tracked{% else %}Resets in {{ current_usage.per_day.reset_in_seconds }} seconds{% endif %}</small>{% endif %}
</div>
<div class="col-xs-6 col-sm-2" data-period="per_week">
<strong>{% trans "Per Week" %}</strong><br>
<span class="text-info usage-calls">{% if not current_usage.per_week %}Unlimited{% elif current_usage.per_week.calls_made|add:"0" == -1 %}Not tracked{% else %}{{ current_usage.per_week.calls_made }} calls made{% endif %}</span><br>
{% if current_usage.per_week %}<small class="text-muted usage-reset">{% if current_usage.per_week.reset_in_seconds|add:"0" == -1 %}Not tracked{% else %}Resets in {{ current_usage.per_week.reset_in_seconds }} seconds{% endif %}</small>{% endif %}
</div>
<div class="col-xs-6 col-sm-2" data-period="per_month">
<strong>{% trans "Per Month" %}</strong><br>
<span class="text-info usage-calls">{% if not current_usage.per_month %}Unlimited{% elif current_usage.per_month.calls_made|add:"0" == -1 %}Not tracked{% else %}{{ current_usage.per_month.calls_made }} calls made{% endif %}</span><br>
{% if current_usage.per_month %}<small class="text-muted usage-reset">{% if current_usage.per_month.reset_in_seconds|add:"0" == -1 %}Not tracked{% else %}Resets in {{ current_usage.per_month.reset_in_seconds }} seconds{% endif %}</small>{% endif %}
</div>
</div>
<div id="refreshProgress" class="progress" style="display: none; margin-top: 15px;">
<div class="progress-bar progress-bar-info progress-bar-striped active" id="progressBar" style="width: 0%"></div>
</div>
<button type="submit" class="btn btn-primary">{% trans "Update Consumer" %}</button>
</form>
</div>
</div>
</div>
</div>
<div class="row">
@ -122,51 +288,16 @@
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">
<div id="consumers-detail-redirect_url">
<strong>{% trans "Redirect URL" %}</strong><br />
<span>{{ consumer.redirect_url }}</span>
</div>
</div>
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_minute_call_limit ">
<strong>{% trans "Per minute call limit" %}</strong><br />
<span>{{ consumer.per_minute_call_limit }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_hour_call_limit ">
<strong>{% trans "Per hour call limit" %} </strong><br />
<span>{{ consumer.per_hour_call_limit }}</span>
</div>
</div>
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_day_call_limit">
<strong>{% trans "Per day call limit" %}</strong><br />
<span>{{ consumer.per_day_call_limit }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div id="consumers-detail-redirect_url">
<strong>{% trans "Redirect URL" %}</strong><br />
<span>{{ consumer.redirect_url }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_week_call_limit">
<strong>{% trans "Per week call limit" %}</strong><br />
<span>{{ consumer.per_week_call_limit }}</span>
</div>
</div>
<div class="col-xs-6 col-sm-6">
<div id="consumers-per_month_call_limit">
<strong>{% trans "Per month call limit" %}</strong><br />
<span>{{ consumer.per_month_call_limit }}</span>
</div>
</div>
</div>
<div class="row">
@ -204,11 +335,409 @@
{% endblock %}
{% block extrajs %}
{% comment %}
<script type="text/javascript" src="{% static 'consumers/js/consumers.js' %}"></script>
<script type="text/javascript">
// Add data attributes for existing datetime values
$(document).ready(function() {
{% if consumer.from_date %}
$('body').attr('data-from-date', '{{ consumer.from_date }}');
{% endif %}
{% if consumer.to_date %}
$('body').attr('data-to-date', '{{ consumer.to_date }}');
{% endif %}
// Debug: Log basic debugging info
console.log('Current usage data available:', {% if current_usage %}true{% else %}false{% endif %});
// Load rate limits data into JavaScript
window.rateLimits = [
{% if call_limits and call_limits.limits %}
{% for limit in call_limits.limits %}
{
per_second_call_limit: {{ limit.per_second_call_limit|default:"-1" }},
per_minute_call_limit: {{ limit.per_minute_call_limit|default:"-1" }},
per_hour_call_limit: {{ limit.per_hour_call_limit|default:"-1" }},
per_day_call_limit: {{ limit.per_day_call_limit|default:"-1" }},
per_week_call_limit: {{ limit.per_week_call_limit|default:"-1" }},
per_month_call_limit: {{ limit.per_month_call_limit|default:"-1" }}
}{% if not forloop.last %},{% endif %}
{% endfor %}
{% endif %}
];
// Initial load and display of usage data
setTimeout(function() {
console.log('Rate limits loaded:', window.rateLimits);
updateUsageDisplayWithLimits();
fetchUsageData();
}, 100);
});
// Function to calculate effective limit for a period by summing all active limits
function getEffectiveLimit(period) {
if (!window.rateLimits || window.rateLimits.length === 0) {
return null;
}
let total = 0;
let hasLimits = false;
window.rateLimits.forEach(function(limit) {
let limitValue;
switch(period) {
case 'per_second': limitValue = limit.per_second_call_limit; break;
case 'per_minute': limitValue = limit.per_minute_call_limit; break;
case 'per_hour': limitValue = limit.per_hour_call_limit; break;
case 'per_day': limitValue = limit.per_day_call_limit; break;
case 'per_week': limitValue = limit.per_week_call_limit; break;
case 'per_month': limitValue = limit.per_month_call_limit; break;
default: limitValue = -1;
}
// Convert to integer and check if it's a valid positive limit
let parsedValue = parseInt(limitValue);
if (!isNaN(parsedValue) && parsedValue > 0) {
total += parsedValue;
hasLimits = true;
}
});
console.log('Effective limit for', period, ':', hasLimits ? total : 'unlimited');
return hasLimits ? total : null;
}
// Function to update usage display with rate limits
function updateUsageDisplayWithLimits() {
$('#usageStatsRow .usage-calls').each(function() {
let $this = $(this);
let period = $this.closest('[data-period]').data('period');
let text = $this.text().trim();
// Only update if it shows "X calls made" format (not "Unlimited" or "Not tracked")
let match = text.match(/^(\d+) calls made$/);
if (match) {
let calls = match[1];
let effectiveLimit = getEffectiveLimit(period);
if (effectiveLimit && effectiveLimit > 0) {
$this.text(calls + ' of ' + effectiveLimit + ' calls made');
}
}
});
}
// Global functions for CRUD operations - attached to window for global access
window.showAddRateLimitForm = function() {
// Reset form for new entry
$("#rateLimitForm").show();
$("#formTitle").text("Add New Rate Limit");
$("#formAction").val("create");
$("#rateLimitId").val("");
// Set default values
$("#from_date").val("2024-01-01T00:00");
$("#to_date").val("2026-01-01T00:00");
$("#per_second_call_limit").val("-1");
$("#per_minute_call_limit").val("-1");
$("#per_hour_call_limit").val("-1");
$("#per_day_call_limit").val("-1");
$("#per_week_call_limit").val("-1");
$("#per_month_call_limit").val("-1");
$("#submitBtn").text("Create Rate Limit");
// Scroll to form
$("html, body").animate({
scrollTop: $("#rateLimitForm").offset().top - 20
}, 500);
};
window.hideRateLimitForm = function() {
$("#rateLimitForm").hide();
};
window.editRateLimit = function(rateLimitingId, fromDate, toDate, perSecond, perMinute, perHour, perDay, perWeek, perMonth) {
// Show form for editing
$("#rateLimitForm").show();
$("#formTitle").text("Edit Rate Limit");
$("#formAction").val("update");
$("#rateLimitId").val(rateLimitingId);
// Convert ISO dates to datetime-local format
function convertISOToLocal(isoString) {
if (!isoString || isoString === "1099-12-31T23:00:00Z") return "2024-01-01T00:00";
try {
var date = new Date(isoString);
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, "0");
var day = String(date.getDate()).padStart(2, "0");
var hours = String(date.getHours()).padStart(2, "0");
var minutes = String(date.getMinutes()).padStart(2, "0");
return year + "-" + month + "-" + day + "T" + hours + ":" + minutes;
} catch (e) {
return "2024-01-01T00:00";
}
}
// Populate form fields
$("#from_date").val(convertISOToLocal(fromDate));
$("#to_date").val(convertISOToLocal(toDate));
$("#per_second_call_limit").val(perSecond);
$("#per_minute_call_limit").val(perMinute);
$("#per_hour_call_limit").val(perHour);
$("#per_day_call_limit").val(perDay);
$("#per_week_call_limit").val(perWeek);
$("#per_month_call_limit").val(perMonth);
$("#submitBtn").text("Update Rate Limit");
// Scroll to form
$("html, body").animate({
scrollTop: $("#rateLimitForm").offset().top - 20
}, 500);
};
window.deleteRateLimit = function(rateLimitingId) {
console.log("Deleting rate limit with ID:", rateLimitingId);
if (!confirm("Are you sure you want to delete this rate limit? This action cannot be undone.")) {
return;
}
// Create form data for delete request
var formData = new FormData();
formData.append("action", "delete");
formData.append("rate_limiting_id", rateLimitingId);
formData.append("csrfmiddlewaretoken", $("[name=csrfmiddlewaretoken]").val());
$.ajax({
url: window.location.pathname,
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
success: function(response) {
console.log("Delete response:", response);
if (response.success) {
// Reload page to show updated data
window.location.reload();
} else {
alert("Error: " + (response.error || "Unknown error occurred"));
}
},
error: function(xhr, status, error) {
console.error("Delete error:", xhr, status, error);
var errorMessage = "Error deleting rate limit";
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
}
alert(errorMessage);
}
});
};
// Global variables for refresh functionality
let refreshInterval = null;
let refreshCount = 0;
const MAX_REFRESH_COUNT = 5;
// Function to refresh usage statistics
window.refreshUsageStats = function() {
const button = document.getElementById('refreshUsageBtn');
const progressDiv = document.getElementById('refreshProgress');
const progressBar = document.getElementById('progressBar');
// Disable button and show progress
button.disabled = true;
button.innerHTML = '<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> Auto Refreshing...';
progressDiv.style.display = 'block';
// Reset counters
refreshCount = 0;
// Start refresh cycle
refreshInterval = setInterval(fetchUsageData, 1000);
fetchUsageData(); // Initial fetch
};
// Function to fetch usage data via AJAX
function fetchUsageData() {
const consumerId = '{{ consumer.consumer_id }}';
const panel = document.getElementById('usageStatsPanel');
console.log('Fetching usage data for consumer:', consumerId);
// Add refreshing effect to panel
panel.classList.add('panel-refreshing');
$.ajax({
url: '{% url "consumers-usage-data" consumer.consumer_id %}',
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
timeout: 5000, // 5 second timeout
success: function(data) {
console.log('Usage data received:', data);
updateUsageDisplay(data);
refreshCount++;
// Update progress bar
const progress = (refreshCount / MAX_REFRESH_COUNT) * 100;
const progressBar = document.getElementById('progressBar');
progressBar.style.width = progress + '%';
progressBar.textContent = refreshCount + '/' + MAX_REFRESH_COUNT;
// Stop after 5 seconds
if (refreshCount >= MAX_REFRESH_COUNT) {
clearInterval(refreshInterval);
resetRefreshButton();
panel.classList.remove('panel-refreshing');
}
},
error: function(xhr, status, error) {
console.error('Error fetching usage data:', error, xhr.responseText);
showRefreshError(error);
refreshCount++;
const progress = (refreshCount / MAX_REFRESH_COUNT) * 100;
document.getElementById('progressBar').style.width = progress + '%';
if (refreshCount >= MAX_REFRESH_COUNT) {
clearInterval(refreshInterval);
resetRefreshButton();
panel.classList.remove('panel-refreshing');
}
}
});
}
// Function to show refresh error
function showRefreshError(error) {
const errorDiv = document.getElementById('refreshError');
if (!errorDiv) {
const newErrorDiv = document.createElement('div');
newErrorDiv.id = 'refreshError';
newErrorDiv.className = 'alert alert-warning alert-dismissible';
newErrorDiv.style.marginTop = '10px';
newErrorDiv.innerHTML = '<button type="button" class="close" data-dismiss="alert">&times;</button><strong>Warning:</strong> Failed to fetch latest data. Continuing...';
document.getElementById('usageStatsContent').appendChild(newErrorDiv);
// Auto-hide after 3 seconds
setTimeout(function() {
if (newErrorDiv.parentNode) {
newErrorDiv.parentNode.removeChild(newErrorDiv);
}
}, 3000);
}
}
// Function to update usage display with new data
function updateUsageDisplay(data) {
console.log('Updating display with data:', data);
if (data) {
const periods = ['per_second', 'per_minute', 'per_hour', 'per_day', 'per_week', 'per_month'];
periods.forEach(function(period) {
const periodData = data[period];
const periodDiv = document.querySelector('[data-period="' + period + '"]');
if (periodDiv) {
const callsSpan = periodDiv.querySelector('.usage-calls');
const resetSpan = periodDiv.querySelector('.usage-reset');
if (callsSpan) {
const oldCalls = callsSpan.textContent.match(/-?\d+/);
const newCalls = periodData ? periodData.calls_made : null;
const effectiveLimit = getEffectiveLimit(period);
const limitText = effectiveLimit ? ' of ' + effectiveLimit : '';
const displayCalls = !periodData ? 'Unlimited' : (newCalls === -1 ? 'Not tracked' : newCalls + limitText + ' calls made');
// Check if calls increased (only if periodData exists)
const callsIncreased = periodData && oldCalls && parseInt(oldCalls[0]) < newCalls && newCalls !== -1;
callsSpan.textContent = displayCalls;
// Add visual feedback only if data is tracked
if (periodData) {
callsSpan.classList.add('updating');
if (callsIncreased) {
periodDiv.classList.add('data-updated');
// Flash effect for increased calls
callsSpan.style.backgroundColor = '#28a745';
callsSpan.style.color = 'white';
setTimeout(function() {
callsSpan.style.backgroundColor = '#d4edda';
callsSpan.style.color = '';
}, 200);
}
setTimeout(function() {
callsSpan.classList.remove('updating');
periodDiv.classList.remove('data-updated');
callsSpan.style.backgroundColor = '';
}, 1000);
}
}
if (resetSpan) {
if (periodData) {
const resetText = periodData.reset_in_seconds === -1 ? 'Not tracked' : 'Resets in ' + periodData.reset_in_seconds + ' seconds';
resetSpan.textContent = resetText;
resetSpan.style.display = 'block';
// Add subtle animation to reset timer
resetSpan.style.opacity = '0.7';
setTimeout(function() {
resetSpan.style.opacity = '1';
}, 300);
} else {
resetSpan.style.display = 'none';
}
}
}
});
// Show last updated time
updateLastRefreshTime();
}
}
// Function to update last refresh time
window.updateLastRefreshTime = function() {
let timeDiv = document.getElementById('lastRefreshTime');
if (!timeDiv) {
timeDiv = document.createElement('small');
timeDiv.id = 'lastRefreshTime';
timeDiv.className = 'text-muted';
timeDiv.style.display = 'block';
timeDiv.style.textAlign = 'center';
timeDiv.style.marginTop = '10px';
document.getElementById('usageStatsContent').appendChild(timeDiv);
}
const now = new Date();
timeDiv.textContent = 'Last updated: ' + now.toLocaleTimeString();
}
// Function to reset refresh button
window.resetRefreshButton = function() {
const button = document.getElementById('refreshUsageBtn');
const progressDiv = document.getElementById('refreshProgress');
button.disabled = false;
button.innerHTML = '<span class="glyphicon glyphicon-refresh"></span> Auto Refresh (5s)';
progressDiv.style.display = 'none';
document.getElementById('progressBar').style.width = '0%';
document.getElementById('progressBar').textContent = '';
// Remove any error messages
const errorDiv = document.getElementById('refreshError');
if (errorDiv && errorDiv.parentNode) {
errorDiv.parentNode.removeChild(errorDiv);
}
};
</script>
{% endcomment %}
{% endblock extrajs %}

View File

@ -0,0 +1 @@
# Template tags module for consumers app

View File

@ -0,0 +1,96 @@
"""
Custom template filters for consumers app
"""
from django import template
from django.conf import settings
from datetime import datetime
import logging
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter
def parse_iso_date(date_str, format_str="Y-m-d H:i"):
"""
Parse ISO date string and format it for display
Usage: {{ date_string|parse_iso_date:"Y-m-d H:i" }}
"""
if not date_str or date_str in ["", "null", "None", None]:
return "N/A"
# Convert to string if it's not already
if not isinstance(date_str, str):
date_str = str(date_str)
# List of common date formats to try
formats_to_try = [
"%Y-%m-%dT%H:%M:%SZ", # 2024-01-01T12:00:00Z
"%Y-%m-%dT%H:%M:%S", # 2024-01-01T12:00:00
"%Y-%m-%dT%H:%M:%S.%fZ", # 2024-01-01T12:00:00.000Z
"%Y-%m-%dT%H:%M:%S.%f", # 2024-01-01T12:00:00.000
"%Y-%m-%d %H:%M:%S", # 2024-01-01 12:00:00
settings.API_DATE_FORMAT_WITH_SECONDS, # From settings
]
# Try to parse with different formats
for fmt in formats_to_try:
try:
parsed_date = datetime.strptime(date_str, fmt)
# Convert Django date format to Python strftime format
django_to_python = {
"Y": "%Y",
"m": "%m",
"d": "%d",
"H": "%H",
"i": "%M",
"s": "%S",
}
# Simple format conversion for common cases
python_format = format_str
for django_fmt, python_fmt in django_to_python.items():
python_format = python_format.replace(django_fmt, python_fmt)
return parsed_date.strftime(python_format)
except (ValueError, TypeError):
continue
# Try using fromisoformat for Python 3.7+
try:
# Handle timezone indicator
clean_date_str = date_str.replace("Z", "+00:00")
parsed_date = datetime.fromisoformat(clean_date_str.replace("Z", ""))
# Convert format and return
python_format = format_str
django_to_python = {
"Y": "%Y",
"m": "%m",
"d": "%d",
"H": "%H",
"i": "%M",
"s": "%S",
}
for django_fmt, python_fmt in django_to_python.items():
python_format = python_format.replace(django_fmt, python_fmt)
return parsed_date.strftime(python_format)
except (ValueError, AttributeError):
pass
# Last resort - return the original string or N/A
logger.warning(f"Could not parse date string: {date_str}")
return "Invalid Date"
@register.filter
def smart_default(value, default_value="N/A"):
"""
Smart default filter that handles various empty/null cases
Usage: {{ value|smart_default:"Default Value" }}
"""
if value is None or value == "" or value == "null" or value == "None":
return default_value
return value

View File

@ -5,19 +5,22 @@ URLs for consumers app
from django.urls import re_path
from .views import IndexView, DetailView, EnableView, DisableView
from .views import IndexView, DetailView, EnableView, DisableView, UsageDataAjaxView
urlpatterns = [
re_path(r'^$',
IndexView.as_view(),
name='consumers-index'),
re_path(r'^(?P<consumer_id>[0-9a-z\-]+)$',
re_path(r'^(?P<consumer_id>[0-9a-zA-Z\-_(){}%]+)$',
DetailView.as_view(),
name='consumers-detail'),
re_path(r'^(?P<consumer_id>[0-9a-z\-]+)/enable$',
re_path(r'^(?P<consumer_id>[0-9a-zA-Z\-_(){}%]+)/enable$',
EnableView.as_view(),
name='consumers-enable'),
re_path(r'^(?P<consumer_id>[0-9a-z\-]+)/disable$',
re_path(r'^(?P<consumer_id>[0-9a-zA-Z\-_(){}%]+)/disable$',
DisableView.as_view(),
name='consumers-disable'),
re_path(r'^(?P<consumer_id>[0-9a-zA-Z\-_(){}%]+)/usage-data$',
UsageDataAjaxView.as_view(),
name='consumers-usage-data'),
]

View File

@ -4,12 +4,15 @@ Views of consumers app
"""
from datetime import datetime
import datetime as dt_module
import json
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.views.generic import TemplateView, RedirectView, FormView
from django.http import JsonResponse, HttpResponseRedirect
from obp.api import API, APIError
from base.filters import BaseFilter, FilterTime
@ -19,32 +22,36 @@ from .forms import ApiConsumersForm
class FilterAppType(BaseFilter):
"""Filter consumers by application type"""
filter_type = 'app_type'
filter_type = "app_type"
def _apply(self, data, filter_value):
filtered = [x for x in data if x['app_type'] == filter_value]
filtered = [x for x in data if x["app_type"] == filter_value]
return filtered
class FilterEnabled(BaseFilter):
"""Filter consumers by enabled state"""
filter_type = 'enabled'
filter_type = "enabled"
def _apply(self, data, filter_value):
enabled = filter_value in ['true']
filtered = [x for x in data if x['enabled'] == enabled]
enabled = filter_value in ["true"]
filtered = [x for x in data if x["enabled"] == enabled]
return filtered
class IndexView(LoginRequiredMixin, TemplateView):
"""Index view for consumers"""
template_name = "consumers/index.html"
def scrub(self, consumers):
"""Scrubs data in the given consumers to adher to certain formats"""
for consumer in consumers:
consumer['created'] = datetime.strptime(
consumer['created'], settings.API_DATE_FORMAT_WITH_SECONDS )
consumer["created"] = datetime.strptime(
consumer["created"], settings.API_DATE_FORMAT_WITH_SECONDS
)
return consumers
def compile_statistics(self, consumers):
@ -52,46 +59,50 @@ class IndexView(LoginRequiredMixin, TemplateView):
unique_developer_email = {}
unique_name = {}
for consumer in consumers:
unique_developer_email[consumer['developer_email']] = True
unique_name[consumer['app_name']] = True
unique_developer_email[consumer["developer_email"]] = True
unique_name[consumer["app_name"]] = True
unique_developer_email = unique_developer_email.keys()
unique_name = unique_name.keys()
statistics = {
'consumers_num': len(consumers),
'unique_developer_email_num': len(unique_developer_email),
'unique_name_num': len(unique_name),
"consumers_num": len(consumers),
"unique_developer_email_num": len(unique_developer_email),
"unique_name_num": len(unique_name),
}
return statistics
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
consumers = []
sorted_consumers=[]
api = API(self.request.session.get('obp'))
sorted_consumers = []
api = API(self.request.session.get("obp"))
try:
limit = self.request.GET.get('limit', 50)
offset = self.request.GET.get('offset', 0)
urlpath = '/management/consumers?limit={}&offset={}'.format(limit, offset)
limit = self.request.GET.get("limit", 50)
offset = self.request.GET.get("offset", 0)
urlpath = "/management/consumers?limit={}&offset={}".format(limit, offset)
consumers = api.get(urlpath)
if 'code' in consumers and consumers['code'] >= 400:
messages.error(self.request, consumers['message'])
if "code" in consumers and consumers["code"] >= 400:
messages.error(self.request, consumers["message"])
else:
consumers = FilterEnabled(context, self.request.GET)\
.apply(consumers['consumers'])
consumers = FilterAppType(context, self.request.GET)\
.apply(consumers)
consumers = FilterTime(context, self.request.GET, 'created')\
.apply(consumers)
consumers = FilterEnabled(context, self.request.GET).apply(
consumers["consumers"]
)
consumers = FilterAppType(context, self.request.GET).apply(consumers)
consumers = FilterTime(context, self.request.GET, "created").apply(
consumers
)
consumers = self.scrub(consumers)
sorted_consumers = sorted(
consumers, key=lambda consumer: consumer['created'], reverse=True)
consumers, key=lambda consumer: consumer["created"], reverse=True
)
context.update({
'consumers': sorted_consumers,
'limit': limit,
'offset': offset,
'statistics': self.compile_statistics(consumers),
})
context.update(
{
"consumers": sorted_consumers,
"limit": limit,
"offset": offset,
"statistics": self.compile_statistics(consumers),
}
)
except APIError as err:
messages.error(self.request, err)
@ -100,111 +111,531 @@ class IndexView(LoginRequiredMixin, TemplateView):
class DetailView(LoginRequiredMixin, FormView):
"""Detail view for a consumer"""
form_class = ApiConsumersForm
template_name = "consumers/detail.html"
def dispatch(self, request, *args, **kwargs):
self.api = API(request.session.get('obp'))
self.api = API(request.session.get("obp"))
return super(DetailView, self).dispatch(request, *args, **kwargs)
def get_form(self, *args, **kwargs):
form = super(DetailView, self).get_form(*args, **kwargs)
form.fields['consumer_id'].initial = self.kwargs['consumer_id']
form.fields["consumer_id"].initial = self.kwargs["consumer_id"]
# Get call limits data to populate form
api = API(self.request.session.get("obp"))
try:
call_limits_urlpath = (
"/management/consumers/{}/consumer/rate-limits".format(
self.kwargs["consumer_id"]
)
)
call_limits = api.get(call_limits_urlpath)
if not ("code" in call_limits and call_limits["code"] >= 400):
# Populate form with existing rate limiting data
if "from_date" in call_limits and call_limits["from_date"]:
try:
from_date_str = call_limits["from_date"].replace("Z", "")
# Parse and ensure no timezone info for form field
dt = datetime.fromisoformat(from_date_str)
if dt.tzinfo:
dt = dt.replace(tzinfo=None)
form.fields["from_date"].initial = dt
except:
pass
if "to_date" in call_limits and call_limits["to_date"]:
try:
to_date_str = call_limits["to_date"].replace("Z", "")
# Parse and ensure no timezone info for form field
dt = datetime.fromisoformat(to_date_str)
if dt.tzinfo:
dt = dt.replace(tzinfo=None)
form.fields["to_date"].initial = dt
except:
pass
form.fields["per_second_call_limit"].initial = call_limits.get(
"per_second_call_limit", "-1"
)
form.fields["per_minute_call_limit"].initial = call_limits.get(
"per_minute_call_limit", "-1"
)
form.fields["per_hour_call_limit"].initial = call_limits.get(
"per_hour_call_limit", "-1"
)
form.fields["per_day_call_limit"].initial = call_limits.get(
"per_day_call_limit", "-1"
)
form.fields["per_week_call_limit"].initial = call_limits.get(
"per_week_call_limit", "-1"
)
form.fields["per_month_call_limit"].initial = call_limits.get(
"per_month_call_limit", "-1"
)
except:
pass
return form
def form_valid(self, form):
def post(self, request, *args, **kwargs):
"""Handle POST requests for rate limit CRUD operations"""
action = request.POST.get("action")
"""Put limits data to API"""
# Check if this is an AJAX request
if request.headers.get("X-Requested-With") == "XMLHttpRequest" or action in [
"create",
"update",
"delete",
]:
if action == "create":
return self.create_rate_limit(request)
elif action == "update":
return self.update_rate_limit(request)
elif action == "delete":
return self.delete_rate_limit(request)
# Fallback to original form handling for compatibility
form = self.get_form()
if form.is_valid():
return self.form_valid_legacy(request, form)
else:
return self.form_invalid(form)
def create_rate_limit(self, request):
"""Create a new rate limit using v6.0.0 POST API"""
try:
data = ''
api_consumers_form = ApiConsumersForm(self.request.POST)
if api_consumers_form.is_valid():
data = api_consumers_form.cleaned_data
consumer_id = self.kwargs["consumer_id"]
urlpath = '/management/consumers/{}/consumer/calls_limit'.format(data['consumer_id'])
# Helper function to format datetime to UTC
def format_datetime_utc(dt_str):
if not dt_str:
return "2024-01-01T00:00:00Z"
try:
dt = datetime.fromisoformat(dt_str)
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
except:
return "2024-01-01T00:00:00Z"
payload = {
'per_minute_call_limit': data['per_minute_call_limit'],
'per_hour_call_limit': data['per_hour_call_limit'],
'per_day_call_limit': data['per_day_call_limit'],
'per_week_call_limit': data['per_week_call_limit'],
'per_month_call_limit': data['per_month_call_limit']
"from_date": format_datetime_utc(request.POST.get("from_date")),
"to_date": format_datetime_utc(request.POST.get("to_date")),
"per_second_call_limit": str(
request.POST.get("per_second_call_limit", "-1")
),
"per_minute_call_limit": str(
request.POST.get("per_minute_call_limit", "-1")
),
"per_hour_call_limit": str(
request.POST.get("per_hour_call_limit", "-1")
),
"per_day_call_limit": str(request.POST.get("per_day_call_limit", "-1")),
"per_week_call_limit": str(
request.POST.get("per_week_call_limit", "-1")
),
"per_month_call_limit": str(
request.POST.get("per_month_call_limit", "-1")
),
}
except APIError as err:
messages.error(self.request, err)
return super(DetailView, self).form_invalid(api_consumers_form)
except Exception as err:
messages.error(self.request, "{}".format(err))
return super(DetailView, self).form_invalid(api_consumers_form)
msg = 'calls limit of consumer {} has been updated successfully.'.format(
data['consumer_id'])
messages.success(self.request, msg)
self.success_url = self.request.path
return super(DetailView, self).form_valid(api_consumers_form)
# Use v6.0.0 API for creating rate limits
urlpath = "/management/consumers/{}/consumer/rate-limits".format(
consumer_id
)
response = self.api.post(
urlpath, payload, version=settings.API_VERSION["v600"]
)
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
else:
messages.success(request, "Rate limit created successfully.")
except APIError as err:
messages.error(request, str(err))
except Exception as err:
messages.error(request, "Error creating rate limit: {}".format(err))
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def update_rate_limit(self, request):
"""Update existing rate limit using v6.0.0 PUT API"""
try:
consumer_id = self.kwargs["consumer_id"]
rate_limiting_id = request.POST.get("rate_limit_id")
if not rate_limiting_id:
messages.error(request, "Rate limiting ID is required for update.")
return JsonResponse(
{"success": False, "error": "Missing rate limiting ID"}
)
# Helper function to format datetime to UTC
def format_datetime_utc(dt_str):
if not dt_str:
return "2024-01-01T00:00:00Z"
try:
dt = datetime.fromisoformat(dt_str)
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
except:
return "2024-01-01T00:00:00Z"
payload = {
"from_date": format_datetime_utc(request.POST.get("from_date")),
"to_date": format_datetime_utc(request.POST.get("to_date")),
"per_second_call_limit": str(
request.POST.get("per_second_call_limit", "-1")
),
"per_minute_call_limit": str(
request.POST.get("per_minute_call_limit", "-1")
),
"per_hour_call_limit": str(
request.POST.get("per_hour_call_limit", "-1")
),
"per_day_call_limit": str(request.POST.get("per_day_call_limit", "-1")),
"per_week_call_limit": str(
request.POST.get("per_week_call_limit", "-1")
),
"per_month_call_limit": str(
request.POST.get("per_month_call_limit", "-1")
),
}
# Use v6.0.0 API for updating rate limits with rate_limiting_id
urlpath = "/management/consumers/{}/consumer/rate-limits/{}".format(
consumer_id, rate_limiting_id
)
response = self.api.put(
urlpath, payload, version=settings.API_VERSION["v600"]
)
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
else:
messages.success(request, "Rate limit updated successfully.")
except APIError as err:
messages.error(request, str(err))
except Exception as err:
messages.error(request, "Error updating rate limit: {}".format(err))
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def delete_rate_limit(self, request):
"""Delete a rate limit using v6.0.0 DELETE API"""
try:
consumer_id = self.kwargs["consumer_id"]
rate_limiting_id = request.POST.get("rate_limiting_id")
if not rate_limiting_id:
messages.error(request, "Rate limiting ID is required for deletion.")
return JsonResponse(
{"success": False, "error": "Missing rate limiting ID"}
)
# Use v6.0.0 API for deleting rate limits
urlpath = "/management/consumers/{}/consumer/rate-limits/{}".format(
consumer_id, rate_limiting_id
)
response = self.api.delete(urlpath, version=settings.API_VERSION["v600"])
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
else:
messages.success(request, "Rate limit deleted successfully.")
except APIError as err:
messages.error(request, str(err))
except Exception as err:
messages.error(request, "Error deleting rate limit: {}".format(err))
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def form_valid_legacy(self, request, form):
"""Legacy form handling for backwards compatibility"""
try:
data = form.cleaned_data
urlpath = "/management/consumers/{}/consumer/rate-limits".format(
data["consumer_id"]
)
# Helper function to format datetime to UTC
def format_datetime_utc(dt):
if not dt:
return "2024-01-01T00:00:00Z"
# Convert to UTC and format as required by API
if dt.tzinfo:
dt = dt.astimezone(dt_module.timezone.utc).replace(tzinfo=None)
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
payload = {
"from_date": format_datetime_utc(data["from_date"]),
"to_date": format_datetime_utc(data["to_date"]),
"per_second_call_limit": str(data["per_second_call_limit"])
if data["per_second_call_limit"] is not None
else "-1",
"per_minute_call_limit": str(data["per_minute_call_limit"])
if data["per_minute_call_limit"] is not None
else "-1",
"per_hour_call_limit": str(data["per_hour_call_limit"])
if data["per_hour_call_limit"] is not None
else "-1",
"per_day_call_limit": str(data["per_day_call_limit"])
if data["per_day_call_limit"] is not None
else "-1",
"per_week_call_limit": str(data["per_week_call_limit"])
if data["per_week_call_limit"] is not None
else "-1",
"per_month_call_limit": str(data["per_month_call_limit"])
if data["per_month_call_limit"] is not None
else "-1",
}
response = self.api.put(
urlpath, payload, version=settings.API_VERSION["v510"]
)
if "code" in response and response["code"] >= 400:
messages.error(request, response["message"])
return super(DetailView, self).form_invalid(form)
except APIError as err:
messages.error(request, err)
return super(DetailView, self).form_invalid(form)
except Exception as err:
messages.error(request, "{}".format(err))
return super(DetailView, self).form_invalid(form)
msg = "Rate limits for consumer {} have been updated successfully.".format(
data["consumer_id"]
)
messages.success(request, msg)
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "redirect": request.path})
else:
return HttpResponseRedirect(request.path)
def get(self, request, *args, **kwargs):
# Check if this is an AJAX request for usage data
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return self.get_usage_data_ajax()
return super(DetailView, self).get(request, *args, **kwargs)
def get_usage_data_ajax(self):
"""Return usage data as JSON for AJAX refresh"""
api = API(self.request.session.get("obp"))
try:
call_limits_urlpath = (
"/management/consumers/{}/consumer/rate-limits".format(
self.kwargs["consumer_id"]
)
)
call_limits = api.get(call_limits_urlpath)
if "code" in call_limits and call_limits["code"] >= 400:
return JsonResponse({"error": call_limits["message"]}, status=400)
return JsonResponse(call_limits)
except APIError as err:
return JsonResponse({"error": str(err)}, status=500)
except Exception as err:
return JsonResponse({"error": str(err)}, status=500)
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
api = API(self.request.session.get('obp'))
try:
urlpath = '/management/consumers/{}'.format(self.kwargs['consumer_id'])
consumer = api.get(urlpath)
consumer['created'] = datetime.strptime(
consumer['created'], settings.API_DATE_FORMAT_WITH_SECONDS )
api = API(self.request.session.get("obp"))
consumer = {}
call_limits = {}
# Initialize current_usage with default values
current_usage = {
"per_second": {"calls_made": -1, "reset_in_seconds": -1},
"per_minute": {"calls_made": -1, "reset_in_seconds": -1},
"per_hour": {"calls_made": -1, "reset_in_seconds": -1},
"per_day": {"calls_made": -1, "reset_in_seconds": -1},
"per_week": {"calls_made": -1, "reset_in_seconds": -1},
"per_month": {"calls_made": -1, "reset_in_seconds": -1},
}
call_limits_urlpath = '/management/consumers/{}/consumer/call-limits'.format(self.kwargs['consumer_id'])
consumer_call_limtis = api.get(call_limits_urlpath)
if 'code' in consumer_call_limtis and consumer_call_limtis['code'] >= 400:
messages.error(self.request, "{}".format(consumer_call_limtis['message']))
try:
urlpath = "/management/consumers/{}".format(self.kwargs["consumer_id"])
consumer = api.get(urlpath)
if "code" in consumer and consumer["code"] >= 400:
messages.error(self.request, consumer["message"])
consumer = {}
else:
consumer['per_minute_call_limit'] = consumer_call_limtis['per_minute_call_limit']
consumer['per_hour_call_limit'] = consumer_call_limtis['per_hour_call_limit']
consumer['per_day_call_limit'] = consumer_call_limtis['per_day_call_limit']
consumer['per_week_call_limit'] = consumer_call_limtis['per_week_call_limit']
consumer['per_month_call_limit'] = consumer_call_limtis['per_month_call_limit']
consumer["created"] = datetime.strptime(
consumer["created"], settings.API_DATE_FORMAT_WITH_SECONDS
)
# Get call limits using the correct API endpoint
call_limits_urlpath = (
"/management/consumers/{}/consumer/rate-limits".format(
self.kwargs["consumer_id"]
)
)
call_limits = api.get(
call_limits_urlpath, version=settings.API_VERSION["v510"]
)
# Get current usage data using v6.0.0 API
current_usage_urlpath = (
"/management/consumers/{}/consumer/current-usage".format(
self.kwargs["consumer_id"]
)
)
current_usage = api.get(
current_usage_urlpath, version=settings.API_VERSION["v600"]
)
if "code" in current_usage and current_usage["code"] >= 400:
# If current usage fails, keep the default values already set
pass
if "code" in call_limits and call_limits["code"] >= 400:
messages.error(self.request, "{}".format(call_limits["message"]))
call_limits = {"limits": []}
else:
# Handle different API response structures
import uuid
# Handle case where API returns data directly instead of in 'limits' array
if (
"limits" not in call_limits
and "per_second_call_limit" in call_limits
):
# API returned single limit object, wrap it in limits array
if "rate_limiting_id" not in call_limits:
call_limits["rate_limiting_id"] = str(uuid.uuid4())
call_limits = {"limits": [call_limits]}
elif "limits" not in call_limits:
# No limits data found
call_limits = {"limits": []}
else:
# Ensure each limit has a rate_limiting_id
for limit in call_limits.get("limits", []):
if (
"rate_limiting_id" not in limit
or not limit["rate_limiting_id"]
):
limit["rate_limiting_id"] = str(uuid.uuid4())
# For backwards compatibility, merge first limit into consumer if limits exist
if call_limits.get("limits") and len(call_limits["limits"]) > 0:
first_limit = call_limits["limits"][0]
consumer.update(
{
"from_date": first_limit.get("from_date", ""),
"to_date": first_limit.get("to_date", ""),
"per_second_call_limit": first_limit.get(
"per_second_call_limit", "-1"
),
"per_minute_call_limit": first_limit.get(
"per_minute_call_limit", "-1"
),
"per_hour_call_limit": first_limit.get(
"per_hour_call_limit", "-1"
),
"per_day_call_limit": first_limit.get(
"per_day_call_limit", "-1"
),
"per_week_call_limit": first_limit.get(
"per_week_call_limit", "-1"
),
"per_month_call_limit": first_limit.get(
"per_month_call_limit", "-1"
),
"current_state": call_limits.get("current_state", {}),
"created_at": first_limit.get("created_at", ""),
"updated_at": first_limit.get("updated_at", ""),
}
)
except APIError as err:
messages.error(self.request, err)
except Exception as err:
messages.error(self.request, "{}".format(err))
finally:
context.update({
'consumer': consumer
})
# Ensure current_usage always has the expected structure
if not current_usage or "per_second" not in current_usage:
current_usage = {
"per_second": {"calls_made": -1, "reset_in_seconds": -1},
"per_minute": {"calls_made": -1, "reset_in_seconds": -1},
"per_hour": {"calls_made": -1, "reset_in_seconds": -1},
"per_day": {"calls_made": -1, "reset_in_seconds": -1},
"per_week": {"calls_made": -1, "reset_in_seconds": -1},
"per_month": {"calls_made": -1, "reset_in_seconds": -1},
}
context.update({"consumer": consumer, "call_limits": call_limits, "current_usage": current_usage})
return context
class UsageDataAjaxView(LoginRequiredMixin, TemplateView):
"""AJAX view to return current usage data for real-time updates"""
def get(self, request, *args, **kwargs):
api = API(self.request.session.get("obp"))
try:
current_usage_urlpath = (
"/management/consumers/{}/consumer/current-usage".format(
self.kwargs["consumer_id"]
)
)
current_usage = api.get(
current_usage_urlpath, version=settings.API_VERSION["v600"]
)
if "code" in current_usage and current_usage["code"] >= 400:
return JsonResponse({"error": current_usage["message"]}, status=400)
return JsonResponse(current_usage)
except APIError as err:
return JsonResponse({"error": str(err)}, status=500)
except Exception as err:
return JsonResponse({"error": str(err)}, status=500)
class EnableDisableView(LoginRequiredMixin, RedirectView):
"""View to enable or disable a consumer"""
enabled = False
success = None
def get_redirect_url(self, *args, **kwargs):
api = API(self.request.session.get('obp'))
api = API(self.request.session.get("obp"))
try:
urlpath = '/management/consumers/{}'.format(kwargs['consumer_id'])
payload = {'enabled': self.enabled}
urlpath = "/management/consumers/{}".format(kwargs["consumer_id"])
payload = {"enabled": self.enabled}
response = api.put(urlpath, payload)
if 'code' in response and response['code'] >= 400:
messages.error(self.request, response['message'])
if "code" in response and response["code"] >= 400:
messages.error(self.request, response["message"])
else:
messages.success(self.request, self.success)
except APIError as err:
messages.error(self.request, err)
urlpath = self.request.POST.get('next', reverse('consumers-index'))
urlpath = self.request.POST.get("next", reverse("consumers-index"))
query = self.request.GET.urlencode()
redirect_url = '{}?{}'.format(urlpath, query)
redirect_url = "{}?{}".format(urlpath, query)
return redirect_url
class EnableView(EnableDisableView):
"""View to enable a consumer"""
enabled = True
success = "Consumer has been enabled."
class DisableView(EnableDisableView):
"""View to disable a consumer"""
enabled = False
success = "Consumer has been disabled."

View File

@ -43,7 +43,7 @@ class API(object):
self.start_session(session_data)
self.session_data = session_data
def call(self, method='GET', url='', payload=None, version=settings.API_VERSION['v500']):
def call(self, method='GET', url='', payload=None, version=settings.API_VERSION['v510']):
"""Workhorse which actually calls the API"""
log(logging.INFO, '{} {}'.format(method, url))
if payload:
@ -64,7 +64,7 @@ class API(object):
response.execution_time = elapsed
return response
def get(self, urlpath='', version=settings.API_VERSION['v500']):
def get(self, urlpath='', version=settings.API_VERSION['v510']):
"""
Gets data from the API
@ -77,7 +77,7 @@ class API(object):
else:
return response
def delete(self, urlpath, version=settings.API_VERSION['v500']):
def delete(self, urlpath, version=settings.API_VERSION['v510']):
"""
Deletes data from the API
@ -87,7 +87,7 @@ class API(object):
response = self.call('DELETE', url)
return self.handle_response(response)
def post(self, urlpath, payload, version=settings.API_VERSION['v500']):
def post(self, urlpath, payload, version=settings.API_VERSION['v510']):
"""
Posts data to given urlpath with given payload
@ -97,7 +97,7 @@ class API(object):
response = self.call('POST', url, payload)
return self.handle_response(response)
def put(self, urlpath, payload, version=settings.API_VERSION['v500']):
def put(self, urlpath, payload, version=settings.API_VERSION['v510']):
"""
Puts data on given urlpath with given payload
@ -175,4 +175,4 @@ class API(object):
result = self.get('/users')
for user in result['users']:
choices.append((user['user_id'], user['username']))
return choices
return choices

View File

@ -92,7 +92,15 @@ class OAuthAuthorizeView(RedirectView, LoginToDjangoMixin):
def get_redirect_url(self, *args, **kwargs):
session_data = self.request.session.get('obp')
if session_data is None:
messages.error(self.request, 'OAuth session expired. Please try logging in again.')
return reverse('home')
authenticator_kwargs = session_data.get('authenticator_kwargs')
if authenticator_kwargs is None:
messages.error(self.request, 'OAuth session data missing. Please try logging in again.')
return reverse('home')
authenticator = OAuthAuthenticator(**authenticator_kwargs)
authorization_url = self.request.build_absolute_uri()
try:

5
cookies.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_127.0.0.1 FALSE / FALSE 1756898860 sessionid .eJxVjL0OgjAAhN-lsyE2yMLWH1pQoKEQiV1MJY0aEmqgxoHw7rYjyw13930rsI8PSFegv-5lJvcetLMzSEMd2VBGAvlEu_mwv9_Hn56fS9A4O5rJ45LHojjntKpVyxCjDayYuvKKxUIq1iecKIElqr1qMcNsnGdODcxunX9KrGh_KeIjUjQXJcsIbGFX0gQTCREH27b9AecrO7Y:1utlZY:ZPojoGt6azhiwEYoVg8XIJi0-y1-UTA-zTRGmMVCiTc

35
development/.env.example Normal file
View File

@ -0,0 +1,35 @@
# Environment configuration for API Manager development
# Copy this file to .env and update the values as needed
# Django Settings
SECRET_KEY=dev-secret-key-change-in-production
DEBUG=True
# API Configuration
API_HOST=http://127.0.0.1:8080
API_PORTAL=http://127.0.0.1:8080
# OAuth Configuration (Required - get these from your OBP API instance)
OAUTH_CONSUMER_KEY=d02e38f6-0f2f-42ba-a50c-662927e30058
OAUTH_CONSUMER_SECRET=sqdb35zzeqs20i1hkmazqiefvz4jupsdil5havpk
# Host Configuration
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,web
CALLBACK_BASE_URL=http://127.0.0.1:8000
# CSRF and CORS Configuration
CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
CORS_ORIGIN_WHITELIST=http://localhost:8000,http://127.0.0.1:8000
# Database Configuration (PostgreSQL - used by docker-compose)
DATABASE_URL=postgresql://apimanager:apimanager@db:5432/apimanager
# PostgreSQL Database Settings (for docker-compose)
POSTGRES_DB=apimanager
POSTGRES_USER=apimanager
POSTGRES_PASSWORD=apimanager
# Optional Settings
# API_EXPLORER_HOST=http://127.0.0.1:8082
# API_TESTER_URL=https://www.example.com
# SHOW_API_TESTER=False

View File

@ -0,0 +1,55 @@
FROM python:3.10
# Create non-root user
RUN groupadd --gid 1000 appuser \
&& useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client \
python3-tk \
tk \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --upgrade pip \
&& pip install -r requirements.txt \
&& pip install dj-database-url
# Copy project files explicitly
COPY requirements.txt /app/
COPY apimanager/ /app/apimanager/
COPY static/ /app/static/
COPY demo/ /app/demo/
COPY gunicorn.conf.py /app/
# Create necessary directories
RUN mkdir -p /app/logs /app/static /app/db /static-collected
# Copy development local settings directly to the correct location
COPY development/local_settings_dev.py /app/apimanager/apimanager/local_settings.py
# Copy entrypoint script to /usr/local/bin
COPY development/docker-entrypoint-dev.sh /usr/local/bin/docker-entrypoint-dev.sh
# Set proper permissions and ownership
RUN chmod +x /app/apimanager/manage.py /usr/local/bin/docker-entrypoint-dev.sh \
&& chown -R appuser:appuser /app \
&& chown -R appuser:appuser /static-collected
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8000
# Use entrypoint script
ENTRYPOINT ["/usr/local/bin/docker-entrypoint-dev.sh"]

101
development/README.md Normal file
View File

@ -0,0 +1,101 @@
# API Manager Development Environment
This folder contains Docker development setup for the Open Bank Project API Manager.
## Quick Start
```bash
# 1. Navigate to development directory
cd development
# 2. Copy environment template
cp .env.example .env
# 3. Run the setup script
./dev-setup.sh
# 4. Access the application
open http://localhost:8000
```
## What's Included
- **docker-compose.yml** - Orchestrates web and database services
- **Dockerfile.dev** - Development-optimized container image
- **local_settings_dev.py** - Django development settings
- **docker-entrypoint-dev.sh** - Container startup script
- **.env.example** - Environment variables template
## Services
- **api-manager-web** - Django application (port 8000)
- **api-manager-db** - PostgreSQL database (port 5434)
## Features
✅ Hot code reloading - changes reflect immediately
✅ PostgreSQL database with persistent storage
✅ Static files properly served
✅ Automatic database migrations
✅ Development superuser (admin/admin123)
✅ OAuth integration with OBP API
## Development Commands
```bash
# View logs
docker-compose logs api-manager-web
# Access container shell
docker-compose exec api-manager-web bash
# Django management commands
docker-compose exec api-manager-web bash -c 'cd apimanager && python manage.py shell'
# Database shell
docker-compose exec api-manager-db psql -U ${POSTGRES_USER:-apimanager} -d ${POSTGRES_DB:-apimanager}
# Stop services
docker-compose down
```
## Configuration
The setup uses environment variables defined in `.env`:
- `OAUTH_CONSUMER_KEY` - OAuth consumer key from OBP API
- `OAUTH_CONSUMER_SECRET` - OAuth consumer secret from OBP API
- `API_HOST` - OBP API server URL (default: http://host.docker.internal:8080)
- `POSTGRES_PASSWORD` - Database password (IMPORTANT: Change from default!)
- `POSTGRES_USER` - Database username (default: apimanager)
- `POSTGRES_DB` - Database name (default: apimanager)
### 🔒 Security Note
**IMPORTANT**: The default database password is `CHANGE_THIS_PASSWORD` and must be changed before deployment. Set a strong password in your `.env` file:
```bash
POSTGRES_PASSWORD=your_secure_password_here
```
## Testing OAuth Integration
1. **First, set a secure database password** in your `.env` file
2. Ensure OBP API is running on http://127.0.0.1:8080/ (accessible as host.docker.internal:8080 from containers)
3. Start the development environment
4. Navigate to http://localhost:8000
5. Click "Proceed to authentication server" to test OAuth flow
## Troubleshooting
- **Port conflicts**: Database uses port 5434 to avoid conflicts
- **OAuth errors**: Verify OAUTH_CONSUMER_KEY and OAUTH_CONSUMER_SECRET in .env
- **Database connection errors**: Ensure POSTGRES_PASSWORD is set in .env and matches between services
- **Connection refused to OBP API**: The setup uses `host.docker.internal:8080` to reach the host machine's OBP API from containers
- **Static files missing**: Restart containers with `docker-compose down && docker-compose up -d`
## Docker Networking
The development setup uses `host.docker.internal:8080` to allow containers to access the OBP API running on the host machine at `127.0.0.1:8080`. This is automatically configured in the docker-compose.yml file.
This development environment provides hot reloading and mirrors the production setup while remaining developer-friendly.

View File

@ -0,0 +1,91 @@
# API Manager Development Setup - Complete ✅
## Summary
Successfully created a complete Docker development environment for the Open Bank Project API Manager with the following achievements:
### ✅ What Was Accomplished
1. **Docker Compose Setup**: Complete development environment with PostgreSQL database
2. **Hot Code Reloading**: File changes automatically trigger Django server reload
3. **OAuth Integration**: Successfully integrated with OBP API at http://127.0.0.1:8080/
4. **Static Files**: Properly configured and served in development mode
5. **Container Naming**: All containers prefixed with `api-manager-`
6. **Database**: PostgreSQL on port 5434 to avoid conflicts
7. **Automated Setup**: Single command deployment with `./dev-setup.sh`
### 📁 Essential Files Created
```
development/
├── docker-compose.yml # Main orchestration file
├── Dockerfile.dev # Development container image
├── local_settings_dev.py # Django development settings
├── docker-entrypoint-dev.sh # Container startup script
├── .env.example # Environment template with OAuth credentials
├── dev-setup.sh # Automated setup script
└── README.md # Development documentation
```
### 🧪 Testing Results
**Application Access**: http://localhost:8000 - WORKING
**OAuth Integration**: Connected to OBP API via host.docker.internal:8080 - WORKING
**Static Files**: CSS/JS loading correctly - WORKING
**Database**: PostgreSQL with persistent storage - WORKING
**Hot Reloading**: Code changes reflect immediately - WORKING
**Admin Access**: admin/admin123 superuser created - WORKING
**Docker Networking**: Fixed container-to-host connectivity - WORKING
### 🔧 OAuth Credentials Used
```
OAUTH_CONSUMER_KEY=d02e38f6-0f2f-42ba-a50c-662927e30058
OAUTH_CONSUMER_SECRET=sqdb35zzeqs20i1hkmazqiefvz4jupsdil5havpk
API_HOST=http://host.docker.internal:8080
```
### 🚀 Usage
```bash
cd development
./dev-setup.sh
# Access http://localhost:8000
```
### 🏗️ Architecture
- **api-manager-web**: Django app (port 8000)
- **api-manager-db**: PostgreSQL (port 5434)
- **Volume Mounts**: Source code hot-reload enabled
- **Network**: Internal Docker network for service communication
### ✨ Key Features
- Zero-config startup with working OAuth
- Real-time code changes without restart
- Production-like database setup
- Comprehensive logging and debugging
- Automated database migrations
- Static file serving for development
### 🧹 Code Changes Made
**Minimal changes to original codebase:**
1. Added static file serving in `urls.py` for development
2. All Docker files contained in `development/` folder
3. Original codebase remains unchanged for production
**Files modified in main codebase:**
- `apimanager/apimanager/urls.py` - Added static file serving for DEBUG mode
**Files removed:**
- `apimanager/apimanager/local_settings.py` - Replaced with development version
### 🔧 Docker Network Fix Applied
**Issue**: Container couldn't connect to OBP API at 127.0.0.1:8080 (connection refused)
**Solution**: Updated API_HOST to use `host.docker.internal:8080` with extra_hosts configuration
**Result**: OAuth flow now works correctly from within Docker containers
The development environment is fully functional and ready for API Manager development work with the OBP API.

118
development/dev-setup.sh Executable file
View File

@ -0,0 +1,118 @@
#!/bin/bash
# API Manager Development Environment Setup Script
# This script sets up the Docker development environment for API Manager
set -e
echo "🚀 API Manager Development Environment Setup"
echo "============================================="
echo ""
echo " Running from: $(pwd)"
echo " This script should be run from the development/ directory"
echo ""
# Check if Docker and Docker Compose are installed
if ! command -v docker &> /dev/null; then
echo "❌ Docker is not installed. Please install Docker first."
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
echo "❌ Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
# Create necessary directories
echo "📁 Creating necessary directories..."
mkdir -p ../logs
# Setup environment file
if [ ! -f .env ]; then
echo "📝 Creating .env file from template..."
cp .env.example .env
echo "⚠️ Please edit .env file and set your OAuth credentials:"
echo " - OAUTH_CONSUMER_KEY"
echo " - OAUTH_CONSUMER_SECRET"
echo ""
read -p "Do you want to edit .env now? (y/n): " edit_env
if [ "$edit_env" = "y" ] || [ "$edit_env" = "Y" ]; then
${EDITOR:-nano} .env
fi
else
echo "✅ .env file already exists"
fi
# Check if OAuth credentials are set
source .env
if [ ! -f .env ]; then
echo "❌ .env file not found. Please run this script from the development directory."
exit 1
fi
# Check database password security
if [ "$POSTGRES_PASSWORD" = "CHANGE_THIS_PASSWORD" ] || [ -z "$POSTGRES_PASSWORD" ]; then
echo "🔒 SECURITY WARNING: Database password not properly set!"
echo " Please update POSTGRES_PASSWORD in .env file with a secure password"
echo " The default password 'CHANGE_THIS_PASSWORD' should not be used"
echo ""
else
echo "✅ Database password configured"
fi
if [ "$OAUTH_CONSUMER_KEY" = "your-oauth-consumer-key" ] || [ "$OAUTH_CONSUMER_SECRET" = "your-oauth-consumer-secret" ] || [ -z "$OAUTH_CONSUMER_KEY" ] || [ -z "$OAUTH_CONSUMER_SECRET" ]; then
echo "⚠️ WARNING: OAuth credentials not properly set!"
echo " Please update OAUTH_CONSUMER_KEY and OAUTH_CONSUMER_SECRET in .env file"
echo " You can get these from your OBP API instance"
echo ""
else
echo "✅ OAuth credentials configured"
fi
# Build and start services
echo "🔨 Building Docker images..."
docker-compose build
echo "🚀 Starting services..."
docker-compose up -d
# Wait for services to be ready
echo "⏳ Waiting for services to be ready..."
sleep 10
# Check if services are running
if docker-compose ps | grep -q "Up"; then
echo "✅ Services are running!"
# Display service information
echo ""
echo "📊 Service Status:"
docker-compose ps
echo ""
echo "🎉 Setup completed successfully!"
echo ""
echo "📝 Next steps:"
echo " 1. Open http://localhost:8000 in your browser"
echo " 2. Login with admin/admin123 for admin access"
echo " 3. Check logs: docker-compose logs -f web"
echo " 4. Stop services: docker-compose down"
echo ""
echo "🔧 Development commands (run from development/ directory):"
echo " - View logs: docker-compose logs api-manager-web"
echo " - Access shell: docker-compose exec api-manager-web bash"
echo " - Django shell: docker-compose exec api-manager-web bash -c 'cd apimanager && python manage.py shell'"
echo " - Database shell: docker-compose exec api-manager-db psql -U \${POSTGRES_USER:-apimanager} -d \${POSTGRES_DB:-apimanager}"
echo ""
# Test if the application is responding
if curl -s -I http://localhost:8000 | grep -q "HTTP/1.1"; then
echo "✅ Application is responding at http://localhost:8000"
else
echo "⚠️ Application might not be fully ready yet. Wait a moment and try accessing http://localhost:8000"
fi
else
echo "❌ Some services failed to start. Check logs with: docker-compose logs"
exit 1
fi

View File

@ -0,0 +1,38 @@
version: "3.8"
services:
api-manager-web:
container_name: api-manager-web
build:
context: ..
dockerfile: development/Dockerfile.dev
network_mode: host
volumes:
- ..:/app
- ../logs:/app/logs
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-apimanager}:${POSTGRES_PASSWORD:-CHANGE_THIS_PASSWORD}@127.0.0.1:5434/${POSTGRES_DB:-apimanager}
- API_HOST=http://127.0.0.1:8080
- CALLBACK_BASE_URL=http://127.0.0.1:8000
- ALLOW_DIRECT_LOGIN=True
env_file:
- .env
depends_on:
- api-manager-db
restart: unless-stopped
api-manager-db:
container_name: api-manager-db
image: postgres:13
environment:
- POSTGRES_DB=${POSTGRES_DB:-apimanager}
- POSTGRES_USER=${POSTGRES_USER:-apimanager}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-CHANGE_THIS_PASSWORD}
volumes:
- api_manager_postgres_data:/var/lib/postgresql/data
ports:
- "5434:5432"
restart: unless-stopped
volumes:
api_manager_postgres_data:

View File

@ -0,0 +1,45 @@
#!/bin/bash
# Development entrypoint script for API Manager
# This script sets up the development environment and starts the Django development server
set -e
# Wait for database to be ready
echo "Waiting for database to be ready..."
DB_USER=${POSTGRES_USER:-apimanager}
while ! pg_isready -h 127.0.0.1 -p 5434 -U "$DB_USER" -q; do
echo "Database is unavailable - sleeping"
sleep 2
done
echo "Database is ready!"
# Change to the Django project directory
cd /app/apimanager
# Run database migrations
echo "Running database migrations..."
python manage.py migrate --noinput
# Collect static files
echo "Collecting static files..."
python manage.py collectstatic --noinput --clear
# Create superuser if it doesn't exist (for development convenience)
echo "Setting up development superuser..."
python manage.py shell -c "
import os
from django.contrib.auth.models import User
username = os.getenv('DJANGO_SUPERUSER_USERNAME', 'admin')
email = os.getenv('DJANGO_SUPERUSER_EMAIL', 'admin@example.com')
password = os.getenv('DJANGO_SUPERUSER_PASSWORD', 'admin123')
if not User.objects.filter(username=username).exists():
User.objects.create_superuser(username, email, password)
print(f'Superuser {username} created successfully')
else:
print(f'Superuser {username} already exists')
" || echo "Superuser setup skipped (error occurred)"
# Start the development server
echo "Starting Django development server..."
exec python manage.py runserver 0.0.0.0:8000

View File

@ -0,0 +1,130 @@
import os
# Development settings for Docker environment
# Debug mode for development - force override
DEBUG = True
if os.getenv('DEBUG'):
DEBUG = os.getenv('DEBUG').lower() in ('true', '1', 'yes', 'on')
# Secret key from environment or default for development
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
# API Configuration
if os.getenv('API_HOST'):
API_HOST = os.getenv('API_HOST')
else:
API_HOST = 'http://127.0.0.1:8080'
if os.getenv('API_PORTAL'):
API_PORTAL = os.getenv('API_PORTAL')
else:
API_PORTAL = API_HOST
# OAuth Configuration
if os.getenv('OAUTH_CONSUMER_KEY'):
OAUTH_CONSUMER_KEY = os.getenv('OAUTH_CONSUMER_KEY')
else:
OAUTH_CONSUMER_KEY = "your-oauth-consumer-key"
if os.getenv('OAUTH_CONSUMER_SECRET'):
OAUTH_CONSUMER_SECRET = os.getenv('OAUTH_CONSUMER_SECRET')
else:
OAUTH_CONSUMER_SECRET = "your-oauth-consumer-secret"
# Callback URL for OAuth - use localhost for browser accessibility
if os.getenv('CALLBACK_BASE_URL'):
CALLBACK_BASE_URL = os.getenv('CALLBACK_BASE_URL')
else:
CALLBACK_BASE_URL = "http://localhost:8000"
# Allowed hosts
if os.getenv('ALLOWED_HOSTS'):
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',')
else:
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0', 'web']
# CSRF and CORS settings for development
if os.getenv('CSRF_TRUSTED_ORIGINS'):
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
else:
CSRF_TRUSTED_ORIGINS = ['http://localhost:8000', 'http://127.0.0.1:8000']
if os.getenv('CORS_ORIGIN_WHITELIST'):
CORS_ORIGIN_WHITELIST = os.getenv('CORS_ORIGIN_WHITELIST').split(',')
# Database configuration
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Check if DATABASE_URL is provided (for PostgreSQL in Docker)
if os.getenv('DATABASE_URL'):
import dj_database_url
DATABASES = {
'default': dj_database_url.parse(os.getenv('DATABASE_URL'))
}
else:
# Fallback to SQLite for development
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Static files configuration for Docker
STATIC_ROOT = '/static-collected'
# Ensure DEBUG is properly set for static file serving
DEBUG = True
# Security settings for development (less restrictive)
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
# Disable SSL redirect for development
SECURE_SSL_REDIRECT = False
# Session configuration for OAuth flow reliability
SESSION_COOKIE_AGE = 3600 # 1 hour instead of 5 minutes
SESSION_ENGINE = "django.contrib.sessions.backends.db" # Use database sessions for reliability
# Logging configuration for development
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
},
'base': {
'handlers': ['console'],
'level': 'DEBUG',
},
'obp': {
'handlers': ['console'],
'level': 'DEBUG',
},
'consumers': {
'handlers': ['console'],
'level': 'DEBUG',
},
'users': {
'handlers': ['console'],
'level': 'DEBUG',
},
'customers': {
'handlers': ['console'],
'level': 'DEBUG',
},
'metrics': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}