Caso de uso - Login
El siguiente proyecto se basa inicialmente en el artículo How To Add Authentication to Your App with Flask-Login.
A partir de él, vamos a crear una aplicación en Flask que ataca a una base de datos de MongoDB para almacenar la información del proyecto PIA Lara.
Esta primera versión de la aplicación únicamente se centra en la gestión de los usuarios, distinguiendo entre los diferentes roles:
Administrador
: superusuario, puede crear, editar y eliminar todo tipo de usuarios.Técnico
: usuario que supervisará a los clientes, el cual, más adelante, puede llegar a crear textos predefinidos para los clientes.Cliente
: usuario final de la aplicación que, más adelante, grabará los audios.
Simplicidad
El presente caso de uso se ha organizado para intentar simplificar al máximo el código y preparar un esqueleto que facilite el crecimiento de la aplicación. Aún así, una solución basada en Django o el uso de herramientas de mapeo entre los datos y los objetos del dominio serían un punto de partida para siguientes fases del proyecto.
Estructura¶
Una vez descargado el proyecto y tras descomprimirlo, o clonado desde https://github.com/aitor-medrano/piafplogin, veremos que tiene la siguiente estructura:
PIAFPLOGIN/
├── migrations/
│ └── user_migration.py
├── project/
│ ├── static/
│ │ ├── pialara.js
│ │ └── pialara.png
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── login.html
│ │ └── profile.html
│ ├── __init__.py
│ ├── auth.py
│ ├── db.py
│ ├── main.py
│ └── models.py
├── .ini
└── requirements.txt
El primer paso es crear un entorno virtual:
python3 -m venv app-env
Y activarlo:
source app-env/bin/activate
A continuación instalaremos las dependencias mediante:
pip3 install -r requirements.txt
Configuración¶
Para configurar el proyecto, partimos del fichero .ini
que reside en la raíz del mismo y contiene los datos de configuración a MongoDB y la clave secreta que utiliza Flask para encriptar la sesión:
[PROD]
SECRET_KEY = eac5e91171438960ddec0c9c469a4c3dd42e96aea462afc5ab830f78527ad80e
PIALARA_DB_URI = mongodb+srv://usuario:contraseña@cluster0.xyz.mongodb.net
PIALARA_DB_NAME = pialara
[LOCAL]
SECRET_KEY = eac5e91171438960ddec0c9c469a4c3dd42e96aea462afc5ab830f78527ad80e
PIALARA_DB_URI = localhost
PIALARA_DB_NAME = pialara
Secret Key
Para generar una clave secreta, tal como indica la documentación de Flask, podemos ejecutar el siguiente comando:
python3 -c 'import secrets; print(secrets.token_hex())'
Una vez ya hemos configurado la conexión y antes de arrancar la aplicación, vamos a cargar unos datos básicos con usuarios. Para ello, en la carpeta migrations
tenemos el archivo users_migration.py
, el cual lee la configuración del archivo anterior, y crea tres usuarios:
from pymongo import MongoClient
from werkzeug.security import generate_password_hash
import os
import configparser
config = configparser.ConfigParser()
config.read(os.path.abspath(os.path.join(".ini")))
DB_URI = config['PROD']['PIALARA_DB_URI']
DB_NAME = config['PROD']['PIALARA_DB_NAME']
# DB_URI = config['LOCAL']['PIALARA_DB_URI']
# DB_NAME = config['LOCAL']['PIALARA_DB_NAME']
db = MongoClient(DB_URI)[DB_NAME]
usuarios = [
{"nombre":"Admin", "email":"admin@admin.com", "password":generate_password_hash("admin", method='sha256'), "rol":"Administrador"},
{"nombre":"Alumno", "email":"alumno@alumno.com", "password":generate_password_hash("alumno", method='sha256'), "rol":"Técnico"},
{"nombre":"Severo Ochoa", "email":"s8a@s8a.com", "password":generate_password_hash("s8a", method='sha256'), "rol":"Cliente", "parent":"alumno@alumno.com"},
]
try:
db.users.insert_many(usuarios)
except Exception as e:
print(e)
Destacar que al definir los documentos con los usuarios, encriptamos la contraseña mediante la función generate_password_hash
para no almacenarla en la base de datos en texto plano.
Así pues, ejecutamos la migración:
python3 migrations/users_migration.py
Finalmente podemos arrancar la aplicación:
flask --app project --debug run
Y acceder a la aplicación a través de http://127.0.0.1:5000/
:
Una vez que un usuario ha entrado a la aplicación, por ejemplo, si es un administrador, dispondrá de las opciones que hemos comentado anteriormente:

