APLICAWEB 4to/Propuesta Intensiva 8 Semanas/Unidad 3/Semana 6
4 hrs guiadas + 1 hr autónoma
semana
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.

Entregable: el repositorio 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.
01

django.contrib.auth: usuarios, hashing de contraseñas y sesiones

⏱ 0:00–0:40práctica guiada

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.

01

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.

02

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.

TIP

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.

02

Vistas de Registro y Login con formularios Tailwind

⏱ 0:40–1:40práctica guiada

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.

01

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.

core/views.py — REEMPLAZA las líneas de import del inicio del archivo
# 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
02

Vista de Registro

Baja hasta el final de core/views.py (debajo de eliminar_servicio), deja una línea en blanco y pega:

core/views.py — pega al FINAL del archivo
# 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})
03

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:

core/templates/core/registro.html — archivo NUEVO
{# 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 %}
04

Vista de Login

Otra vez al final de core/views.py, debajo de registro, pega:

core/views.py — pega al FINAL del archivo
# 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})
05

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:

core/templates/core/login.html — archivo NUEVO
{# 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 %}
TIP

Recuerda la regla de la Semana 5: en plantillas, los comentarios van con {# … #}. Un <!-- --> que contenga {% %} rompe la página.

OJO

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.

03

@login_required, Logout y mensajes de sesión

⏱ 1:40–2:20práctica guiada

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").

01

Vista de Logout

Al final de core/views.py, debajo de iniciar_sesion, pega:

core/views.py — pega al FINAL del archivo
# 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')
02

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):

core/urls.py — archivo COMPLETO (reemplaza todo)
# 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
]
03

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:

miproyecto/settings.py — REEMPLAZA el LOGIN_URL de la Semana 5
# 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'
04

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.

core/templates/core/base.html — archivo COMPLETO (reemplaza todo)
{# 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>
TIP

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.

04

"Cada quien ve/edita solo lo suyo" — filtrar por usuario dueño

⏱ 2:20–3:10práctica guiada

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.

01

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:

core/views.py — REEMPLAZA la función servicios
# @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.

02

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:

core/views.py — REEMPLAZA la función editar_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'})
03

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:

core/views.py — REEMPLAZA la función eliminar_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})
04

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.

core/views.py — archivo COMPLETO al terminar el paso 04 (para comparar)
# 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')
05

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:

TERMINAL DE VS CODE — PowerShell · con (venv) activo
# 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
  1. 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.
  2. Clic en «Regístrate». Crea la cuenta prueba_local con 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».
  3. Crea un servicio con «+ Nuevo servicio»: aparece solo ese, no los de tu superusuario.
  4. Clic en «Cerrar sesión»: vuelves a /login/ con el mensaje «Sesión cerrada correctamente.».
  5. Entra con tu superusuario: ves sus servicios, pero no el de prueba_local.
  6. Detén el servidor con Ctrl + C.
Error que vesCausaSolución
NameError: name 'login' is not defined (u otro nombre)No reemplazaste los import del paso 02.1Repite 02.1.
AttributeError: module 'core.views' has no attribute 'registro'Falta pegar una función al final de views.pyCompara con el views.py completo de arriba.
Sigue saliendo el login de /adminNo cambiaste LOGIN_URLRepite 03.3.
TemplateSyntaxErrorComentario <!-- --> 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 seguraNo es un error de tu código: usa otra contraseña más larga y distinta del usuario.
OJO

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.

05

Checklist de seguridad y deploy final de la unidad

⏱ 3:10–4:00práctica guiada

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.

05.1

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.

variables de entorno en Render
# 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
05.2

Verificar que el .env nunca se subió a GitHub

.gitignore
# .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
TERMINAL DE VS CODE — PowerShell
# confirma si .env aparece en el historial de Git (no debería aparecer ningún resultado)
git log --all --full-history -- .env
05.3

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:

TERMINAL DE VS CODE — PowerShell · con (venv) activo
# 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
NOTA · MIGRACIONES EN RENDER

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.

05.4

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)

prefijou3s6- apellido paternohernandez- apellido maternonunez- nombre(s)maria-jose
  • Prefijo de la práctica: u + número de unidad + s + número de semana. Esta práctica es u3s6.
  • Todo en minúsculas, sin acentos (á → a), la ñ se escribe n y 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.
AlumnoNombre del repositorio y del servicio de Render
María José Hernández Núñezu3s6-hernandez-nunez-maria-jose
José Ángel De la Cruz Peñau3s6-de-la-cruz-pena-jose-angel
Ana López (un solo apellido)u3s6-lopez-ana
PrefijoPráctica del curso
u1s1Unidad 1 · Semana 1 — Sitio estático en Render
u1s2Unidad 1 · Semana 2 — Tailwind CSS
u2s3Unidad 2 · Semana 3 — Django + MySQL en Aiven
u2s4Unidad 2 · Semana 4 — Modelos y deploy en Render
u3s5Unidad 3 · Semana 5 — CRUD con ModelForms
u3s6Unidad 3 · Semana 6 — Autenticación y sesiones (esta guía)
u4s7Unidad 4 · Semana 7 — Pulido y defensa
u4s8Unidad 4 · Semana 8 — Deploy final

Genera tu nombre sin errores

Repositorio y servicio de Renderu3s6-hernandez-nunez-maria-jose

Tu 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».

  1. Entra a https://github.com y confirma arriba a la derecha que es tu cuenta.
  2. Clic en + → New repository. En Repository name pega el nombre que copiaste del generador de arriba (u3s6-…).
  3. 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.
