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