Blueprints¶
El archivo .ini que hemos configurado previamente define unos valores que la aplicación va a cargar desde el archivo __init__.py
, el cual actúa como factoría de la aplicación y le indica a Flask los blueprints a utilizar:
from flask import Flask
from flask_login import LoginManager
from . import db
import os
import configparser
config = configparser.ConfigParser()
config.read(os.path.abspath(os.path.join(".ini")))
def create_app():
app = Flask(__name__)
# cargamos la configuración
app.config['PIALARA_DB_URI'] = config['LOCAL']['PIALARA_DB_URI']
app.config['PIALARA_DB_NAME'] = config['LOCAL']['PIALARA_DB_NAME']
app.config['SECRET_KEY'] = config['LOCAL']['SECRET_KEY']
# configuramos flask-login con la ruta del login
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.init_app(app)
# función que utiliza flask-login para recuperar el usuario
@login_manager.user_loader
def load_user(user_id):
return db.get_user_by_id(user_id)
# blueprint para las rutas de autenticación
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
# blueprint para la aplicación
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
Un blueprint permite organizar un grupo de vistas y código en módulos. En vez de registrar las vistas y el resto de código en la aplicación, se registran en el blueprint, y éste es que se registra en la aplicación en la función create_app
.
En nuestro caso, vamos a empezar con dos blueprints, uno para las funciones de autenticación, y otra para las funciones de gestión de usuarios. El código de cada blueprint irá en un módulo separado. Como la gestión de usuarios necesita primero la autenticación, vamos a ver cómo funciona.
Login¶
Para la gestión de la autenticación, nos hemos apoyado en la librería Flask-login que facilita la gestión la sesión del usuario.
En archivo auth.py
contiene la lógica del login y el logout:
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_user, login_required, logout_user, current_user
from werkzeug.security import check_password_hash
from . import db
auth = Blueprint('auth', __name__)
@auth.route('/login')
def login():
return render_template('login.html')
@auth.route('/login', methods=['POST'])
def login_post():
email = request.form.get('email')
password = request.form.get('password')
remember = True if request.form.get('remember') else False
user = db.get_user(email)
# comprobamos si el usuario existe
# cogemos la contraseña, la hasheamos y la comparamos con la contraseña hasheada
if not user or not check_password_hash(user.password, password):
flash('Por favor, comprueba tus datos y vuélvelo a intentar.')
# si el usuario no existe, o está mal la contraseña, recargamos la página
return redirect(url_for('auth.login'))
# marcamos al usuario como autenticado en flask_login
login_user(user, remember=remember)
return redirect(url_for('main.profile', nombre = current_user.nombre))
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('Sesión cerrada con éxito')
return redirect(url_for('auth.login'))
Los usuarios van a entrar al sistema mediante su email y una contraseña. Así pues, una vez hayamos recuperado un usuario por dicho email, creamos el hash de la contraseña recibida, y vemos si comprueba con la recuperada de la base de datos.
El método login_user
de la línea 27 pertenece a la librería Flask-Login y se utiliza para indicar que el usuario ha sido autenticado, de manera que lo almacena en la sesión. La variable user
es una clase propia que hemos definido nosotros con los atributos básicos de un usuario,el cual se encuentra en el archivo models.py
:
from flask_login import UserMixin
class User(UserMixin):
def __init__(self, id, email, nombre, password, rol, parent = ""):
self.id = id
self.email = email
self.nombre = nombre
self.password = password
self.rol = rol
self.parent = parent
def __str__(self):
return f"{self.email} ({self.nombre} / {self.password})"
Como se puede observar, la clase define los atributos básicos de un usuario. El atributo parent
lo vamos a emplear para que los clientes almacenen el email del técnico que tienen asignado.
Plantillas¶
Las diferentes plantillas heredan de una plantilla base.html
, la cual emplea el framework Bulma para la apariencia de la web. Su funcionamiento es muy similar a Bootstrap.
Por ejemplo, vamos a revisar un fragmento de la plantilla base para ver cómo gestionamos la visualización del menú dependiendo del rol del usuario:
...
<section class="hero is-white is-fullheight">
<nav class="navbar is-transparent">
<div class="navbar-brand">
<a class="navbar-item" href="https://piafplara.es">
<img src="{{ url_for('static', filename='pialara.png') }}" alt="PIA Lara: un proyecto que habla por ti" width="112" height="28">
</a>
<div class="navbar-burger burger" data-target="navbarPIALara">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navbarPIALara" class="navbar-menu">
<div class="navbar-start">
<a href="{{ url_for('main.index') }}" class="navbar-item">
Inicio
</a>
{% if not current_user.is_authenticated %}
<a href="{{ url_for('auth.login') }}" class="navbar-item">
Login
</a>
{% endif %}
{% if current_user.is_authenticated %}
<a href="{{ url_for('main.profile') }}" class="navbar-item">
Perfil
</a>
{% if current_user.rol == "Administrador" %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">
Usuarios
</a>
<div class="navbar-dropdown is-hidden-mobile is-boxed">
<a class="navbar-item" href="{{ url_for('main.user_create') }}">
Alta
</a>
<a class="navbar-item" href="{{ url_for('main.user_list') }}">
Listado
</a>
</div>
</div>
{% endif %}
...
En la línea 6 utilizamos la función url_for
con el parámetro static
para indicarle que cargue la imagen con el logo del proyecto desde la carpeta static
.
Al utilizar la librería Flask Login, tendremos siempre disponible el usuario logueado en la variable current_user
. Además de las propiedades que hayamos definido en la clase, disponemos de la función is_authenticated
para comprobar si está autenticado (línea 20). De igual forma, podemos comprobar el rol y condicionar el contenido dependiendo de si es Administrador
, Técnico
o Cliente
(línea 24).
Acceso a los datos¶
Todo el acceso a los datos los hemos encapsulado en el archivo db.py
:
from pymongo import MongoClient
from bson.objectid import ObjectId
from pymongo import ASCENDING
from flask import current_app, g
from werkzeug.local import LocalProxy
from project.models import User
def get_db():
"""
Método de configuración para obtener una instancia de db
"""
db = getattr(g, "_database", None)
PIALARA_DB_URI = current_app.config["PIALARA_DB_URI"]
PIALARA_DB_DB_NAME = current_app.config["PIALARA_DB_NAME"]
if db is None:
db = g._database = MongoClient(
PIALARA_DB_URI,
maxPoolSize=50,
timeoutMS=2500
)[PIALARA_DB_DB_NAME]
return db
# Utilizamos LocalProxy para leer la variable global usando sólo db
db = LocalProxy(get_db)
La función get_db
utiliza el objeto g
, el cual en Flask, es un objeto especial que es único para cada petición. Se utiliza para almacenar datos que serán accesibles desde múltiples funciones durante el request
. Así pues, almacenamos la conexión, mejor dicho, el pool de conexiones a MongoDB en vez de crear un nuevo pool cada vez que queramos obtener acceso a la base de datos.
A continuación, creamos un LocalProxy
para leer la variable global usando sólo la referencia db
, de manera que internamente cada referencia a db
realmente está llamando a get_db()
.
A continuación, mostramos un par de métodos del mismo archivo que muestran cómo obtenemos datos desde MongoDB haciendo uso de PyMongo:
def get_all_users():
"""
Devuelve una lista con todos los usuarios del sistema
"""
try:
return list(db.users.find({}).sort("nombre", ASCENDING))
except Exception as e:
return e
def get_user_by_id(id):
"""
Devuelve un objeto User a partir de su id
"""
try:
usuario = db.users.find_one({"_id":ObjectId(id)})
usuario_obj = User( id=usuario["_id"],
email=usuario.get("email"),
nombre=usuario.get("nombre"),
password=usuario.get("password"),
rol=usuario.get("rol"),
parent=usuario.get("parent"))
return usuario_obj
except Exception as e:
return e
...
Cuando recuperamos un usuario por su id
, lo convertimos en un objeto User
para que Flask Login falicita la gestión de la autenticación. En cambio, en el listado de todos los usuarios, vamos a acceder al cursor de usuarios que ofrece MongoDB.
Referencias¶
- Tutorial oficial de PyMongo
- Introduction to Multi-Document ACID Transactions in Python
- How To Use Transactions in MongoDB
Actividades¶
-
(RASBD.3 / CESBD.3d / 4p) A partir del caso de uso, se pide:
- (0.25) Configurar la URI de Mongo Atlas para atacar vuestra propia base de datos.
- (0.25) Modificar la migración para introducir más usuarios (al menos uno más de cada rol)
- (0.75) Cuando un usuario pulsa sobre su nombre, actualmente aparece un formulario para editar sus datos, pero no puede cambiar la contraseña. Modifica (o crea) el/los formulario/s adecuado/s para que cada usuario pueda cambiar su propia contraseña.
- (0.75) Desde el rol
Administrador
, al crear un usuario, si es un cliente, debe mostrar un desplegable con todos los técnicos disponibles. - (1) Tanto el
Técnico
como elCliente
, al dar de alta o editar un cliente, almacenarán datos necesarios para el proyecto, como son el sexo, la fecha de nacimiento y la patología. - (1) Cuando un
Técnico
visualiza el listado de sus clientes, debe recuperar únicamente el nombre, el sexo, la edad y su patología.