TERMINAL DE VS CODE — PowerShell · en la carpeta que tiene manage.py
# 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).

05.5

Crear el Web Service en Render, campo por campo

  1. Abre https://dashboard.render.com e inicia sesión con GitHub.
  2. Arriba a la derecha: + New → Web Service. No elijas «Static Site».
  3. En «Source Code» elige Git Provider, busca tu repo u3s6-… (no el u3s5-…) y clic en Connect.
  4. 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:

CampoQué escribirPor qué
NameEl 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.
LanguagePython 3Render lo detecta por requirements.txt; si no, elígelo.
BranchmainCada git push a esta rama redespliega solo.
RegionLa que venga (ej. Oregon (US West))Dónde está el servidor. Cualquiera sirve.
Root DirectoryVacíoSolo se llena si manage.py está en una subcarpeta (ej. backend).
Build CommandEl del bloque de abajoSe corre una vez en cada deploy: instala, junta estáticos y migra.
Start CommandEl del bloque de abajoSe queda corriendo siempre para atender visitas.
Instance TypeFreeGratis; alcanza para el curso.
RENDER — campo «Build Command» (en una sola línea)
# 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
RENDER — campo «Start Command»
# 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.

05.6

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_NAMENombre de la base, normalmente defaultdbAiven → tu servicio MySQL → Overview → «Database name». Igual que en tu .env.
DB_USERUsuario de MySQL, normalmente avnadminAiven → Overview → «User». Igual que en tu .env.
DB_PASSWORDLa contraseña real de AivenAiven → Overview → «Password» (icono del ojo). Igual que en tu .env.
DB_HOSTEl host, termina en .aivencloud.com, sin https://Aiven → Overview → «Host». Igual que en tu .env.
DB_PORTUn número de 5 dígitos (ej. 12345), no 3306Aiven → Overview → «Port». Igual que en tu .env.
SECRET_KEYUna clave larga nueva, distinta de la localSe genera en la terminal de VS Code (bloque de abajo). No la reutilices.
DEBUGFalse (con F mayúscula)En producción siempre False. En tu .env local va True.
ALLOWED_HOSTSu3s6-hernandez-nunez-maria-jose.onrender.comTu dominio de Render: sin https:// y sin / al final.
CSRF_TRUSTED_ORIGINShttps://u3s6-hernandez-nunez-maria-jose.onrender.comLa dirección completa: con https:// y sin / al final. Sin ella, el registro, el login y los formularios del CRUD dan «403 CSRF».
TERMINAL DE VS CODE — generar la SECRET_KEY de producción
# 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.

RENDER — «Add from .env» (cambia TODOS los valores por los tuyos)
# 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
OJO

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.

TIP

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.

05.7

Desplegar y leer el log

Clic en Deploy Web Service. Tarda de 2 a 5 minutos. En el log debes ver, en este orden:

LOG DE RENDER — lo que significa cada línea
# 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 navegadorCausaSolución
SECRET_KEY not found (u otra variable)Falta una variable o el nombre tiene un error de dedoPestaña Environment: revisa las 9 y sus nombres exactos.
ModuleNotFoundError: No module named 'miproyecto'Start Command con otro nombre de carpetaSettings → «Start Command»: corrígelo (ver 05.5).
Can't connect to MySQL server / Access deniedAlgún DB_ mal copiado o Aiven apagadoCompara con tu .env y revisa que Aiven diga «Running».
Bad Request (400) al abrir la páginaALLOWED_HOSTS no coincide con tu dominioCorrígelo sin https:// y sin /.
Prohibido (403) · CSRF verification failed al guardar un formularioFalta o está mal CSRF_TRUSTED_ORIGINSDebe ser https://tu-dominio.onrender.com, sin / final.
TemplateDoesNotExist: core/login.htmlEl template no se subió o tiene otro nombreRevisa en GitHub la carpeta core/templates/core/.
Table '...core_servicio' doesn't exist / Unknown columnCambiaste el modelo y no se subió la migraciónRepite 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.pyAgrega 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.

05.8

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".

  1. 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/.
  2. Entra a /registro/, crea una cuenta y confirma que te deja en /servicios/ con el mensaje de bienvenida.
  3. Clic en «Cerrar sesión» y vuelve a entrar desde /login/ con esa cuenta.
  4. Agrega /no-existe/ al final de la URL: debe salir un «Not Found» simple, sin la página amarilla de Django (prueba de que DEBUG=False).
TIP

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.

06

Práctica autónoma — comprobar el aislamiento con 2 cuentas de prueba

⏱ 1 hrpráctica autónoma

La mejor forma de confiar en que el filtro del paso 04 funciona de verdad es intentar romperlo tú mismo con dos cuentas reales.

01

Registrar dos cuentas desde /registro/ en producción

Ej. usuario_a y usuario_b, cada una con un correo y contraseña distintos.

02

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.

03

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.

04

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.

05

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.

TIP

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.

07

Entregar y presentar

Marca cada punto a medida que lo completas — queda guardado en este navegador.

0/8

Cómo subir la práctica

01

Actualizar el README

README.md
<!-- # : 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.

TERMINAL DE VS CODE — PowerShell
# 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
02

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".

0:00

Registrar un usuario nuevo en vivo

Desde /registro/, crea una cuenta frente al profesor y muestra que queda logueado automáticamente.

1:00

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.

2:00

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.

3:00

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.

4:00

Repasar el checklist de seguridad

Muestra las variables de entorno en Render (sin revelar la SECRET_KEY completa) y explica qué protege cada una.

08

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.

HerramientaPara 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