06/08
Autenticación, sesiones y seguridad
La semana pasada "tomaste prestado" el login de /admin para poder usar tu propio CRUD. Esta semana construyes tu propia pantalla de Registro y Login con django.contrib.auth — el sistema de autenticación que Django trae incluido de fábrica — y haces que cada usuario vea y edite solo sus propios servicios. Cierras con un checklist de seguridad antes del último deploy de la unidad.
u3s6-apellidopaterno-apellidomaterno-nombres y su Web Service en https://u3s6-apellidopaterno-apellidomaterno-nombres.onrender.com — registro/login/logout funcionando en vivo; un usuario nuevo solo puede editar o borrar sus propios registros.
django.contrib.auth: usuarios, hashing de contraseñas y sesiones
Django trae de fábrica una app llamada django.contrib.auth, instalada desde el primer startproject (la usas sin saberlo desde la Semana 3, para el panel /admin). Ahí vive el modelo User, el hashing de contraseñas y el sistema de sesiones.
Nunca se guarda una contraseña en texto plano
Cuando llamas a createsuperuser o registras un usuario, Django nunca guarda la contraseña tal cual la tecleaste: la pasa por una función de hashing (PBKDF2 por defecto) que la convierte en un texto largo e irreversible. Al iniciar sesión, Django vuelve a aplicar el mismo hash a lo que escribiste y compara los resultados — nunca "desencripta" nada, porque el hashing no se puede revertir.
Qué es una sesión
Al iniciar sesión, Django guarda un identificador en una cookie del navegador y los datos reales (qué usuario eres) en el servidor. En cada petición siguiente, esa cookie le dice a Django "este visitante ya se autenticó" sin que tengas que volver a escribir la contraseña en cada clic.
request.user es la variable que usas en cualquier vista para saber quién hizo la petición. Si nadie inició sesión, request.user es un usuario "anónimo" especial (AnonymousUser), no None.
Vistas de Registro y Login con formularios Tailwind
Django ya trae un formulario de registro (UserCreationForm) y las funciones de login/logout — no hace falta escribir el hashing ni la validación de contraseñas a mano.
Actualizar los import de views.py
Esta semana usarás funciones nuevas de Django (login, logout, formularios de usuario y mensajes). Abre core/views.py: arriba de todo están las líneas que empiezan con from (con sus comentarios), que pegaste en la Semana 5. Selecciónalas todas, desde la primera línea del archivo hasta from .forms import ServicioForm, bórralas y pega en su lugar este bloque. Las funciones de abajo (inicio, servicios, crear_servicio…) no se tocan todavía.
# render, redirect, get_object_or_404: los mismos atajos de Django que ya usabas en la Semana 5
from django.shortcuts import render, redirect, get_object_or_404
# connection: datos de la conexión a la base (lo usa la vista "inicio" de la Semana 3)
from django.db import connection
# login_required: decorador que exige sesión iniciada antes de entrar a la vista
from django.contrib.auth.decorators import login_required
# login: inicia la sesión de un usuario · logout: la cierra
from django.contrib.auth import login, logout
# UserCreationForm: formulario de registro listo · AuthenticationForm: formulario de inicio de sesión listo
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
# messages: mensajes flash que se muestran una sola vez y luego desaparecen
from django.contrib import messages
# Servicio: el modelo de la Semana 4
from .models import Servicio
# ServicioForm: el ModelForm de la Semana 5
from .forms import ServicioForm
Vista de Registro
Baja hasta el final de core/views.py (debajo de eliminar_servicio), deja una línea en blanco y pega:
# registro: la vista que atiende /registro/ (recibe la petición del navegador)
def registro(request):
# si el usuario envió el formulario (botón «Registrarme»), la petición llega como POST
if request.method == 'POST':
# reconstruye el formulario con los datos tecleados (usuario y contraseñas)
form = UserCreationForm(request.POST)
# is_valid(): revisa que el usuario no exista y que las 2 contraseñas coincidan y sean seguras
if form.is_valid():
# save(): crea el User en MySQL, ya con la contraseña hasheada por Django
usuario_nuevo = form.save()
# login(): inicia sesión automáticamente con la cuenta recién creada
login(request, usuario_nuevo)
# messages.success: mensaje verde de bienvenida que se verá una sola vez en la siguiente página
messages.success(request, f'Cuenta creada. ¡Bienvenido, {usuario_nuevo.username}!')
# redirect: manda al usuario ya logueado al listado (name='servicios')
return redirect('servicios')
# si no es POST, es GET: el usuario apenas abrió la página
else:
# formulario vacío para cuando la vista se abre por primera vez (GET)
form = UserCreationForm()
# render: dibuja registro.html con el formulario (vacío o con los errores)
return render(request, 'core/registro.html', {'form': form})
Template de Registro con Tailwind
Clic derecho en core/templates/core/ → New File → registro.html, y pega. Usa el molde base.html de la Semana 5, así que ya trae Tailwind:
{# extends: la pantalla de registro usa el molde base.html de la Semana 5 (debe ser la primera etiqueta) #}
{% extends 'core/base.html' %}
{# block titulo: el texto de la pestaña del navegador #}
{% block titulo %}Crear cuenta{% endblock %}
{# block contenido: lo que va dentro del body de base.html #}
{% block contenido %}
{# flex + justify-center: centra la tarjeta a lo ancho de la página #}
<div class="flex justify-center">
{# bg-white + shadow: la "tarjeta" blanca del formulario, con sombra y esquinas redondeadas #}
<div class="bg-white shadow-md rounded-xl p-8 w-full max-w-sm border border-slate-200">
{# h1: título visible de la tarjeta #}
<h1 class="text-2xl font-bold mb-6">Crear cuenta</h1>
{# method="POST": envía los datos ocultos en la petición; space-y-4 separa los campos #}
<form method="POST" class="space-y-4">
{# csrf_token: obligatorio en todo formulario POST de Django #}
{% csrf_token %}
{# form.as_p: dibuja usuario, contraseña y confirmar contraseña con sus errores #}
{{ form.as_p }}
{# w-full: el botón ocupa todo el ancho de la tarjeta #}
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 text-white font-semibold py-2 rounded-lg">
{# texto visible dentro del botón #}
Registrarme
{# cierra el botón de enviar #}
</button>
{# cierra el formulario de registro #}
</form>
{# enlace para quien ya tiene cuenta: lleva a la ruta con name='login' #}
<p class="mt-4 text-sm">¿Ya tienes cuenta? <a href="{% url 'login' %}" class="text-blue-600 underline">Inicia sesión</a></p>
{# cierra la tarjeta blanca #}
</div>
{# cierra el contenedor que centra la tarjeta #}
</div>
{# endblock: fin del bloque contenido #}
{% endblock %}
Vista de Login
Otra vez al final de core/views.py, debajo de registro, pega:
# iniciar_sesion: la vista que atiende /login/
def iniciar_sesion(request):
# si el usuario envió usuario y contraseña, la petición llega como POST
if request.method == 'POST':
# AuthenticationForm necesita "request" además de los datos, para poder validar contra la BD
form = AuthenticationForm(request, data=request.POST)
# is_valid(): aquí Django comprueba que el usuario exista y la contraseña sea correcta
if form.is_valid():
# get_user(): devuelve el User ya verificado · login(): guarda la sesión en una cookie
login(request, form.get_user())
# messages.success: mensaje verde que confirma el inicio de sesión
messages.success(request, 'Sesión iniciada correctamente.')
# redirect: tras iniciar sesión, lo manda al listado de servicios
return redirect('servicios')
# si no es POST, es GET: el usuario apenas abrió la página de login
else:
# AuthenticationForm(): formulario vacío de usuario y contraseña
form = AuthenticationForm()
# render: dibuja login.html con el formulario (vacío o con el error de credenciales)
return render(request, 'core/login.html', {'form': form})
Template de Login
Es la misma tarjeta de registro.html con otro título, otro botón y el enlace al revés. Clic derecho en core/templates/core/ → New File → login.html, y pega:
{# extends: la pantalla de login usa el mismo molde base.html (debe ser la primera etiqueta) #}
{% extends 'core/base.html' %}
{# block titulo: el texto de la pestaña del navegador #}
{% block titulo %}Iniciar sesión{% endblock %}
{# block contenido: lo que va dentro del body de base.html #}
{% block contenido %}
{# flex + justify-center: centra la tarjeta a lo ancho de la página #}
<div class="flex justify-center">
{# bg-white + shadow: la misma tarjeta blanca que en registro.html #}
<div class="bg-white shadow-md rounded-xl p-8 w-full max-w-sm border border-slate-200">
{# h1: título visible de la tarjeta #}
<h1 class="text-2xl font-bold mb-6">Iniciar sesión</h1>
{# method="POST": el usuario y la contraseña viajan ocultos dentro de la petición #}
<form method="POST" class="space-y-4">
{# csrf_token: obligatorio en todo formulario POST de Django #}
{% csrf_token %}
{# form.as_p: dibuja usuario y contraseña; si fallan, muestra "usuario o contraseña incorrectos" #}
{{ form.as_p }}
{# w-full: el botón ocupa todo el ancho de la tarjeta #}
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 text-white font-semibold py-2 rounded-lg">
{# texto visible dentro del botón #}
Entrar
{# cierra el botón de enviar #}
</button>
{# cierra el formulario de login #}
</form>
{# enlace para quien todavía no tiene cuenta: lleva a la ruta con name='registro' #}
<p class="mt-4 text-sm">¿No tienes cuenta? <a href="{% url 'registro' %}" class="text-blue-600 underline">Regístrate</a></p>
{# cierra la tarjeta blanca #}
</div>
{# cierra el contenedor que centra la tarjeta #}
</div>
{# endblock: fin del bloque contenido #}
{% endblock %}
Recuerda la regla de la Semana 5: en plantillas, los comentarios van con {# … #}. Un <!-- --> que contenga {% %} rompe la página.
AuthenticationForm(request, data=request.POST): si olvidas pasar request como primer argumento, Django no puede aplicar sus protecciones extra (como frenar intentos repetidos de login) y el formulario falla.
@login_required, Logout y mensajes de sesión
Ya usaste @login_required en la Semana 5 sobre el login "prestado" de /admin. Ahora agregas las rutas, el Logout y mensajes flash de una sola vista (ej. "sesión iniciada con éxito").
Vista de Logout
Al final de core/views.py, debajo de iniciar_sesion, pega:
# cerrar_sesion: la vista que atiende /logout/
def cerrar_sesion(request):
# logout(request): invalida la sesión actual; request.user vuelve a ser AnonymousUser
logout(request)
# messages.info: guarda un mensaje que se muestra una única vez en la próxima página
messages.info(request, 'Sesión cerrada correctamente.')
# redirect: manda a la pantalla de login, donde se verá el mensaje
return redirect('login')
Registrar las 3 rutas nuevas
Abre core/urls.py, selecciona todo con Ctrl + A, bórralo y pega el archivo completo (son las rutas de la Semana 5 más registro, login y logout):
# path: función para declarar una ruta URL y a qué vista apunta
from django.urls import path
# views: el módulo con todas las funciones-vista de esta app
from . import views
# urlpatterns: la lista de rutas que Django revisa, en orden, para cada URL
urlpatterns = [
# '': la página de inicio de la Semana 3; no la borres
path('', views.inicio, name='inicio'),
# /servicios/: listado (Semana 3), ahora filtrado por usuario
path('servicios/', views.servicios, name='servicios'),
# /servicios/nuevo/: crear (Semana 5)
path('servicios/nuevo/', views.crear_servicio, name='crear_servicio'),
# /servicios/<id>/editar/: editar (Semana 5)
path('servicios/<int:pk>/editar/', views.editar_servicio, name='editar_servicio'),
# /servicios/<id>/eliminar/: eliminar con confirmación (Semana 5)
path('servicios/<int:pk>/eliminar/', views.eliminar_servicio, name='eliminar_servicio'),
# /registro/: NUEVA, pantalla de alta de cuenta
path('registro/', views.registro, name='registro'),
# /login/: NUEVA, pantalla de inicio de sesión propia (reemplaza al login prestado de /admin)
path('login/', views.iniciar_sesion, name='login'),
# /logout/: NUEVA, cierra la sesión activa
path('logout/', views.cerrar_sesion, name='logout'),
# cierra la lista urlpatterns
]
Configurar a dónde redirige el login
Abre miproyecto/settings.py y baja al final. Ahí están las 3 líneas que pegaste en la Semana 5 (2 comentarios y LOGIN_URL = '/admin/login/'). Bórralas y pega en su lugar estas 4, que apuntan a tu login propio:
# LOGIN_URL: a dónde manda @login_required a un visitante sin sesión; 'login' es el name= de tu ruta /login/
LOGIN_URL = 'login'
# LOGIN_REDIRECT_URL: a dónde va el usuario justo después de iniciar sesión con éxito
LOGIN_REDIRECT_URL = 'servicios'
Mostrar los mensajes flash y el estado de sesión en la plantilla base
Abre core/templates/core/base.html (el molde que creaste en la Semana 5), selecciona todo con Ctrl + A, bórralo y pega el archivo completo. Es el mismo molde con dos cosas nuevas: la barra de arriba (<nav>) con «Hola, usuario» / «Cerrar sesión» y la cajita verde de los mensajes.
{# base.html: el "molde" de la Semana 5, ahora con barra de sesión y mensajes flash #}
<!DOCTYPE html>
{# lang="es": el contenido de la página está en español #}
<html lang="es">
{# head: datos de la página que no se ven (título, acentos, estilos) #}
<head>
{# charset UTF-8: permite acentos y ñ #}
<meta charset="UTF-8">
{# viewport: hace que la página se adapte al ancho del celular #}
<meta name="viewport" content="width=device-width, initial-scale=1">
{# block titulo: cada página puede cambiar el texto de la pestaña; si no lo cambia, dice "Servicios" #}
<title>{% block titulo %}Servicios{% endblock %}</title>
{# Play CDN de Tailwind: el mismo que usaste en la Semana 2 #}
<script src="https://cdn.tailwindcss.com"></script>
{# style: reglas mínimas para que se vean los campos que dibuja form.as_p (Tailwind les quita el borde) #}
<style>
/* input, textarea, select: borde gris, esquinas redondeadas, relleno y ancho completo en cada campo */
input, textarea, select { border: 1px solid #94a3b8; border-radius: .5rem; padding: .4rem .6rem; width: 100%; }
/* label: cada etiqueta en su propia línea y en negritas */
label { display: block; font-weight: 600; margin-bottom: .2rem; }
/* .errorlist: la lista de errores que genera Django; se pinta en rojo */
.errorlist { color: #dc2626; font-size: .9rem; }
/* .helptext: los textos de ayuda de Django (ej. reglas de contraseña), en gris y pequeños */
.helptext { color: #64748b; font-size: .8rem; display: block; }
{# cierra el bloque de estilos #}
</style>
{# cierra el head #}
</head>
{# body: lo que se ve; max-w-2xl + mx-auto centran el contenido con un ancho máximo #}
<body class="max-w-2xl mx-auto py-10 px-6 bg-white text-slate-900">
{# nav: NUEVA barra superior; flex + justify-end alinea los enlaces a la derecha #}
<nav class="flex justify-end items-center gap-4 mb-6 text-sm">
{# if user.is_authenticated: True si hay sesión iniciada, False si el visitante es anónimo #}
{% if user.is_authenticated %}
{# user.username: nombre del usuario que inició sesión #}
<span>Hola, <b>{{ user.username }}</b></span>
{# enlace a la ruta con name='logout' para cerrar la sesión #}
<a href="{% url 'logout' %}" class="text-blue-600 underline">Cerrar sesión</a>
{# else: lo que ve un visitante SIN sesión iniciada #}
{% else %}
{# enlace a la ruta con name='login' #}
<a href="{% url 'login' %}" class="text-blue-600 underline">Iniciar sesión</a>
{# enlace a la ruta con name='registro' para crear una cuenta nueva #}
<a href="{% url 'registro' %}" class="text-blue-600 underline">Crear cuenta</a>
{# endif: fin del bloque con sesión / sin sesión #}
{% endif %}
{# cierra la barra superior #}
</nav>
{# for message in messages: recorre los mensajes guardados con messages.success() o messages.info() #}
{% for message in messages %}
{# cada mensaje se dibuja en una cajita verde (fondo emerald-100, texto emerald-800) #}
<div class="bg-emerald-100 text-emerald-800 px-4 py-2 rounded-lg mb-4">{{ message }}</div>
{# endfor: fin de la repetición de mensajes #}
{% endfor %}
{# block contenido: aquí cada página (listado, formularios, login, registro) pone lo suyo #}
{% block contenido %}{% endblock %}
{# cierra el body #}
</body>
{# cierra el documento HTML #}
</html>
user.is_authenticated funciona en cualquier plantilla sin necesidad de pasarlo desde la vista: Django lo agrega solo a todas las plantillas a través de un "context processor" que ya viene activado por defecto.
"Cada quien ve/edita solo lo suyo" — filtrar por usuario dueño
El campo propietario del modelo Servicio ya existe desde la Semana 4 (ForeignKey a User). Hasta ahora todas las vistas mostraban todos los registros; esta semana se filtra por el usuario que inició sesión.
Listar solo los servicios del usuario actual
En core/views.py busca la función def servicios(request):. Selecciona desde su comentario de arriba (# servicios: la vista del listado…) hasta su línea return, bórrala y pega en su lugar:
# @login_required: NUEVO aquí; si no hay sesión, manda a LOGIN_URL en lugar de ejecutar la vista
@login_required
# servicios: la vista del listado
def servicios(request):
# filter(propietario=request.user): SELECT ... WHERE propietario_id = <id del usuario logueado>
# reemplaza al .all() de la Semana 4, que traía los servicios de TODOS los usuarios
lista_servicios = Servicio.objects.filter(propietario=request.user)
# render: dibuja la tabla solo con los servicios del usuario actual
return render(request, 'core/servicios.html', {'servicios': lista_servicios})
Esta vista ahora también lleva @login_required: sin sesión iniciada no existe un request.user real para filtrar.
Impedir que alguien edite el servicio de otro usuario
Busca def editar_servicio(request, pk):. Selecciona desde el comentario # @login_required… que está justo arriba hasta su último return, bórralo y pega:
# @login_required: solo usuarios con sesión pueden editar
@login_required
# pk: el id que viene en la URL /servicios/<pk>/editar/
def editar_servicio(request, pk):
# get_object_or_404 con DOS condiciones: el id correcto Y que el dueño sea el usuario actual
# si alguien intenta editar el servicio de otro cambiando el número en la URL, recibe un 404
servicio = get_object_or_404(Servicio, pk=pk, propietario=request.user)
# si el usuario envió el formulario con cambios, la petición llega como POST
if request.method == 'POST':
# instance=servicio: el formulario actualiza ESE registro en vez de crear uno nuevo
form = ServicioForm(request.POST, instance=servicio)
# is_valid(): revisa tipos, campos obligatorios y tu clean_precio
if form.is_valid():
# save(): guarda los cambios en MySQL (UPDATE)
form.save()
# redirect: regresa al listado después de guardar
return redirect('servicios')
# si no es POST, es GET: se acaba de abrir la pantalla de edición
else:
# formulario precargado con los datos actuales del servicio
form = ServicioForm(instance=servicio)
# render: dibuja el mismo template de Crear, con título "Editar servicio"
return render(request, 'core/servicio_form.html', {'form': form, 'titulo_pagina': 'Editar servicio'})
Impedir que alguien borre el servicio de otro usuario
Mismo candado en eliminar_servicio. Busca def eliminar_servicio(request, pk):, selecciona desde el comentario # @login_required… de arriba hasta su último return, bórralo y pega:
# @login_required: solo usuarios con sesión iniciada pueden borrar
@login_required
# eliminar_servicio: recibe el pk (id) desde la URL /servicios/<pk>/eliminar/
def eliminar_servicio(request, pk):
# mismo candado que en editar: el id correcto Y que el dueño sea el usuario actual; si no, 404
servicio = get_object_or_404(Servicio, pk=pk, propietario=request.user)
# solo se borra si llega un POST (el botón de confirmar), nunca con un simple GET
if request.method == 'POST':
# delete(): borra la fila de MySQL de verdad
servicio.delete()
# redirect: regresa al listado, ya sin el registro borrado
return redirect('servicios')
# si es GET, solo muestra la pantalla de "¿Seguro?"; todavía no borra nada
return render(request, 'core/servicio_confirm_delete.html', {'servicio': servicio})
Compara: así debe quedar views.py completo
Si algo falla o no sabes si pegaste bien, compara tu archivo con este. Debe tener, en este orden: los import, inicio, servicios, crear_servicio, editar_servicio, eliminar_servicio, registro, iniciar_sesion y cerrar_sesion. También puedes reemplazar todo tu views.py por este bloque.
# render, redirect, get_object_or_404: los mismos atajos de Django que ya usabas en la Semana 5
from django.shortcuts import render, redirect, get_object_or_404
# connection: datos de la conexión a la base (lo usa la vista "inicio" de la Semana 3)
from django.db import connection
# login_required: decorador que exige sesión iniciada antes de entrar a la vista
from django.contrib.auth.decorators import login_required
# login: inicia la sesión de un usuario · logout: la cierra
from django.contrib.auth import login, logout
# UserCreationForm: formulario de registro listo · AuthenticationForm: formulario de inicio de sesión listo
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
# messages: mensajes flash que se muestran una sola vez y luego desaparecen
from django.contrib import messages
# Servicio: el modelo de la Semana 4
from .models import Servicio
# ServicioForm: el ModelForm de la Semana 5
from .forms import ServicioForm
# inicio: la vista de la Semana 3 que muestra a qué base estás conectado (no cambia)
def inicio(request):
# settings_dict: diccionario con ENGINE, HOST, NAME, etc. de la conexión activa
info_bd = connection.settings_dict
# contexto: los datos que la plantilla podrá usar
contexto = {
# motor: el tipo de base de datos (debe decir mysql)
'motor': info_bd['ENGINE'],
# host: el servidor de la base (en Aiven termina en aivencloud.com)
'host': info_bd['HOST'],
# cierre del diccionario contexto
}
# render: junta inicio.html con el contexto y devuelve la página
return render(request, 'core/inicio.html', contexto)
# @login_required: NUEVO aquí; si no hay sesión, manda a LOGIN_URL en lugar de ejecutar la vista
@login_required
# servicios: la vista del listado
def servicios(request):
# filter(propietario=request.user): SELECT ... WHERE propietario_id = <id del usuario logueado>
# reemplaza al .all() de la Semana 4, que traía los servicios de TODOS los usuarios
lista_servicios = Servicio.objects.filter(propietario=request.user)
# render: dibuja la tabla solo con los servicios del usuario actual
return render(request, 'core/servicios.html', {'servicios': lista_servicios})
# @login_required: si nadie inició sesión, Django lo manda a LOGIN_URL (el login de /admin)
@login_required
# crear_servicio: la vista que atiende /servicios/nuevo/
def crear_servicio(request):
# request.method: distingue si el navegador pide el formulario (GET) o lo está enviando (POST)
if request.method == 'POST':
# ServicioForm(request.POST): reconstruye el formulario con los datos que el usuario tecleó
form = ServicioForm(request.POST)
# is_valid(): corre todas las validaciones del modelo (tipos, max_length, campos obligatorios)
if form.is_valid():
# commit=False: crea el objeto Servicio en memoria SIN guardarlo todavía en MySQL
nuevo = form.save(commit=False)
# propietario: se asigna aquí, no en el formulario, para que nadie pueda falsificarlo
nuevo.propietario = request.user
# save(): ahora sí guarda el registro completo en MySQL
nuevo.save()
# redirect('servicios'): evita reenvíos duplicados si el usuario recarga la página
return redirect('servicios')
# si no es POST, es GET: el usuario apenas abrió la página
else:
# ServicioForm() sin datos: formulario vacío
form = ServicioForm()
# render: dibuja la plantilla, pasándole el formulario (vacío o con errores)
return render(request, 'core/servicio_form.html', {'form': form, 'titulo_pagina': 'Nuevo servicio'})
# @login_required: solo usuarios con sesión pueden editar
@login_required
# pk: el id que viene en la URL /servicios/<pk>/editar/
def editar_servicio(request, pk):
# get_object_or_404 con DOS condiciones: el id correcto Y que el dueño sea el usuario actual
# si alguien intenta editar el servicio de otro cambiando el número en la URL, recibe un 404
servicio = get_object_or_404(Servicio, pk=pk, propietario=request.user)
# si el usuario envió el formulario con cambios, la petición llega como POST
if request.method == 'POST':
# instance=servicio: el formulario actualiza ESE registro en vez de crear uno nuevo
form = ServicioForm(request.POST, instance=servicio)
# is_valid(): revisa tipos, campos obligatorios y tu clean_precio
if form.is_valid():
# save(): guarda los cambios en MySQL (UPDATE)
form.save()
# redirect: regresa al listado después de guardar
return redirect('servicios')
# si no es POST, es GET: se acaba de abrir la pantalla de edición
else:
# formulario precargado con los datos actuales del servicio
form = ServicioForm(instance=servicio)
# render: dibuja el mismo template de Crear, con título "Editar servicio"
return render(request, 'core/servicio_form.html', {'form': form, 'titulo_pagina': 'Editar servicio'})
# @login_required: solo usuarios con sesión iniciada pueden borrar
@login_required
# eliminar_servicio: recibe el pk (id) desde la URL /servicios/<pk>/eliminar/
def eliminar_servicio(request, pk):
# mismo candado que en editar: el id correcto Y que el dueño sea el usuario actual; si no, 404
servicio = get_object_or_404(Servicio, pk=pk, propietario=request.user)
# solo se borra si llega un POST (el botón de confirmar), nunca con un simple GET
if request.method == 'POST':
# delete(): borra la fila de MySQL de verdad
servicio.delete()
# redirect: regresa al listado, ya sin el registro borrado
return redirect('servicios')
# si es GET, solo muestra la pantalla de "¿Seguro?"; todavía no borra nada
return render(request, 'core/servicio_confirm_delete.html', {'servicio': servicio})
# registro: la vista que atiende /registro/ (recibe la petición del navegador)
def registro(request):
# si el usuario envió el formulario (botón «Registrarme»), la petición llega como POST
if request.method == 'POST':
# reconstruye el formulario con los datos tecleados (usuario y contraseñas)
form = UserCreationForm(request.POST)
# is_valid(): revisa que el usuario no exista y que las 2 contraseñas coincidan y sean seguras
if form.is_valid():
# save(): crea el User en MySQL, ya con la contraseña hasheada por Django
usuario_nuevo = form.save()
# login(): inicia sesión automáticamente con la cuenta recién creada
login(request, usuario_nuevo)
# messages.success: mensaje verde de bienvenida que se verá una sola vez en la siguiente página
messages.success(request, f'Cuenta creada. ¡Bienvenido, {usuario_nuevo.username}!')
# redirect: manda al usuario ya logueado al listado (name='servicios')
return redirect('servicios')
# si no es POST, es GET: el usuario apenas abrió la página
else:
# formulario vacío para cuando la vista se abre por primera vez (GET)
form = UserCreationForm()
# render: dibuja registro.html con el formulario (vacío o con los errores)
return render(request, 'core/registro.html', {'form': form})
# iniciar_sesion: la vista que atiende /login/
def iniciar_sesion(request):
# si el usuario envió usuario y contraseña, la petición llega como POST
if request.method == 'POST':
# AuthenticationForm necesita "request" además de los datos, para poder validar contra la BD
form = AuthenticationForm(request, data=request.POST)
# is_valid(): aquí Django comprueba que el usuario exista y la contraseña sea correcta
if form.is_valid():
# get_user(): devuelve el User ya verificado · login(): guarda la sesión en una cookie
login(request, form.get_user())
# messages.success: mensaje verde que confirma el inicio de sesión
messages.success(request, 'Sesión iniciada correctamente.')
# redirect: tras iniciar sesión, lo manda al listado de servicios
return redirect('servicios')
# si no es POST, es GET: el usuario apenas abrió la página de login
else:
# AuthenticationForm(): formulario vacío de usuario y contraseña
form = AuthenticationForm()
# render: dibuja login.html con el formulario (vacío o con el error de credenciales)
return render(request, 'core/login.html', {'form': form})
# cerrar_sesion: la vista que atiende /logout/
def cerrar_sesion(request):
# logout(request): invalida la sesión actual; request.user vuelve a ser AnonymousUser
logout(request)
# messages.info: guarda un mensaje que se muestra una única vez en la próxima página
messages.info(request, 'Sesión cerrada correctamente.')
# redirect: manda a la pantalla de login, donde se verá el mensaje
return redirect('login')
Probar todo en tu computadora antes de subir
En la terminal de VS Code, con (venv) activo y en la carpeta que tiene manage.py:
# Revisa que no haya errores de escritura en settings, urls, vistas ni modelos
python manage.py check
# Arranca el servidor en http://127.0.0.1:8000 (se detiene con Ctrl + C)
python manage.py runserver
- Abre una ventana de incógnito (Ctrl + Shift + N) en
http://127.0.0.1:8000/servicios/: como no hay sesión, debe mandarte a/login/con tu tarjeta de login. - Clic en «Regístrate». Crea la cuenta
prueba_localcon una contraseña de 8 o más caracteres que no sea solo números. Debes llegar a/servicios/con el mensaje verde de bienvenida, «Hola, prueba_local» arriba y la tabla con «Todavía no hay servicios». - Crea un servicio con «+ Nuevo servicio»: aparece solo ese, no los de tu superusuario.
- Clic en «Cerrar sesión»: vuelves a
/login/con el mensaje «Sesión cerrada correctamente.». - Entra con tu superusuario: ves sus servicios, pero no el de
prueba_local. - Detén el servidor con Ctrl + C.
| Error que ves | Causa | Solución |
|---|---|---|
NameError: name 'login' is not defined (u otro nombre) | No reemplazaste los import del paso 02.1 | Repite 02.1. |
AttributeError: module 'core.views' has no attribute 'registro' | Falta pegar una función al final de views.py | Compara con el views.py completo de arriba. |
Sigue saliendo el login de /admin | No cambiaste LOGIN_URL | Repite 03.3. |
TemplateSyntaxError | Comentario <!-- --> con {% %} adentro, o falta un {% endblock %} / {% endif %} | Compara la plantilla con la de la guía. |
| Al registrarte sale «La contraseña es demasiado similar…» o «demasiado común» | Django valida que la contraseña sea segura | No es un error de tu código: usa otra contraseña más larga y distinta del usuario. |
Sin este filtro, cualquier usuario con sesión iniciada podría escribir /servicios/7/editar/ a mano en la barra de direcciones y editar el servicio con id=7 aunque sea de otra persona. Esto se llama una vulnerabilidad de referencia insegura a objetos (IDOR) — es uno de los errores de seguridad más comunes en aplicaciones CRUD reales.
Checklist de seguridad y deploy final de la unidad
Antes de publicar, una revisión rápida de las reglas de seguridad más básicas de Django en producción — varias ya las configuraste en la Semana 4. Después subes todo a un repositorio nuevo u3s6-…, creas su Web Service en Render con sus 9 variables de entorno y pruebas registro, login y logout en producción.
Repaso: las 4 variables críticas de seguridad
De las 9 variables que cargarás en Render (05.6), estas 4 son las que protegen tu app. Ya las usaste en las Semanas 4 y 5; hoy importan más que nunca porque tu app ya tiene usuarios reales con contraseña y sesión.
# DEBUG en False: oculta el código fuente y las variables internas si ocurre un error
DEBUG=False
# SECRET_KEY distinta a la local: Django la usa para firmar cookies de sesión y tokens CSRF
SECRET_KEY=una-clave-larga-y-aleatoria-distinta-de-la-local
# ALLOWED_HOSTS con el dominio exacto (sin https://): evita ataques de "Host header" con dominios falsos
ALLOWED_HOSTS=u3s6-hernandez-nunez-maria-jose.onrender.com
# CSRF_TRUSTED_ORIGINS con https://: sin ella, Django rechaza (403) los formularios de registro y login
CSRF_TRUSTED_ORIGINS=https://u3s6-hernandez-nunez-maria-jose.onrender.com
Verificar que el .env nunca se subió a GitHub
# .env: excluye tu archivo de variables locales del repositorio
# si .env ya se subió antes por error, hay que borrarlo del repo y avisar al docente
.env
# confirma si .env aparece en el historial de Git (no debería aparecer ningún resultado)
git log --all --full-history -- .env
Revisar si hay migraciones pendientes (antes de subir)
Esta práctica no cambia el modelo: agrega vistas de registro/login/logout, templates, rutas y LOGIN_URL en settings.py. Las tablas de usuarios y sesiones (auth_user, django_session) existen desde la Semana 3 y el campo propietario desde la Semana 4, así que normalmente no hay migración nueva. Pero si por cualquier motivo tocaste core/models.py — agregaste un campo, cambiaste un max_length, pusiste unique=True… — entonces sí hay un cambio que MySQL necesita conocer. Compruébalo en la terminal de VS Code, con (venv) activo y dentro de la carpeta que tiene manage.py:
# Compara tus modelos con las migraciones guardadas; si responde "No changes detected", no hay nada que hacer
python manage.py makemigrations
# Solo si el comando anterior creó un archivo (ej. core/migrations/0002_...py): aplícalo en tu base de Aiven
python manage.py migrate
# Lista las migraciones de core: todas deben salir con [X] (aplicadas)
python manage.py showmigrations core
En el plan Free de Render no hay pestaña «Shell», así que nunca vas a escribir migrate dentro de Render. Lo hace solo porque está al final del Build Command (paso 05.5): en cada deploy corre python manage.py migrate. Para que funcione, el archivo nuevo de core/migrations/ tiene que subirse a GitHub junto con tu código (el git add . del paso 05.4 lo incluye; no lo pongas en .gitignore). Si no hubo cambios, el log de Render dirá No migrations to apply. y eso es correcto. Como tu compu y Render usan la misma base de Aiven, si ya corriste migrate en local, Render tampoco tendrá nada pendiente.
Crear el repositorio u3s6-… y subir los cambios
Cada práctica va en su propio repositorio y en su propio Web Service de Render, con el nombre u3s6-apellidopaterno-apellidomaterno-nombres (minúsculas, sin acentos, ñ → n, espacios → guion). Ej.: u3s6-hernandez-nunez-maria-jose. El repo de la Semana 5 (u3s5-…) se queda intacto como evidencia.
La regla del nombre · siempre igual (repo de GitHub y servicio de Render)
- Prefijo de la práctica:
u+ número de unidad +s+ número de semana. Esta práctica esu3s6. - Todo en minúsculas, sin acentos (
á → a), lañse escribeny los espacios se cambian por guion-. - Orden fijo: prefijo → apellido paterno → apellido materno → nombre(s). Si tienes dos nombres, van los dos.
- El mismo nombre se usa en dos lugares: como Repository name en GitHub y como Name del servicio en Render (paso 05.5). Así tu URL queda
https://u3s6-….onrender.com. - GitHub solo acepta letras sin acento, números,
-,_y.(máximo 100 caracteres). Por eso la regla quita acentos y espacios.
| Alumno | Nombre del repositorio y del servicio de Render |
|---|---|
| María José Hernández Núñez | u3s6-hernandez-nunez-maria-jose |
| José Ángel De la Cruz Peña | u3s6-de-la-cruz-pena-jose-angel |
| Ana López (un solo apellido) | u3s6-lopez-ana |
| Prefijo | Práctica del curso |
|---|---|
u1s1 | Unidad 1 · Semana 1 — Sitio estático en Render |
u1s2 | Unidad 1 · Semana 2 — Tailwind CSS |
u2s3 | Unidad 2 · Semana 3 — Django + MySQL en Aiven |
u2s4 | Unidad 2 · Semana 4 — Modelos y deploy en Render |
u3s5 | Unidad 3 · Semana 5 — CRUD con ModelForms |
u3s6 | Unidad 3 · Semana 6 — Autenticación y sesiones (esta guía) |
u4s7 | Unidad 4 · Semana 7 — Pulido y defensa |
u4s8 | Unidad 4 · Semana 8 — Deploy final |
Genera tu nombre sin errores
u3s6-hernandez-nunez-maria-joseTu repo quedará en https://github.com/TU_USUARIO/u3s6-hernandez-nunez-maria-jose y, con el mismo nombre en Render (paso 05.5), tu sitio en https://u3s6-hernandez-nunez-maria-jose.onrender.com. Los datos de ejemplo están precargados: escribe los tuyos y usa «Copiar nombre».
- Entra a
https://github.comy confirma arriba a la derecha que es tu cuenta. - Clic en + → New repository. En Repository name pega el nombre que copiaste del generador de arriba (
u3s6-…). - Déjalo en Public y no marques «Add a README», «.gitignore» ni «license» (el repo debe nacer vacío). Clic en Create repository y copia la URL que termina en
.git.
# Muestra a qué repo apunta hoy tu proyecto (debe salir el u3s5-… de la Semana 5)
git remote -v
# Cambia "origin" al repo nuevo de esta práctica; pega TU URL (set-url porque origin ya existe)
git remote set-url origin https://github.com/TU_USUARIO/u3s6-hernandez-nunez-maria-jose.git
# Confirma que ahora apunta al u3s6-… (sale dos veces: fetch y push)
git remote -v
# Lista lo que cambió: NO deben aparecer .env, venv/ ni staticfiles/
git status
# Agrega todos los archivos nuevos y modificados (incluye core/migrations/ si hubo migración)
git add .
# Crea el commit con un mensaje que describe el cambio
git commit -m "U3S6: registro, login, logout y aislamiento de datos por usuario"
# Sube la rama main al repo nuevo; -u la deja vinculada para que después baste con "git push"
git push -u origin main
Recarga la página del repo en GitHub: deben verse manage.py, requirements.txt, core/templates/core/registro.html y core/templates/core/login.html. Si ves .env, avisa al profesor antes de seguir (hay que cambiar la contraseña de Aiven).
Crear el Web Service en Render, campo por campo
- Abre
https://dashboard.render.come inicia sesión con GitHub. - Arriba a la derecha: + New → Web Service. No elijas «Static Site».
- En «Source Code» elige Git Provider, busca tu repo
u3s6-…(no elu3s5-…) y clic en Connect. - Si el repo no aparece: clic en Configure GitHub, en «Repository access» agrega
u3s6-…, guarda y regresa a Render.
Llena solo estos campos; el resto déjalo como viene:
| Campo | Qué escribir | Por qué |
|---|---|---|
| Name | El mismo nombre del repo, el que copiaste del generador (ej. u3s6-hernandez-nunez-maria-jose) | Se vuelve tu dirección: https://u3s6-hernandez-nunez-maria-jose.onrender.com. Anótala; la usas en 05.6. |
| Language | Python 3 | Render lo detecta por requirements.txt; si no, elígelo. |
| Branch | main | Cada git push a esta rama redespliega solo. |
| Region | La que venga (ej. Oregon (US West)) | Dónde está el servidor. Cualquiera sirve. |
| Root Directory | Vacío | Solo se llena si manage.py está en una subcarpeta (ej. backend). |
| Build Command | El del bloque de abajo | Se corre una vez en cada deploy: instala, junta estáticos y migra. |
| Start Command | El del bloque de abajo | Se queda corriendo siempre para atender visitas. |
| Instance Type | Free | Gratis; alcanza para el curso. |
# 1) instala requirements.txt 2) junta los estáticos para whitenoise 3) aplica migraciones en Aiven; "&&" = "si el anterior salió bien, sigue"
pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate
# arranca gunicorn con miproyecto/wsgi.py; cambia "miproyecto" por la carpeta que tiene settings.py
gunicorn miproyecto.wsgi
¿Qué va en lugar de miproyecto? En VS Code abre settings.py, busca con Ctrl + F WSGI_APPLICATION y copia lo que está antes de .application. Ej.: WSGI_APPLICATION = 'miproyecto.wsgi.application' → gunicorn miproyecto.wsgi. Es el mismo Start Command que usaste en las Semanas 4 y 5.
Las 9 variables de entorno: nombre, valor y de dónde sale
Render no tiene tu .env (no se sube a GitHub). En el mismo formulario baja a «Environment Variables». Los nombres (Key) deben escribirse exactamente así, en mayúsculas, porque settings.py los busca con config('NOMBRE'):
| Key (nombre exacto) | Value (qué debe tener) | De dónde lo sacas |
|---|---|---|
DB_NAME | Nombre de la base, normalmente defaultdb | Aiven → tu servicio MySQL → Overview → «Database name». Igual que en tu .env. |
DB_USER | Usuario de MySQL, normalmente avnadmin | Aiven → Overview → «User». Igual que en tu .env. |
DB_PASSWORD | La contraseña real de Aiven | Aiven → Overview → «Password» (icono del ojo). Igual que en tu .env. |
DB_HOST | El host, termina en .aivencloud.com, sin https:// | Aiven → Overview → «Host». Igual que en tu .env. |
DB_PORT | Un número de 5 dígitos (ej. 12345), no 3306 | Aiven → Overview → «Port». Igual que en tu .env. |
SECRET_KEY | Una clave larga nueva, distinta de la local | Se genera en la terminal de VS Code (bloque de abajo). No la reutilices. |
DEBUG | False (con F mayúscula) | En producción siempre False. En tu .env local va True. |
ALLOWED_HOSTS | u3s6-hernandez-nunez-maria-jose.onrender.com | Tu dominio de Render: sin https:// y sin / al final. |
CSRF_TRUSTED_ORIGINS | https://u3s6-hernandez-nunez-maria-jose.onrender.com | La dirección completa: con https:// y sin / al final. Sin ella, el registro, el login y los formularios del CRUD dan «403 CSRF». |
# Imprime una clave aleatoria de ~67 caracteres; cópiala completa y pégala como valor de SECRET_KEY en Render
python -c "import secrets; print(secrets.token_urlsafe(50))"
Para cargarlas tienes dos formas: rápida, clic en Add from .env, pega el bloque de abajo con tus valores y clic en Add variables; o manual, clic 9 veces en + Add Environment Variable escribiendo Key y Value.
# nombre de la base de Aiven (cópialo de tu .env local)
DB_NAME=defaultdb
# usuario de Aiven (cópialo de tu .env local)
DB_USER=avnadmin
# contraseña real de Aiven (cópiala de tu .env local)
DB_PASSWORD=tu-password-real-de-aiven
# host real de Aiven, sin https:// (cópialo de tu .env local)
DB_HOST=tu-host-real.aivencloud.com
# puerto real de Aiven (cópialo de tu .env local)
DB_PORT=12345
# clave NUEVA generada con el comando de arriba, solo para Render
SECRET_KEY=pega-aqui-la-clave-nueva
# en producción SIEMPRE False
DEBUG=False
# tu dominio de esta práctica: SIN https:// y SIN / al final
ALLOWED_HOSTS=u3s6-hernandez-nunez-maria-jose.onrender.com
# tu dirección completa: CON https:// y SIN / al final
CSRF_TRUSTED_ORIGINS=https://u3s6-hernandez-nunez-maria-jose.onrender.com
En «Add from .env» Render puede tomar las líneas que empiezan con # como comentarios, pero para no arriesgarte pega solo las 9 líneas NOMBRE=valor, sin los comentarios. Sin espacios antes ni después del =, y sin comillas.
Si el nombre ya estaba ocupado, Render le agrega letras (ej. u3s6-...-a1b2.onrender.com). Al terminar, copia la dirección real que aparece debajo del nombre del servicio y corrige ALLOWED_HOSTS y CSRF_TRUSTED_ORIGINS en la pestaña Environment → Edit → Save, rebuild, and deploy.
Desplegar y leer el log
Clic en Deploy Web Service. Tarda de 2 a 5 minutos. En el log debes ver, en este orden:
# 1. Render descarga tu repo u3s6-… de GitHub
==> Cloning from https://github.com/tu-usuario/u3s6-hernandez-nunez-maria-jose
# 2. empieza tu Build Command
==> Running build command 'pip install -r requirements.txt && ...'
# 3. pip instaló Django, PyMySQL, gunicorn, whitenoise, python-decouple...
Successfully installed Django-5.1.4 PyMySQL-1.1.1 gunicorn-23.0.0 ...
# 4. collectstatic juntó tus CSS/JS e imágenes
127 static files copied to '/opt/render/project/src/staticfiles', 381 post-processed.
# 5. migrate se conectó a Aiven; si no cambiaste modelos, no hay nada pendiente (correcto)
No migrations to apply.
# 6. el build terminó bien
==> Build successful 🎉
# 7. arranca tu Start Command
==> Running 'gunicorn miproyecto.wsgi'
# 8. el estado cambia a "Live" en verde: tu sitio ya responde
==> Your service is live 🎉
| Error en el log o en el navegador | Causa | Solución |
|---|---|---|
SECRET_KEY not found (u otra variable) | Falta una variable o el nombre tiene un error de dedo | Pestaña Environment: revisa las 9 y sus nombres exactos. |
ModuleNotFoundError: No module named 'miproyecto' | Start Command con otro nombre de carpeta | Settings → «Start Command»: corrígelo (ver 05.5). |
Can't connect to MySQL server / Access denied | Algún DB_ mal copiado o Aiven apagado | Compara con tu .env y revisa que Aiven diga «Running». |
Bad Request (400) al abrir la página | ALLOWED_HOSTS no coincide con tu dominio | Corrígelo sin https:// y sin /. |
Prohibido (403) · CSRF verification failed al guardar un formulario | Falta o está mal CSRF_TRUSTED_ORIGINS | Debe ser https://tu-dominio.onrender.com, sin / final. |
TemplateDoesNotExist: core/login.html | El template no se subió o tiene otro nombre | Revisa en GitHub la carpeta core/templates/core/. |
Table '...core_servicio' doesn't exist / Unknown column | Cambiaste el modelo y no se subió la migración | Repite 05.3 y sube core/migrations/ con git push. |
Al entrar a /servicios/ sin sesión manda a /accounts/login/ (404) | Falta LOGIN_URL en settings.py | Agrega LOGIN_URL = 'login' (paso 03) y haz git push. |
Después de corregir algo en Environment, Render redespliega solo. Si corregiste código, haz git push; o usa Manual Deploy → Deploy latest commit.
Confirmar HTTPS y probar la autenticación en producción
Render sirve HTTPS automáticamente en todos los planes, incluido el gratuito — confirma que la URL empieza con https:// y que el navegador muestra el candado, no un aviso de "no seguro".
- Abre
https://u3s6-tus-apellidos-nombres.onrender.com/servicios/en una ventana de incógnito (Ctrl + Shift + N): como no tienes sesión, debe mandarte a/login/. - Entra a
/registro/, crea una cuenta y confirma que te deja en/servicios/con el mensaje de bienvenida. - Clic en «Cerrar sesión» y vuelve a entrar desde
/login/con esa cuenta. - Agrega
/no-existe/al final de la URL: debe salir un «Not Found» simple, sin la página amarilla de Django (prueba de queDEBUG=False).
Este checklist de 5 puntos (DEBUG, SECRET_KEY, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, HTTPS) es exactamente lo primero que revisa cualquier defensa oral o auditoría real de seguridad de una app Django — vale la pena memorizarlo.
Práctica autónoma — comprobar el aislamiento con 2 cuentas de prueba
La mejor forma de confiar en que el filtro del paso 04 funciona de verdad es intentar romperlo tú mismo con dos cuentas reales.
Registrar dos cuentas desde /registro/ en producción
Ej. usuario_a y usuario_b, cada una con un correo y contraseña distintos.
Cargar servicios distintos con cada cuenta
Con sesión iniciada como usuario_a, crea 2 servicios. Cierra sesión, inicia como usuario_b y crea otros 2 servicios diferentes.
Confirmar que el listado no se mezcla
Con sesión en usuario_b, en /servicios/ solo deben aparecer sus 2 servicios, nunca los de usuario_a.
Intentar "saltarse" el aislamiento a propósito
Primero, con sesión en usuario_a, da clic en «Editar» de uno de sus servicios y anota el número que aparece en la barra de direcciones (ej. en /servicios/7/editar/ el id es 7). Cierra sesión, entra como usuario_b y escribe a mano en la barra de direcciones /servicios/<ese id>/editar/. Debe responder con un error 404, no con el formulario de edición.
Documentar la prueba en el README
Escribe en 3-4 líneas qué probaste y qué resultado obtuviste — esto es justamente la evidencia que se pide entregar esta semana.
Probar "romper" tu propia app a propósito (intentar acceder a algo que no debería ser tuyo) es una práctica real de seguridad llamada pruebas de control de acceso — mucho más confiable que solo revisar el código a simple vista.
Entregar y presentar
Marca cada punto a medida que lo completas — queda guardado en este navegador.
Cómo subir la práctica
Actualizar el README
<!-- # : título principal del README (lo primero que se ve en GitHub) -->
# Proyecto Django — [Tus Nombres y Apellidos]
<!-- ## : subtítulo de la sección con el enlace a producción -->
## Demo en vivo (produccion)
<!-- la URL pública de tu Web Service u3s6-… en Render -->
https://u3s6-hernandez-nunez-maria-jose.onrender.com
<!-- ## : subtítulo con lo que se hizo esta semana -->
## Semana 6 — Autenticacion, sesiones y seguridad
<!-- primera línea de la descripción de la práctica -->
Registro/login/logout propios; cada usuario ve y edita solo
<!-- segunda línea de la descripción de la práctica -->
sus propios servicios (probado con usuario_a y usuario_b).
<!-- ## : subtítulo de la sección con la prueba de aislamiento -->
## Prueba de aislamiento
<!-- reemplaza el texto entre corchetes por la descripción de tu prueba -->
[Describe aqui en 3-4 lineas la prueba del paso 06]
Las líneas <!-- … --> son comentarios de Markdown: GitHub no las muestra en la página del repo, así que puedes dejarlas o borrarlas.
# agrega solo el README modificado al área de preparación de Git
git add README.md
# crea el commit con un mensaje que explica el cambio
git commit -m "Actualizar README con avance de semana 6"
# sube el commit al repo u3s6-…; Render detecta el push y redespliega solo
git push
Entregar
Pega en la tarea de la Semana 6: el enlace del repo (ej. https://github.com/tu-usuario/u3s6-hernandez-nunez-maria-jose) y la URL pública de Render (ej. https://u3s6-hernandez-nunez-maria-jose.onrender.com/servicios/).
Cómo presentar en vivo — guion paso a paso
Un recorrido de ~5 minutos frente al profesor. Es el cierre de la Unidad 3: puede pedirse el live coding sugerido "crea un usuario nuevo y demuestra que no ve los datos de otro".
Registrar un usuario nuevo en vivo
Desde /registro/, crea una cuenta frente al profesor y muestra que queda logueado automáticamente.
Mostrar que el listado está vacío para esa cuenta nueva
Explica que Servicio.objects.filter(propietario=request.user) es lo que produce esa lista vacía.
Crear un servicio y cerrar sesión
Muestra que aparece en el listado de esa cuenta, y que desaparece del listado al cerrar sesión y entrar con otra cuenta.
Live coding sugerido: demostrar el 404 de acceso ajeno
Escribe a mano en la URL el id de un servicio de otra cuenta y muestra el 404, explicando la línea exacta de views.py que lo produce.
Repasar el checklist de seguridad
Muestra las variables de entorno en Render (sin revelar la SECRET_KEY completa) y explica qué protege cada una.
Tabla de referencias — de dónde sale toda la información
Todo lo de esta guía viene de documentación oficial de cada herramienta, no de blogs de terceros.
| Herramienta | Para qué | Dónde |
|---|---|---|
| Django — Authentication system | Modelo User, login(), logout(), sesiones | docs.djangoproject.com — Auth |
| Django — Password management | Cómo Django hashea y verifica contraseñas | docs.djangoproject.com — Passwords |
| Django — Built-in auth forms | UserCreationForm, AuthenticationForm |
docs.djangoproject.com — Auth forms |
| Django — login_required | Restringir vistas a usuarios con sesión | docs.djangoproject.com — login_required |
| Django — Messages framework | Mensajes flash de una sola vista | docs.djangoproject.com — Messages |
| Django — Deployment checklist | DEBUG, SECRET_KEY, ALLOWED_HOSTS y más antes de producción | docs.djangoproject.com — Deployment checklist |
| OWASP — Broken Access Control | Por qué filtrar por dueño en cada consulta (IDOR) | owasp.org — Broken Access Control |
| Render — Deploy Django | Web Service, Build Command y Start Command con gunicorn | render.com/docs — Deploy Django |
| Render — Environment Variables | Cargar las 9 variables a mano o con «Add from .env» | render.com/docs — Environment Variables |
| Django — Migrations | Cuándo hace falta makemigrations/migrate |
docs.djangoproject.com — Migrations |