Bakend en flask
This commit is contained in:
parent
4f6966a400
commit
a5bf51eb47
16
api/Dockerfile
Normal file
16
api/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Instalar dependencias del sistema necesarias para PostgreSQL y manejo de imágenes
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["python", "run.py"]
|
||||||
139
api/app/commands.py
Normal file
139
api/app/commands.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import click
|
||||||
|
from flask.cli import with_appcontext
|
||||||
|
from app.database import db
|
||||||
|
from app.models import Role, User
|
||||||
|
from app.models.biometria import (
|
||||||
|
Persona, OrigenHuella, TiposF, Subclasificacion,
|
||||||
|
Huella, PuntoCaracteristico, HuellaPuntoCaracteristico, FichaClasificacion
|
||||||
|
)
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
@click.command(name="seed") # Definimos explícitamente el nombre del comando CLI
|
||||||
|
@with_appcontext
|
||||||
|
def seed_db():
|
||||||
|
"""Siembra de roles, usuarios administrativos y datos iniciales del S.I.P"""
|
||||||
|
click.echo("Iniciando el sembrado de datos en PostgreSQL...")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# [ROLES Y USUARIOS]
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
admin_role = Role.query.filter_by(name='admin').first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(name='admin', permissions=['read:users', 'write:users', 'manage:biometria'])
|
||||||
|
db.session.add(admin_role)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
investigador_role = Role.query.filter_by(name='investigador').first()
|
||||||
|
if not investigador_role:
|
||||||
|
investigador_role = Role(name='investigador', permissions=['manage:biometria'])
|
||||||
|
db.session.add(investigador_role)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
admin_user = User.query.filter_by(username='admin_root').first()
|
||||||
|
if not admin_user:
|
||||||
|
admin_user = User(username='admin_root', email='admin@uader.edu.ar', role_id=admin_role.id)
|
||||||
|
admin_user.set_password('admin1234')
|
||||||
|
db.session.add(admin_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
investigador_user = User.query.filter_by(username='investigador_demo').first()
|
||||||
|
if not investigador_user:
|
||||||
|
investigador_user = User(username='investigador_demo', email='investigador@uader.edu.ar', role_id=investigador_role.id)
|
||||||
|
investigador_user.set_password('investigador1234')
|
||||||
|
db.session.add(investigador_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 1. TABLA MAESTRA: PERSONAS
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if Persona.query.count() == 0:
|
||||||
|
personas_data = [
|
||||||
|
Persona(cod_id=1, cod_nombre='Juan Pérez', fecha_nacimiento=datetime.strptime('1990-05-20', '%Y-%m-%d').date(), sexo='M'),
|
||||||
|
Persona(cod_id=2, cod_nombre='Ana Gómez', fecha_nacimiento=datetime.strptime('1985-12-01', '%Y-%m-%d').date(), sexo='F'),
|
||||||
|
Persona(cod_id=3, cod_nombre='Carlos López', fecha_nacimiento=datetime.strptime('2000-07-15', '%Y-%m-%d').date(), sexo='M')
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(personas_data)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 2. TABLA MAESTRA: ORIGEN DE HUELLAS
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if OrigenHuella.query.count() == 0:
|
||||||
|
origenes_data = [
|
||||||
|
OrigenHuella(id=1, descripcion='Índice derecho', lado='derecho'),
|
||||||
|
OrigenHuella(id=2, descripcion='Índice izquierdo', lado='izquierdo'),
|
||||||
|
OrigenHuella(id=3, descripcion='Pulgar derecho', lado='derecho')
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(origenes_data)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 3. TABLA MAESTRA: TIPOS F
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if TiposF.query.count() == 0:
|
||||||
|
tipos_data = [
|
||||||
|
TiposF(id=1, nombre='Arco', descripcion='Huella en forma de arco', foto=None),
|
||||||
|
TiposF(id=2, nombre='Presilla', descripcion='Huella en forma de presilla', foto=None),
|
||||||
|
TiposF(id=3, nombre='Verticilo', descripcion='Huella en forma de espiral', foto=None)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(tipos_data)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 4. SUBTABLA DEPENDIENTE: SUBCLASIFICACIÓN
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if Subclasificacion.query.count() == 0:
|
||||||
|
sub_data = [
|
||||||
|
Subclasificacion(id=1, nombre='Arco simple', tipo_f_id=1),
|
||||||
|
Subclasificacion(id=2, nombre='Presilla derecha', tipo_f_id=2),
|
||||||
|
Subclasificacion(id=3, nombre='Verticilo central', tipo_f_id=3)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(sub_data)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 5. TABLA MAESTRA: PUNTOS CARACTERÍSTICOS
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if PuntoCaracteristico.query.count() == 0:
|
||||||
|
puntos_data = [
|
||||||
|
PuntoCaracteristico(id=1, nombre='Bifurcación', foto=None),
|
||||||
|
PuntoCaracteristico(id=2, nombre='Final de cresta', foto=None),
|
||||||
|
PuntoCaracteristico(id=3, nombre='Punto de isla', foto=None)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(puntos_data)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 6. NÚCLEO BIOMÉTRICO: HUELLAS
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if Huella.query.count() == 0:
|
||||||
|
huellas_data = [
|
||||||
|
Huella(id=1, descripcion='Huella digital clara', origen_hd='Escáner', foto=None, persona_cod_id=1, origen_huella_id=1),
|
||||||
|
Huella(id=2, descripcion='Huella con ligera distorsión', origen_hd='Tinta', foto=None, persona_cod_id=2, origen_huella_id=2),
|
||||||
|
Huella(id=3, descripcion='Huella poco definida', origen_hd='Escáner', foto=None, persona_cod_id=3, origen_huella_id=3)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(huellas_data)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 7. TABLA PIVOTE: HUELLA / PUNTOS
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if HuellaPuntoCaracteristico.query.count() == 0:
|
||||||
|
coordenadas_data = [
|
||||||
|
HuellaPuntoCaracteristico(huella_id=1, punto_id=1, x=12.345678, y=45.678912, angulo=30.00),
|
||||||
|
HuellaPuntoCaracteristico(huella_id=1, punto_id=2, x=15.111111, y=50.222222, angulo=45.00),
|
||||||
|
HuellaPuntoCaracteristico(huella_id=2, punto_id=3, x=20.333333, y=60.444444, angulo=60.00)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(coordenadas_data)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 8. FICHA DE CLASIFICACIÓN
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if FichaClasificacion.query.count() == 0:
|
||||||
|
fichas_data = [
|
||||||
|
FichaClasificacion(id=1, huella_id=1, subclasificacion_id=1, tipos_f_id=1, fecha=datetime.strptime('2025-09-30', '%Y-%m-%d').date()),
|
||||||
|
FichaClasificacion(id=2, huella_id=2, subclasificacion_id=2, tipos_f_id=2, fecha=datetime.strptime('2025-09-30', '%Y-%m-%d').date()),
|
||||||
|
FichaClasificacion(id=3, huella_id=3, subclasificacion_id=3, tipos_f_id=3, fecha=datetime.strptime('2025-09-30', '%Y-%m-%d').date())
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(fichas_data)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
click.echo("¡Base de datos del S.I.P sembrada e integrada al 100%!")
|
||||||
0
api/app/config.py
Normal file
0
api/app/config.py
Normal file
5
api/app/database.py
Normal file
5
api/app/database.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
migrate = Migrate()
|
||||||
33
api/app/middlewares/auth_middleware.py
Normal file
33
api/app/middlewares/auth_middleware.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from flask import request, jsonify
|
||||||
|
import jwt
|
||||||
|
import os
|
||||||
|
|
||||||
|
def token_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
# ⬇️ MOVE DE ACÁ LA IMPORTACIÓN DEL MODELO ⬇️
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
token = request.headers.get('Authorization')
|
||||||
|
if not token:
|
||||||
|
return jsonify({'message': 'Token faltante'}), 401
|
||||||
|
try:
|
||||||
|
token = token.split(" ")[1] if " " in token else token
|
||||||
|
data = jwt.decode(token, os.environ.get('JWT_SECRET_KEY'), algorithms=["HS256"])
|
||||||
|
current_user = User.query.get(data['user_id'])
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'message': 'Token inválido o expirado'}), 401
|
||||||
|
|
||||||
|
return f(current_user, *args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
def has_permission(required_permission):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(current_user, *args, **kwargs):
|
||||||
|
if not current_user.role or required_permission not in current_user.role.permissions:
|
||||||
|
return jsonify({'message': 'No tenés permisos para realizar esta acción'}), 403
|
||||||
|
return f(current_user, *args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
12
api/app/models/__init__.py
Normal file
12
api/app/models/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# api/app/models/__init__.py
|
||||||
|
from app.models.user import User, Role
|
||||||
|
from app.models.biometria import (
|
||||||
|
Persona, OrigenHuella, TiposF, Subclasificacion,
|
||||||
|
Huella, PuntoCaracteristico, HuellaPuntoCaracteristico, FichaClasificacion
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'User', 'Role', 'Persona', 'OrigenHuella', 'TiposF',
|
||||||
|
'Subclasificacion', 'Huella', 'PuntoCaracteristico',
|
||||||
|
'HuellaPuntoCaracteristico', 'FichaClasificacion'
|
||||||
|
]
|
||||||
92
api/app/models/biometria.py
Normal file
92
api/app/models/biometria.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
from app.database import db
|
||||||
|
|
||||||
|
class Persona(db.Model):
|
||||||
|
__tablename__ = 'personas'
|
||||||
|
|
||||||
|
cod_id = db.Column(db.Integer, primary_key=True)
|
||||||
|
cod_nombre = db.Column(db.String(255), nullable=False)
|
||||||
|
fecha_nacimiento = db.Column(db.Date, nullable=True)
|
||||||
|
sexo = db.Column(db.String(1), nullable=True) # 'M', 'F' u otros
|
||||||
|
|
||||||
|
# Relación inversa: Una persona puede tener varias huellas registradas
|
||||||
|
huellas = db.relationship('Huella', backref='persona', cascade='all, delete-orphan', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class OrigenHuella(db.Model):
|
||||||
|
__tablename__ = 'origen_huellas'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
descripcion = db.Column(db.String(255), nullable=False) # Ej: Índice, pulgar
|
||||||
|
lado = db.Column(db.String(50), nullable=True) # Ej: Derecho, izquierdo
|
||||||
|
|
||||||
|
huellas = db.relationship('Huella', backref='origen', lazy=True)
|
||||||
|
|
||||||
|
#Tipo fundamental para la clasificación dactilar, con relaciones a subclasificaciones y fichas de clasificación
|
||||||
|
class TiposF(db.Model):
|
||||||
|
__tablename__ = 'tipos_f'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
nombre = db.Column(db.String(255), nullable=False) # Ej: Arco, Presilla, Verticilo
|
||||||
|
foto = db.Column(db.LargeBinary, nullable=True) # Almacenamiento binario eficiente en Postgres
|
||||||
|
descripcion = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
|
subclasificaciones = db.relationship('Subclasificacion', backref='tipo_f', lazy=True)
|
||||||
|
fichas = db.relationship('FichaClasificacion', backref='tipo_f', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Subclasificacion(db.Model):
|
||||||
|
__tablename__ = 'subclasificaciones'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
nombre = db.Column(db.String(255), nullable=False) # Ej: Arco simple, Presilla derecha
|
||||||
|
tipo_f_id = db.Column(db.Integer, db.ForeignKey('tipos_f.id', ondelete='RESTRICT'), nullable=False)
|
||||||
|
|
||||||
|
fichas = db.relationship('FichaClasificacion', backref='subclasificacion', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Huella(db.Model):
|
||||||
|
__tablename__ = 'huellas'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
descripcion = db.Column(db.String(255), nullable=True)
|
||||||
|
origen_hd = db.Column(db.String(255), nullable=True) # Ej: Escáner, Tinta
|
||||||
|
foto = db.Column(db.LargeBinary, nullable=True) # Imagen de la huella dactilar (Fichero RAW/WSQ)
|
||||||
|
persona_cod_id = db.Column(db.Integer, db.ForeignKey('personas.cod_id', ondelete='CASCADE'), nullable=False)
|
||||||
|
origen_huella_id = db.Column(db.Integer, db.ForeignKey('origen_huellas.id', ondelete='RESTRICT'), nullable=False)
|
||||||
|
|
||||||
|
# Relaciones Muchos a Muchos mediante tabla intermedia con atributos compuestos
|
||||||
|
puntos_caracteristicos = db.relationship('HuellaPuntoCaracteristico', backref='huella', cascade='all, delete-orphan', lazy=True)
|
||||||
|
fichas = db.relationship('FichaClasificacion', backref='huella', cascade='all, delete-orphan', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PuntoCaracteristico(db.Model):
|
||||||
|
__tablename__ = 'puntos_caracteristicos'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
nombre = db.Column(db.String(255), nullable=False) # Ej: Bifurcación, Final de cresta
|
||||||
|
foto = db.Column(db.LargeBinary, nullable=True) # Gráfico patrón del punto
|
||||||
|
|
||||||
|
huellas_asociadas = db.relationship('HuellaPuntoCaracteristico', backref='punto', cascade='all, delete-orphan', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class HuellaPuntoCaracteristico(db.Model):
|
||||||
|
""" Tabla pivote relacional con atributos de coordenadas geo-espaciales (X, Y) y ángulo de la minucia """
|
||||||
|
__tablename__ = 'huella_punto_caracteristico'
|
||||||
|
|
||||||
|
huella_id = db.Column(db.Integer, db.ForeignKey('huellas.id', ondelete='CASCADE'), primary_key=True)
|
||||||
|
punto_id = db.Column(db.Integer, db.ForeignKey('puntos_caracteristicos.id', ondelete='CASCADE'), primary_key=True)
|
||||||
|
|
||||||
|
# Coordenadas numéricas precisas para algoritmos de correspondencia (Matching)
|
||||||
|
x = db.Column(db.Numeric(10, 6), nullable=False)
|
||||||
|
y = db.Column(db.Numeric(10, 6), nullable=False)
|
||||||
|
angulo = db.Column(db.Numeric(5, 2), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class FichaClasificacion(db.Model):
|
||||||
|
__tablename__ = 'fichas_clasificacion'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
huella_id = db.Column(db.Integer, db.ForeignKey('huellas.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
subclasificacion_id = db.Column(db.Integer, db.ForeignKey('subclasificaciones.id', ondelete='RESTRICT'), nullable=False)
|
||||||
|
tipos_f_id = db.Column(db.Integer, db.ForeignKey('tipos_f.id', ondelete='RESTRICT'), nullable=False)
|
||||||
|
fecha = db.Column(db.Date, nullable=False, default=db.func.current_date())
|
||||||
23
api/app/models/fingerprint.py
Normal file
23
api/app/models/fingerprint.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from app.database import db
|
||||||
|
from app.models.fingerprint import Fingerprint # Asumiendo que creás este modelo
|
||||||
|
from app.middlewares.auth_middleware import token_required, has_permission
|
||||||
|
|
||||||
|
fingerprints_bp = Blueprint('fingerprints', __name__)
|
||||||
|
|
||||||
|
@fingerprints_bp.route('/api/fingerprints', methods=['POST'])
|
||||||
|
@token_required
|
||||||
|
@has_permission('write:fingerprints')
|
||||||
|
def register_fingerprint(current_user):
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# Lógica de persistencia equivalente a: Fingerprint::create([...])
|
||||||
|
new_print = Fingerprint(
|
||||||
|
patient_id=data.get('patient_id'),
|
||||||
|
image_path=data.get('image_path'), # Ruta local o S3
|
||||||
|
researcher_id=current_user.id
|
||||||
|
)
|
||||||
|
db.session.add(new_print)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({'status': 'Huella registrada con éxito', 'id': new_print.id}), 21
|
||||||
0
api/app/models/report.py
Normal file
0
api/app/models/report.py
Normal file
33
api/app/models/user.py
Normal file
33
api/app/models/user.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from app.database import db
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
class Role(db.Model):
|
||||||
|
__tablename__ = 'roles'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(50), unique=True, nullable=False)
|
||||||
|
permissions = db.Column(db.JSON, nullable=False)
|
||||||
|
|
||||||
|
users = db.relationship('User', backref='role', lazy=True)
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(128), nullable=False)
|
||||||
|
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), nullable=True)
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return bcrypt.checkpw(password.encode('utf-8'), self.password_hash.encode('utf-8'))
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'username': self.username,
|
||||||
|
'email': self.email,
|
||||||
|
'role': self.role.name if self.role else None,
|
||||||
|
'permissions': self.role.permissions if self.role else []
|
||||||
|
}
|
||||||
0
api/app/routes/__init__.py
Normal file
0
api/app/routes/__init__.py
Normal file
69
api/app/routes/auth.py
Normal file
69
api/app/routes/auth.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from app.database import db
|
||||||
|
# ⬇️ Cambiamos esta línea para importar desde el módulo raíz de modelos ⬇️
|
||||||
|
from app.models import User, Role
|
||||||
|
from app.middlewares.auth_middleware import token_required
|
||||||
|
import jwt
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
# (El resto del código del archivo auth.py queda exactamente igual)
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
@auth_bp.route('/register', methods=['POST'])
|
||||||
|
def register():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data or not data.get('username') or not data.get('password') or not data.get('email'):
|
||||||
|
return jsonify({'message': 'Datos incompletos'}), 400
|
||||||
|
|
||||||
|
if User.query.filter((User.username == data['username']) | (User.email == data['email'])).first():
|
||||||
|
return jsonify({'message': 'El usuario o email ya existe'}), 400
|
||||||
|
|
||||||
|
# Asignar un rol por defecto si no se especifica (ej: el ID del rol investigador)
|
||||||
|
default_role = Role.query.filter_by(name='investigador').first()
|
||||||
|
|
||||||
|
new_user = User(
|
||||||
|
username=data['username'],
|
||||||
|
email=data['email'],
|
||||||
|
role_id=default_role.id if default_role else None
|
||||||
|
)
|
||||||
|
new_user.set_password(data['password'])
|
||||||
|
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({'message': 'Usuario registrado con éxito'}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data or not data.get('username') or not data.get('password'):
|
||||||
|
return jsonify({'message': 'Credenciales requeridas'}), 400
|
||||||
|
|
||||||
|
user = User.query.filter_by(username=data['username']).first()
|
||||||
|
|
||||||
|
if not user or not user.check_password(data['password']):
|
||||||
|
return jsonify({'message': 'Usuario o contraseña incorrectos'}), 401
|
||||||
|
|
||||||
|
# Generación del JWT Token
|
||||||
|
payload = {
|
||||||
|
'user_id': user.id,
|
||||||
|
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=2)
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload, os.environ.get('JWT_SECRET_KEY', 'secret'), algorithm='HS256')
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'token': token,
|
||||||
|
'user': user.to_dict()
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/me', methods=['GET'])
|
||||||
|
@token_required
|
||||||
|
def me(current_user):
|
||||||
|
# 'current_user' es inyectado automáticamente por nuestro middleware 'token_required'
|
||||||
|
return jsonify(current_user.to_dict()), 200
|
||||||
0
api/app/routes/fingerprints.py
Normal file
0
api/app/routes/fingerprints.py
Normal file
0
api/app/routes/reports.py
Normal file
0
api/app/routes/reports.py
Normal file
60
api/app/routes/roles.py
Normal file
60
api/app/routes/roles.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from app.database import db
|
||||||
|
from app.models.user import Role
|
||||||
|
from app.middlewares.auth_middleware import token_required, has_permission
|
||||||
|
|
||||||
|
roles_bp = Blueprint('roles', __name__)
|
||||||
|
|
||||||
|
# INDEX: Listar todos los roles
|
||||||
|
@roles_bp.route('/roles', methods=['GET'])
|
||||||
|
@token_required
|
||||||
|
@has_permission('read:users') # Permiso administrativo general
|
||||||
|
def index(current_user):
|
||||||
|
roles = Role.query.all()
|
||||||
|
return jsonify([{'id': r.id, 'name': r.name, 'permissions': r.permissions} for r in roles]), 200
|
||||||
|
|
||||||
|
# STORE: Crear un nuevo rol
|
||||||
|
@roles_bp.route('/roles', methods=['POST'])
|
||||||
|
@token_required
|
||||||
|
@has_permission('write:users')
|
||||||
|
def store(current_user):
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or not data.get('name') or not data.get('permissions'):
|
||||||
|
return jsonify({'message': 'Datos incompletos'}), 400
|
||||||
|
|
||||||
|
if Role.query.filter_by(name=data['name']).first():
|
||||||
|
return jsonify({'message': 'El rol ya existe'}), 400
|
||||||
|
|
||||||
|
new_role = Role(name=data['name'], permissions=data['permissions']) # Recibe lista de strings
|
||||||
|
db.session.add(new_role)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'message': 'Rol creado con éxito', 'id': new_role.id}), 201
|
||||||
|
|
||||||
|
# UPDATE: Modificar un rol existente
|
||||||
|
@roles_bp.route('/roles/<int:id>', methods=['PUT'])
|
||||||
|
@token_required
|
||||||
|
@has_permission('write:users')
|
||||||
|
def update(current_user, id):
|
||||||
|
role = Role.query.get_or_404(id)
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
role.name = data.get('name', role.name)
|
||||||
|
role.permissions = data.get('permissions', role.permissions)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'message': 'Rol actualizado con éxito'}), 200
|
||||||
|
|
||||||
|
# DESTROY: Eliminar un rol
|
||||||
|
@roles_bp.route('/roles/<int:id>', methods=['DELETE'])
|
||||||
|
@token_required
|
||||||
|
@has_permission('write:users')
|
||||||
|
def destroy(current_user, id):
|
||||||
|
role = Role.query.get_or_404(id)
|
||||||
|
|
||||||
|
# Evitar romper restricciones de clave foránea si hay usuarios usándolo
|
||||||
|
if len(role.users) > 0:
|
||||||
|
return jsonify({'message': 'No se puede eliminar un rol asignado a usuarios activos'}), 400
|
||||||
|
|
||||||
|
db.session.delete(role)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'message': 'Rol eliminado con éxito'}), 200
|
||||||
12
api/app/routes/users.py
Normal file
12
api/app/routes/users.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from app.models import User
|
||||||
|
from app.middlewares.auth_middleware import token_required, has_permission
|
||||||
|
|
||||||
|
users_bp = Blueprint('users', __name__)
|
||||||
|
|
||||||
|
@users_bp.route('/users', methods=['GET'])
|
||||||
|
@token_required
|
||||||
|
@has_permission('read:users')
|
||||||
|
def index(current_user):
|
||||||
|
users = User.query.all()
|
||||||
|
return jsonify([user.to_dict() for user in users]), 200
|
||||||
1
api/migrations/README
Normal file
1
api/migrations/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
||||||
50
api/migrations/alembic.ini
Normal file
50
api/migrations/alembic.ini
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic,flask_migrate
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
113
api/migrations/env.py
Normal file
113
api/migrations/env.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata():
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas[None]
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
conf_args = current_app.extensions['migrate'].configure_args
|
||||||
|
if conf_args.get("process_revision_directives") is None:
|
||||||
|
conf_args["process_revision_directives"] = process_revision_directives
|
||||||
|
|
||||||
|
connectable = get_engine()
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
**conf_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
api/migrations/script.py.mako
Normal file
24
api/migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
"""crear_tablas_iniciales
|
||||||
|
|
||||||
|
Revision ID: 04e47df0b26f
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-05-21 16:20:06.426487
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '04e47df0b26f'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('roles',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('permissions', sa.JSON(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=80), nullable=False),
|
||||||
|
sa.Column('email', sa.String(length=120), nullable=False),
|
||||||
|
sa.Column('password_hash', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('email'),
|
||||||
|
sa.UniqueConstraint('username')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_table('roles')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
"""migracion_sistema_papiloscopico
|
||||||
|
|
||||||
|
Revision ID: a946075b6c2c
|
||||||
|
Revises: 04e47df0b26f
|
||||||
|
Create Date: 2026-05-21 19:06:40.586192
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a946075b6c2c'
|
||||||
|
down_revision = '04e47df0b26f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('origen_huellas',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('descripcion', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('lado', sa.String(length=50), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('personas',
|
||||||
|
sa.Column('cod_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('cod_nombre', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('fecha_nacimiento', sa.Date(), nullable=True),
|
||||||
|
sa.Column('sexo', sa.String(length=1), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('cod_id')
|
||||||
|
)
|
||||||
|
op.create_table('puntos_caracteristicos',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('nombre', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('foto', sa.LargeBinary(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('tipos_f',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('nombre', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('foto', sa.LargeBinary(), nullable=True),
|
||||||
|
sa.Column('descripcion', sa.Text(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('huellas',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('descripcion', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('origen_hd', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('foto', sa.LargeBinary(), nullable=True),
|
||||||
|
sa.Column('persona_cod_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('origen_huella_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['origen_huella_id'], ['origen_huellas.id'], ondelete='RESTRICT'),
|
||||||
|
sa.ForeignKeyConstraint(['persona_cod_id'], ['personas.cod_id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('subclasificaciones',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('nombre', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('tipo_f_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['tipo_f_id'], ['tipos_f.id'], ondelete='RESTRICT'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('fichas_clasificacion',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('huella_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('subclasificacion_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('tipos_f_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('fecha', sa.Date(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['huella_id'], ['huellas.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['subclasificacion_id'], ['subclasificaciones.id'], ondelete='RESTRICT'),
|
||||||
|
sa.ForeignKeyConstraint(['tipos_f_id'], ['tipos_f.id'], ondelete='RESTRICT'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('huella_punto_caracteristico',
|
||||||
|
sa.Column('huella_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('punto_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('x', sa.Numeric(precision=10, scale=6), nullable=False),
|
||||||
|
sa.Column('y', sa.Numeric(precision=10, scale=6), nullable=False),
|
||||||
|
sa.Column('angulo', sa.Numeric(precision=5, scale=2), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['huella_id'], ['huellas.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['punto_id'], ['puntos_caracteristicos.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('huella_id', 'punto_id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('huella_punto_caracteristico')
|
||||||
|
op.drop_table('fichas_clasificacion')
|
||||||
|
op.drop_table('subclasificaciones')
|
||||||
|
op.drop_table('huellas')
|
||||||
|
op.drop_table('tipos_f')
|
||||||
|
op.drop_table('puntos_caracteristicos')
|
||||||
|
op.drop_table('personas')
|
||||||
|
op.drop_table('origen_huellas')
|
||||||
|
# ### end Alembic commands ###
|
||||||
7
api/requirements.txt
Normal file
7
api/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Flask==3.0.2
|
||||||
|
Flask-SQLAlchemy==3.1.1
|
||||||
|
Flask-Migrate==4.0.7
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
PyJWT==2.8.0
|
||||||
|
bcrypt==4.1.2
|
||||||
|
Flask-CORS==4.0.0
|
||||||
215
api/run.py
Normal file
215
api/run.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import click
|
||||||
|
from flask import Flask
|
||||||
|
from flask.cli import with_appcontext
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
from flask_cors import CORS
|
||||||
|
# Importamos la base de datos y los modelos del S.I.P
|
||||||
|
from app.database import db
|
||||||
|
from app.models import Role, User
|
||||||
|
from app.models.biometria import (
|
||||||
|
Persona, OrigenHuella, TiposF, Subclasificacion,
|
||||||
|
Huella, PuntoCaracteristico, HuellaPuntoCaracteristico, FichaClasificacion
|
||||||
|
)
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 1. COMANDO CLI DE CLICK (Configurado Correctamente)
|
||||||
|
# =========================================================
|
||||||
|
@click.command(name="seed")
|
||||||
|
@with_appcontext
|
||||||
|
def seed_db_command():
|
||||||
|
"""Siembra de roles, usuarios administrativos y datos iniciales del S.I.P"""
|
||||||
|
click.echo("Iniciando el sembrado de datos en PostgreSQL...")
|
||||||
|
|
||||||
|
# --- [ROLES Y USUARIOS] ---
|
||||||
|
admin_role = Role.query.filter_by(name='admin').first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(name='admin', permissions=['read:users', 'write:users', 'manage:biometria'])
|
||||||
|
db.session.add(admin_role)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
investigador_role = Role.query.filter_by(name='investigador').first()
|
||||||
|
if not investigador_role:
|
||||||
|
investigador_role = Role(name='investigador', permissions=['manage:biometria'])
|
||||||
|
db.session.add(investigador_role)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
admin_user = User.query.filter_by(username='admin_root').first()
|
||||||
|
if not admin_user:
|
||||||
|
admin_user = User(username='admin_root', email='admin@uader.edu.ar', role_id=admin_role.id)
|
||||||
|
admin_user.set_password('admin1234')
|
||||||
|
db.session.add(admin_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
investigador_user = User.query.filter_by(username='investigador_demo').first()
|
||||||
|
if not investigador_user:
|
||||||
|
investigador_user = User(username='investigador_demo', email='investigador@uader.edu.ar', role_id=investigador_role.id)
|
||||||
|
investigador_user.set_password('investigador1234')
|
||||||
|
db.session.add(investigador_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# --- [1. PERSONAS] ---
|
||||||
|
if Persona.query.count() == 0:
|
||||||
|
personas_data = [
|
||||||
|
Persona(cod_id=1, cod_nombre='Juan Pérez', fecha_nacimiento=datetime.strptime('1990-05-20', '%Y-%m-%d').date(), sexo='M'),
|
||||||
|
Persona(cod_id=2, cod_nombre='Ana Gómez', fecha_nacimiento=datetime.strptime('1985-12-01', '%Y-%m-%d').date(), sexo='F'),
|
||||||
|
Persona(cod_id=3, cod_nombre='Carlos López', fecha_nacimiento=datetime.strptime('2000-07-15', '%Y-%m-%d').date(), sexo='M')
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(personas_data)
|
||||||
|
|
||||||
|
# --- [2. ORIGEN DE HUELLAS] ---
|
||||||
|
if OrigenHuella.query.count() == 0:
|
||||||
|
origenes_data = [
|
||||||
|
OrigenHuella(id=1, descripcion='Índice derecho', lado='derecho'),
|
||||||
|
OrigenHuella(id=2, descripcion='Índice izquierdo', lado='izquierdo'),
|
||||||
|
OrigenHuella(id=3, descripcion='Pulgar derecho', lado='derecho')
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(origenes_data)
|
||||||
|
|
||||||
|
# --- [3. TIPOS FUNDAMENTALES] ---
|
||||||
|
if TiposF.query.count() == 0:
|
||||||
|
tipos_data = [
|
||||||
|
TiposF(id=1, nombre='Arco', descripcion='Huella en forma de arco', foto=None),
|
||||||
|
TiposF(id=2, nombre='Presilla', descripcion='Huella en forma de presilla', foto=None),
|
||||||
|
TiposF(id=3, nombre='Verticilo', descripcion='Huella en forma de espiral', foto=None)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(tipos_data)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# --- [4. SUBCLASIFICACIONES] ---
|
||||||
|
if Subclasificacion.query.count() == 0:
|
||||||
|
sub_data = [
|
||||||
|
Subclasificacion(id=1, nombre='Arco simple', tipo_f_id=1),
|
||||||
|
Subclasificacion(id=2, nombre='Presilla derecha', tipo_f_id=2),
|
||||||
|
Subclasificacion(id=3, nombre='Verticilo central', tipo_f_id=3)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(sub_data)
|
||||||
|
|
||||||
|
# --- [5. PUNTOS CARACTERÍSTICOS] ---
|
||||||
|
if PuntoCaracteristico.query.count() == 0:
|
||||||
|
puntos_data = [
|
||||||
|
PuntoCaracteristico(id=1, nombre='Bifurcación', foto=None),
|
||||||
|
PuntoCaracteristico(id=2, nombre='Final de cresta', foto=None),
|
||||||
|
PuntoCaracteristico(id=3, nombre='Punto de isla', foto=None)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(puntos_data)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# --- [6. HUELLAS] ---
|
||||||
|
if Huella.query.count() == 0:
|
||||||
|
huellas_data = [
|
||||||
|
Huella(id=1, descripcion='Huella digital clara', origen_hd='Escáner', foto=None, persona_cod_id=1, origen_huella_id=1),
|
||||||
|
Huella(id=2, descripcion='Huella con ligera distorsión', origen_hd='Tinta', foto=None, persona_cod_id=2, origen_huella_id=2),
|
||||||
|
Huella(id=3, descripcion='Huella poco definida', origen_hd='Escáner', foto=None, persona_cod_id=3, origen_huella_id=3)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(huellas_data)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# --- [7. COORDENADAS PIVOTE] ---
|
||||||
|
if HuellaPuntoCaracteristico.query.count() == 0:
|
||||||
|
coordenadas_data = [
|
||||||
|
HuellaPuntoCaracteristico(huella_id=1, punto_id=1, x=12.345678, y=45.678912, angulo=30.00),
|
||||||
|
HuellaPuntoCaracteristico(huella_id=1, punto_id=2, x=15.111111, y=50.222222, angulo=45.00),
|
||||||
|
HuellaPuntoCaracteristico(huella_id=2, punto_id=3, x=20.333333, y=60.444444, angulo=60.00)
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(coordenadas_data)
|
||||||
|
|
||||||
|
# --- [8. FICHAS DE CLASIFICACIÓN] ---
|
||||||
|
if FichaClasificacion.query.count() == 0:
|
||||||
|
fichas_data = [
|
||||||
|
FichaClasificacion(id=1, huella_id=1, subclasificacion_id=1, tipos_f_id=1, fecha=datetime.strptime('2025-09-30', '%Y-%m-%d').date()),
|
||||||
|
FichaClasificacion(id=2, huella_id=2, subclasificacion_id=2, tipos_f_id=2, fecha=datetime.strptime('2025-09-30', '%Y-%m-%d').date()),
|
||||||
|
FichaClasificacion(id=3, huella_id=3, subclasificacion_id=3, tipos_f_id=3, fecha=datetime.strptime('2025-09-30', '%Y-%m-%d').date())
|
||||||
|
]
|
||||||
|
db.session.bulk_save_objects(fichas_data)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
click.echo("¡Base de datos del S.I.P sembrada e integrada al 100%!")
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 2. FABRICA DE LA APLICACIÓN (Application Factory)
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# 3. Inicialización de Flask-CORS agresiva a nivel de aplicación interna
|
||||||
|
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
|
||||||
|
|
||||||
|
# 4. CAPA DE CONTROL ADICIONAL: Interceptor nativo de Preflight Checks
|
||||||
|
@app.after_request
|
||||||
|
def add_cors_headers(response):
|
||||||
|
# Permitimos cualquier origen de desarrollo (como tu puerto 8080)
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization"
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS"
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def handle_options_directly():
|
||||||
|
from flask import make_response, request
|
||||||
|
# Si el navegador pregunta mediante OPTIONS, respondemos de inmediato con 200 OK
|
||||||
|
# saltándonos cualquier validación posterior de rutas o Blueprints
|
||||||
|
if request.method.upper() == "OPTIONS":
|
||||||
|
response = make_response()
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization"
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS"
|
||||||
|
return response, 200
|
||||||
|
|
||||||
|
# Mapeo exacto de tu archivo .env de la raíz
|
||||||
|
POSTGRES_USER = os.getenv('POSTGRES_USER', 'admin')
|
||||||
|
POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD', 'secretpassword_2026_x8')
|
||||||
|
POSTGRES_DB = os.getenv('POSTGRES_DB', 'huellas_investigacion')
|
||||||
|
POSTGRES_HOST = os.getenv('POSTGRES_HOST', 'db') # Cambiado a 'db' según tu .env
|
||||||
|
POSTGRES_PORT = os.getenv('POSTGRES_PORT', '5432')
|
||||||
|
|
||||||
|
# Reconstruimos la URI de forma segura en Python para evitar fallos de interpolación string
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = f'postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}'
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
|
# Secreto para los Tokens JWT de tu módulo auth
|
||||||
|
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'laravel_style_jwt_secret_token_scientific_2026')
|
||||||
|
|
||||||
|
# Inicializar base de datos y migraciones
|
||||||
|
db.init_app(app)
|
||||||
|
Migrate(app, db)
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# REGISTRO DE BLUEPRINTS (Normalización de Prefijos REST)
|
||||||
|
# =========================================================
|
||||||
|
try:
|
||||||
|
# 1. Rutas de Autenticación
|
||||||
|
from app.routes.auth import auth_bp
|
||||||
|
# Mapea directamente: POST /api/auth/login y GET /api/auth/me
|
||||||
|
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||||
|
print("-> Blueprint 'auth' registrado en /api/auth")
|
||||||
|
|
||||||
|
# 2. Rutas de Usuarios
|
||||||
|
from app.routes.users import users_bp
|
||||||
|
# Al poner el prefijo '/api' a secas, mapea exacto: GET /api/users
|
||||||
|
app.register_blueprint(users_bp, url_prefix='/api')
|
||||||
|
print("-> Blueprint 'users' registrado en /api")
|
||||||
|
|
||||||
|
# 3. Rutas de Roles
|
||||||
|
from app.routes.roles import roles_bp
|
||||||
|
# Al poner el prefijo '/api' a secas, mapea exacto: GET /api/roles y POST /api/roles
|
||||||
|
app.register_blueprint(roles_bp, url_prefix='/api')
|
||||||
|
print("-> Blueprint 'roles' registrado en /api")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"⚠️ Error al importar los módulos de rutas: {e}")
|
||||||
|
|
||||||
|
# Registrar comando seed
|
||||||
|
app.cli.add_command(seed_db_command)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
Loading…
Reference in New Issue
Block a user