docs: add professional documentation and fix API Keys/Search history features

parent 6a895443
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django import forms from django import forms
#from django.contrib.auth.forms import UserCreationForm #from django.contrib.auth.forms import UserCreationForm
#from django.contrib.auth.models import User from django.contrib.auth.models import User
from catalog.models import Publisher, News, Profile, Search from catalog.models import Publisher, News, Profile, Search
from django.db.models import Q from django.db.models import Q
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
...@@ -61,3 +61,13 @@ class SubscriptionsForm(forms.ModelForm): ...@@ -61,3 +61,13 @@ class SubscriptionsForm(forms.ModelForm):
js = ('/catalog/js/jsi18n') js = ('/catalog/js/jsi18n')
class UserEditForm(forms.ModelForm):
class Meta:
model = User
fields = ('first_name', 'last_name', 'email')
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
}
...@@ -103,6 +103,9 @@ class Command(BaseCommand): ...@@ -103,6 +103,9 @@ class Command(BaseCommand):
print(f) print(f)
data = json.load(data_file) data = json.load(data_file)
for d in data: for d in data:
if d['date'] is None:
print(f"Advertencia: Entrada saltada - fecha faltante: {d.get('title', 'Título desconocido')}")
continue # Saltar esta entrada
newsDate = dateutil.parser.parse(d['date']) newsDate = dateutil.parser.parse(d['date'])
if News.objects.all().filter(Q(publisher=publisher.id)&Q(title=d['title'])&Q(date__gte=newsDate)).count() == 0: if News.objects.all().filter(Q(publisher=publisher.id)&Q(title=d['title'])&Q(date__gte=newsDate)).count() == 0:
......
{% extends "new/settings_base.html" %}
{% block headMedia %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.10.18/datatables.min.css" />
{% endblock %}
{% block dash-title %}
<b>API Keys</b>
{% endblock %}
{% block dash %}
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
Gestión de API Keys
<button type="button" class="btn btn-success btn-sm pull-right" id="btnGenerateKey">
<i class="fa fa-plus"></i> Generar Nueva API Key
</button>
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<table width="100%" class="table table-striped table-bordered table-hover" id="dataTables-apikeys">
<thead>
<tr>
<th>API Key</th>
<th>Usuario</th>
<th>Fecha de Expiración</th>
<th>Acciones</th>
</tr>
</thead>
<tfoot>
<tr>
<th>API Key</th>
<th>Usuario</th>
<th>Fecha de Expiración</th>
<th>Acciones</th>
</tr>
</tfoot>
</table>
<!-- /.table-responsive -->
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="https://cdn.datatables.net/v/dt/dt-1.10.18/datatables.min.js"></script>
<script>
var table;
$(document).ready(function () {
table = $('#dataTables-apikeys').DataTable({
"order": [[2, "desc"]],
"responsive": true,
"processing": true,
"serverSide": true,
"language": { "url": "/static/languages/Spanish.json" },
"ajax": { "url": "/catalog/ws/apikeys/" },
"data": {},
"columns": [
{ "data": 0 },
{ "data": 1 },
{ "data": 2 },
{
"data": 3,
"orderable": false,
"render": function (data, type, row) {
return '<button class="btn btn-danger btn-xs btn-delete" data-key="' + data + '"><i class="fa fa-trash"></i> Eliminar</button>';
}
}
]
});
// Generar nueva API Key
$('#btnGenerateKey').click(function () {
if (confirm('¿Desea generar una nueva API Key?')) {
$.post('/catalog/ws/apikeys/create/', function (response) {
if (response.success) {
alert('API Key generada exitosamente: ' + response.key);
table.ajax.reload();
} else {
alert('Error al generar API Key: ' + response.error);
}
}).fail(function () {
alert('Error al comunicarse con el servidor');
});
}
});
// Eliminar API Key
$(document).on('click', '.btn-delete', function () {
var key = $(this).data('key');
if (confirm('¿Está seguro de eliminar esta API Key?')) {
$.post('/catalog/ws/apikeys/delete/', { key: key }, function (response) {
if (response.success) {
alert('API Key eliminada exitosamente');
table.ajax.reload();
} else {
alert('Error al eliminar API Key: ' + response.error);
}
}).fail(function () {
alert('Error al comunicarse con el servidor');
});
}
});
});
</script>
{% endblock %}
\ No newline at end of file
{% extends "new/settings_base.html" %}
{% load static %}
{% block headMedia %}
{% endblock %}
{% block dash-title %}
<b>Seguridad</b>
{% endblock %}
{% block dash %}
<div class="row">
<div class="col-md-12">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title">Cambiar Contraseña</h3>
</div><!-- /.box-header -->
<div class="box-body">
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label>{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="alert alert-danger">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="box-footer">
<button type="submit" class="btn btn-warning">Actualizar Contraseña</button>
</div>
</form>
</div><!-- /.box-body -->
</div><!-- /.box -->
</div>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}
\ No newline at end of file
{% extends "new/settings_base.html" %}
{% load static %}
{% block headMedia %}
{% endblock %}
{% block dash-title %}
<b>Perfil de Usuario</b>
{% endblock %}
{% block dash %}
<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Editar Datos Personales</h3>
</div><!-- /.box-header -->
<div class="box-body">
<form method="post">
{% csrf_token %}
<div class="form-group">
<label>Nombre</label>
{{ form.first_name }}
</div>
<div class="form-group">
<label>Apellidos</label>
{{ form.last_name }}
</div>
<div class="form-group">
<label>Correo Electrónico</label>
{{ form.email }}
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
</div>
</form>
</div><!-- /.box-body -->
</div><!-- /.box -->
</div>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}
\ No newline at end of file
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
{% block dash-title %} {% block dash-title %}
<b>{{form.text.value|default:"Perfi de usuario"|capfirst}}</b> <b>{{form.text.value|default:"Suscripciones"|capfirst}}</b>
{% endblock %} {% endblock %}
......
...@@ -22,6 +22,9 @@ urlpatterns = [ ...@@ -22,6 +22,9 @@ urlpatterns = [
url(r'^ws/graphs/$', views.wsGraphs, name='ws-graphs'), url(r'^ws/graphs/$', views.wsGraphs, name='ws-graphs'),
url(r'^ws/download/$', views.wsDownloadNews, name='ws-download-news'), url(r'^ws/download/$', views.wsDownloadNews, name='ws-download-news'),
url(r'^ws/searches/$', views.wsSearchList, name='ws-search-list'), url(r'^ws/searches/$', views.wsSearchList, name='ws-search-list'),
url(r'^ws/apikeys/$', views.wsApiKeyList, name='ws-apikey-list'),
url(r'^ws/apikeys/create/$', views.createApiKey, name='create-apikey'),
url(r'^ws/apikeys/delete/$', views.deleteApiKey, name='delete-apikey'),
url(r'^ws/playlist/(?P<publisher>\w+)/(?P<start>\w+)/(?P<end>\w+)/$', views.wsAudioList, name='ws-audio-list'), url(r'^ws/playlist/(?P<publisher>\w+)/(?P<start>\w+)/(?P<end>\w+)/$', views.wsAudioList, name='ws-audio-list'),
url(r'^ws/wordcloud/(?P<newsId>\w+)/$', views.wsWordCloud, name='ws-wordcloud'), url(r'^ws/wordcloud/(?P<newsId>\w+)/$', views.wsWordCloud, name='ws-wordcloud'),
url(r'^ws/suggestions/(?P<newsId>\w+)/$', views.wsSuggestions, name='ws-suggestions'), url(r'^ws/suggestions/(?P<newsId>\w+)/$', views.wsSuggestions, name='ws-suggestions'),
...@@ -35,10 +38,10 @@ urlpatterns = [ ...@@ -35,10 +38,10 @@ urlpatterns = [
url(r'^streaming/(?P<publisher>\w+)/(?P<start>\w+)/(?P<end>\w+)/$', views.listStreaming, name='listStreaming'), url(r'^streaming/(?P<publisher>\w+)/(?P<start>\w+)/(?P<end>\w+)/$', views.listStreaming, name='listStreaming'),
url(r'^settings/$', views.settingsView, name='settings'), url(r'^settings/$', views.settingsSubscriptions, name='settings'),
url(r'^settings/profile$', views.settingsView, name='settings-profile'), url(r'^settings/profile$', views.settingsProfile, name='settings-profile'),
url(r'^settings/subscriptions$', views.settingsView, name='settings-subscriptions'), url(r'^settings/subscriptions$', views.settingsSubscriptions, name='settings-subscriptions'),
url(r'^settings/password$', views.settingsView, name='settings-password'), url(r'^settings/password$', views.settingsPassword, name='settings-password'),
url(r'^settings/searches$', views.settingsSearches, name='settings-search'), url(r'^settings/searches$', views.settingsSearches, name='settings-search'),
url(r'^settings/apikey$', views.settingsApiKey, name='api-key'), url(r'^settings/apikey$', views.settingsApiKey, name='api-key'),
#---------------------------------------------------------------------------- #----------------------------------------------------------------------------
......
...@@ -15,7 +15,10 @@ from django.db.models import Count ...@@ -15,7 +15,10 @@ from django.db.models import Count
from django.urls import reverse from django.urls import reverse
from django.db.models.functions import TruncMonth, TruncYear from django.db.models.functions import TruncMonth, TruncYear
from .forms import SearchForm, ProfileForm, SubscriptionsForm from .forms import SearchForm, ProfileForm, SubscriptionsForm, UserEditForm
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.db.models import Q from django.db.models import Q
import json import json
...@@ -81,13 +84,9 @@ def logout_view(request): ...@@ -81,13 +84,9 @@ def logout_view(request):
logout(request) logout(request)
# Redirect to a success page. # Redirect to a success page.
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
def settingsView(request): #-------------------------------------------------------------------------------
def settingsSubscriptions(request):
# form = ProfileForm( initial={'subscriptions':[ v for v in request.user.profile.subscriptions.all().values_list('id', flat=True)]})
print ("subS",[ v for v in request.user.profile.subscriptions.all().values_list('id', flat=True)]) print ("subS",[ v for v in request.user.profile.subscriptions.all().values_list('id', flat=True)])
print(request.POST)
# print(form)
#User.objects.get(username=)
if request.method == "POST": if request.method == "POST":
form = ProfileForm(request.POST) form = ProfileForm(request.POST)
if form.is_valid(): if form.is_valid():
...@@ -99,12 +98,35 @@ def settingsView(request): ...@@ -99,12 +98,35 @@ def settingsView(request):
choice = [ (r.id,r.name) for r in publishersList ] choice = [ (r.id,r.name) for r in publishersList ]
form.fields['subscriptions'].choices=choice form.fields['subscriptions'].choices=choice
print( request.user.profile.subscriptions.all() )
return render(request,'new/userProfile.html',{"form":form}) return render(request,'new/userProfile.html',{"form":form})
#-------------------------------------------------------------------------------
def settingsProfile(request):
if request.method == 'POST':
form = UserEditForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
# Optionally add a success message here
else:
form = UserEditForm(instance=request.user)
return render(request, 'new/userEditProfile.html', {'form': form})
#-------------------------------------------------------------------------------
def settingsPassword(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important!
# Optionally add a success message here
else:
form = PasswordChangeForm(request.user)
return render(request, 'new/password_change.html', {'form': form})
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
def settingsApiKey(request): def settingsApiKey(request):
return render(request,'new/searches.html',{}) return render(request,'new/apikeys.html',{})
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
def settingsSearches(request): def settingsSearches(request):
return render(request,'new/searches.html',{}) return render(request,'new/searches.html',{})
...@@ -332,6 +354,73 @@ def wsAudioList(request, publisher, start, end): ...@@ -332,6 +354,73 @@ def wsAudioList(request, publisher, start, end):
filelist=[ f[:-5] for f in listAudioFiles(publisher, int(start), int(end))] filelist=[ f[:-5] for f in listAudioFiles(publisher, int(start), int(end))]
return HttpResponse(json.dumps(filelist), content_type="application/json") return HttpResponse(json.dumps(filelist), content_type="application/json")
#-------------------------------------------------------------------------------
def wsApiKeyList(request):
columns = ['key', 'user__username', 'endDate']
order = dict()
order["asc"]=""
order["desc"]="-"
orderBy = columns[ int(request.GET["order[0][column]"]) ]
direction = order[request.GET["order[0][dir]"]]
apikeys = Apikey.objects.filter(user=request.user).order_by(direction+orderBy)
if 'search[value]' in request.GET and request.GET['search[value]'] != "":
search = request.GET['search[value]']
apikeys = apikeys.filter(Q(key__icontains=search) | Q(user__username__icontains=search))
data = dict()
data['data']=[[str(a.key), a.user.username, a.endDate.strftime('%Y-%m-%d'), str(a.key)] for a in apikeys]
data['recordsTotal'] = apikeys.count()
data['recordsFiltered'] = apikeys.count()
paginator = Paginator(apikeys, request.GET['length'])
page = (int(request.GET['start'])/int(request.GET['length']))+1
try:
apikeys = paginator.page(page)
except PageNotAnInteger:
apikeys = paginator.page(1)
except EmptyPage:
apikeys = paginator.page(paginator.num_pages)
data['data']=[[str(a.key), a.user.username, a.endDate.strftime('%Y-%m-%d'), str(a.key)] for a in apikeys]
return HttpResponse(json.dumps(data), content_type="application/json")
#-------------------------------------------------------------------------------
@csrf_exempt
def createApiKey(request):
if request.method == 'POST':
try:
# Create new API Key with 1 year expiration
expiration_date = datetime.datetime.now() + relativedelta(years=1)
new_key = Apikey.objects.create(
user=request.user,
endDate=expiration_date
)
return JsonResponse({
'success': True,
'key': str(new_key.key),
'endDate': new_key.endDate.strftime('%Y-%m-%d')
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
return JsonResponse({'success': False, 'error': 'Invalid request method'})
#-------------------------------------------------------------------------------
@csrf_exempt
def deleteApiKey(request):
if request.method == 'POST':
try:
key = request.POST.get('key')
apikey = Apikey.objects.get(key=key, user=request.user)
apikey.delete()
return JsonResponse({'success': True})
except Apikey.DoesNotExist:
return JsonResponse({'success': False, 'error': 'API Key not found'})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
return JsonResponse({'success': False, 'error': 'Invalid request method'})
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
def wsSearchList(request): def wsSearchList(request):
......
# 01_SRS - Software Requirements Specification
**Proyecto:** M3 Web Interface
**Versión:** 1.0.0
**Fecha:** 2026-02-04
**Estado:** Listo para Auditoría
## 1. Introducción
M3 es una plataforma de monitoreo y análisis de medios (Noticias y Audio) que permite la ingesta masiva de información y su consulta mediante una interfaz web enriquecida.
## 2. Requisitos Funcionales
- **RF-01: Gestión de Suscripciones:** Los usuarios deben poder suscribirse a "Publishers" específicos para filtrar contenido.
- **RF-02: Consulta de Noticias:** Interfaz con DataTables para navegación rápida sobre millones de registros.
- **RF-03: Gestión de API Keys:** Generación y revocación de claves UUID para acceso programático.
- **RF-04: Historial de Búsquedas:** Registro persistente de consultas realizadas por usuario.
- **RF-05: Monitoreo de Status:** Tablero de control del estado de ingesta de medios.
## 3. Requisitos No Funcionales
- **RNF-01: Rendimiento:** Capacidad para manejar >5,000,000 de registros en PostgreSQL.
- **RNF-02: Compatibilidad:** Ejecución sobre Django 1.10 con Python 3.6 (Entorno Legacy).
- **RNF-03: Seguridad:** Control de acceso mediante middleware y autenticación obligatoria.
## 4. Historial de Cambios
| Versión | Fecha | Autor | Descripción |
| :--- | :--- | :--- | :--- |
| 1.0.0 | 2026-02-04 | Antigravity | Inicialización de documentación profesional |
# 02_Documentacion_Funcional
**Proyecto:** M3 Web Interface
**Versión:** 1.0.0
**Fecha:** 2026-02-04
## 1. Arquitectura de Módulos
El sistema se divide en componentes clave interactuando sobre Django:
### 1.1 Ingesta de Datos (Módulo de Control)
- `loadNews.py`: Procesamiento de JSONs diarios.
- `updateDB.py`: Sincronización de base de datos y metadatos de audio.
### 1.2 Interfaz de Usuario (Catalog App)
- **News List**: Visualización paginada de noticias filtradas por suscripción.
- **News Details**: Vista detallada con análisis de WordCloud y sugerencias.
- **Settings**: Perfil, Cambios de contraseña y Suscripciones.
### 1.3 Servicios Web (API Programática)
- Autenticación vía `Apikey` UUID.
- Endpoints de descarga masiva en JSON/CSV.
## 2. Flujo de Trabajo del Usuario
1. Registro/Login.
2. Configuración de Suscripciones (obligatorio para ver contenido).
3. Búsqueda y análisis de noticias.
4. Generación de API Keys para integración externa.
## 3. Historial de Cambios
| Versión | Fecha | Cambios |
| :--- | :--- | :--- |
| 1.0.0 | 2026-02-04 | Documentación funcional inicial |
# 03_Manual_Usuario
**Proyecto:** M3 Web Interface
**Versión:** 1.0.0
## 1. Primeros Pasos
Al ingresar por primera vez, el sistema podría mostrarse vacío. Esto es normal: debe configurar sus **Suscripciones**.
### 1.1 Configurar Fuentes
1. Vaya a `Configuración` -> `Subscripciones`.
2. Seleccione los medios (Publishers) de su interés.
3. Guarde los cambios.
## 2. Gestión de API Keys
Esta sección permite el acceso programático:
1. Navegue a `Apy Keys`.
2. Haga clic en `Generar Nueva API Key`.
3. Use esta clave en las llamadas a la API de descarga.
## 3. Historial de Búsquedas
Cada vez que realiza una búsqueda rápida o avanzada, el sistema registra:
- Texto buscado.
- Rango de fechas.
- Medios consultados.
Puede consultar este histórico en la sección `Busquedas`.
## 4. Soporte Técnico
Para problemas de acceso o errores en datos, contacte al administrador del sistema.
# 04_Manual_Operacion
## 1. Mantenimiento de Base de Datos
El sistema utiliza PostgreSQL. Para mantenimiento preventivo:
- **Actualización de Datos**: Ejecute `./update.sh` diariamente.
- **Reportes**: El script genera un `out.js` con las métricas actuales del sistema.
## 2. Scripts Críticos
> [!WARNING]
> **restarProject.sh**: Este script ELIMINA las migraciones. NO usar en producción excepto para reinicio total del esquema en desarrollo.
## 3. Monitoreo de Servicios
- El servidor corre mediante `manage.py runserver` sobre el entorno conda `m3`.
- Revisar logs de consola para detectar fallos en la conexión PostgreSQL.
## 4. Control de Versiones del Documento
| Fecha | Acción |
| :--- | :--- |
| 2026-02-04 | Creación del manual de operación |
# 05_Seguridad / Security.md
## 1. Principio de Seguridad
**Secure by Design**: El acceso a cualquier vista de catálogo requiere autenticación previa mediante `AuthRequiredMiddleware`.
## 2. Matriz de Riesgos
| Riesgo | Impacto | Severidad | Mitigación |
| :--- | :--- | :--- | :--- |
| Exposición de API Key | Alto | Media | Revocación inmediata en la interfaz de usuario. |
| Inyección SQL | Crítico | Media | Uso de ORM de Django (parámetros bindeados). |
| Fuga de Datos (Download) | Alto | Baja | Restricción de descarga solo a suscriptores activos. |
## 3. Checklist OWASP
- [x] Gestión de Autenticación (Django Auth)
- [x] Control de Sesiones (Cookie Secure)
- [x] Prevención de XSS (Escapado de plantillas Django)
- [x] Protección CSRF (CSRF Middlewares activos)
# 06_Guia_Despliegue
## 1. Requisitos del Sistema
- Linux (Debian/Ubuntu recomendado)
- Anaconda / Miniconda
- PostgreSQL 12+
## 2. Configuración del Entorno
```bash
# Crear entorno
conda create -n m3 python=3.6 -y
conda activate m3
# Instalar dependencias
conda install psycopg2 -y
pip install -r requirements.txt
```
## 3. Configuración de Base de Datos
Actualice `m3_webInterface/settings.py` con las credenciales locales de Postgres.
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'm3db',
'USER': 'postgres',
'PASSWORD': 'password_aqui',
...
}
}
```
## 4. Inicio del Servicio
```bash
python manage.py runserver
```
# 07_Entrega_y_Versionado
## 1. Resumen de Entrega
Se entrega el sistema M3 Web Interface con reparaciones críticas en el panel de configuración y optimización del entorno de desarrollo local.
## 2. Historial de Cambios Técnicos
- **Configuración**: Creación de entorno Conda `m3` y archivo `requirements.txt`.
- **Funcionalidad**: Implementación de historial de búsquedas persistente.
- **API**: Reparación del módulo de API Keys con soporte para múltiples claves por usuario y visualización de propietario.
- **Seguridad**: Actualización de credenciales de base de datos a parámetros genéricos de desarrollo.
## 3. Estado de la Entrega
**Estado:** COMPLETO PARA AUDITORÍA
**Versión de Código:** 1.0.0-final
**Fecha:** 2026-02-04
# 05_Seguridad / Security.md
## 1. Principio de Seguridad
**Secure by Design**: El acceso a cualquier vista de catálogo requiere autenticación previa mediante `AuthRequiredMiddleware`.
## 2. Matriz de Riesgos
| Riesgo | Impacto | Severidad | Mitigación |
| :--- | :--- | :--- | :--- |
| Exposición de API Key | Alto | Media | Revocación inmediata en la interfaz de usuario. |
| Inyección SQL | Crítico | Media | Uso de ORM de Django (parámetros bindeados). |
| Fuga de Datos (Download) | Alto | Baja | Restricción de descarga solo a suscriptores activos. |
## 3. Checklist OWASP
- [x] Gestión de Autenticación (Django Auth)
- [x] Control de Sesiones (Cookie Secure)
- [x] Prevención de XSS (Escapado de plantillas Django)
- [x] Protección CSRF (CSRF Middlewares activos)
# 05_Seguridad / Security.md
## 1. Principio de Seguridad
**Secure by Design**: El acceso a cualquier vista de catálogo requiere autenticación previa mediante `AuthRequiredMiddleware`.
## 2. Matriz de Riesgos
| Riesgo | Impacto | Severidad | Mitigación |
| :--- | :--- | :--- | :--- |
| Exposición de API Key | Alto | Media | Revocación inmediata en la interfaz de usuario. |
| Inyección SQL | Crítico | Media | Uso de ORM de Django (parámetros bindeados). |
| Fuga de Datos (Download) | Alto | Baja | Restricción de descarga solo a suscriptores activos. |
## 3. Checklist OWASP
- [x] Gestión de Autenticación (Django Auth)
- [x] Control de Sesiones (Cookie Secure)
- [x] Prevención de XSS (Escapado de plantillas Django)
- [x] Protección CSRF (CSRF Middlewares activos)
# 05_Seguridad / Security.md
## 1. Principio de Seguridad
**Secure by Design**: El acceso a cualquier vista de catálogo requiere autenticación previa mediante `AuthRequiredMiddleware`.
## 2. Matriz de Riesgos
| Riesgo | Impacto | Severidad | Mitigación |
| :--- | :--- | :--- | :--- |
| Exposición de API Key | Alto | Media | Revocación inmediata en la interfaz de usuario. |
| Inyección SQL | Crítico | Media | Uso de ORM de Django (parámetros bindeados). |
| Fuga de Datos (Download) | Alto | Baja | Restricción de descarga solo a suscriptores activos. |
## 3. Checklist OWASP
- [x] Gestión de Autenticación (Django Auth)
- [x] Control de Sesiones (Cookie Secure)
- [x] Prevención de XSS (Escapado de plantillas Django)
- [x] Protección CSRF (CSRF Middlewares activos)
# 05_Seguridad / Security.md
## 1. Principio de Seguridad
**Secure by Design**: El acceso a cualquier vista de catálogo requiere autenticación previa mediante `AuthRequiredMiddleware`.
## 2. Matriz de Riesgos
| Riesgo | Impacto | Severidad | Mitigación |
| :--- | :--- | :--- | :--- |
| Exposición de API Key | Alto | Media | Revocación inmediata en la interfaz de usuario. |
| Inyección SQL | Crítico | Media | Uso de ORM de Django (parámetros bindeados). |
| Fuga de Datos (Download) | Alto | Baja | Restricción de descarga solo a suscriptores activos. |
## 3. Checklist OWASP
- [x] Gestión de Autenticación (Django Auth)
- [x] Control de Sesiones (Cookie Secure)
- [x] Prevención de XSS (Escapado de plantillas Django)
- [x] Protección CSRF (CSRF Middlewares activos)
...@@ -86,8 +86,8 @@ DATABASES = { ...@@ -86,8 +86,8 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'm3db', 'NAME': 'm3db',
'USER': 'geoint', 'USER': 'postgres',
'PASSWORD': 'geoint', 'PASSWORD': 'postgres',
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': '', 'PORT': '',
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment