password recuperation scripts
Browse files- app/main.py +100 -5
- app/templates/forgot_password.html +25 -0
- app/templates/login.html +4 -0
- app/templates/register.html +6 -0
- app/templates/reset_password.html +28 -0
app/main.py
CHANGED
|
@@ -62,9 +62,9 @@ class UserManager:
|
|
| 62 |
with open(self.filename, "w") as f:
|
| 63 |
json.dump(self.users, f, indent=4)
|
| 64 |
|
| 65 |
-
def add_user(self, username, password, status="PENDING"):
|
| 66 |
-
"""Registra un nuevo usuario con contraseña hasheada y estado pendiente."""
|
| 67 |
-
if self.get_by_username(username):
|
| 68 |
return False
|
| 69 |
|
| 70 |
user_id = str(uuid.uuid4())
|
|
@@ -72,8 +72,11 @@ class UserManager:
|
|
| 72 |
self.users.append({
|
| 73 |
"id": user_id,
|
| 74 |
"username": username,
|
|
|
|
| 75 |
"password": hashed_pw,
|
| 76 |
-
"status": status
|
|
|
|
|
|
|
| 77 |
})
|
| 78 |
self.save()
|
| 79 |
return True
|
|
@@ -100,6 +103,13 @@ class UserManager:
|
|
| 100 |
return user
|
| 101 |
return None
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
def get_by_id(self, user_id):
|
| 104 |
"""Busca un usuario por su ID."""
|
| 105 |
for user in self.users:
|
|
@@ -107,6 +117,40 @@ class UserManager:
|
|
| 107 |
return user
|
| 108 |
return None
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
user_mgr = UserManager()
|
| 111 |
|
| 112 |
@login_manager.user_loader
|
|
@@ -335,6 +379,7 @@ def register():
|
|
| 335 |
|
| 336 |
if request.method == 'POST':
|
| 337 |
username = request.form.get('username')
|
|
|
|
| 338 |
password = request.form.get('password')
|
| 339 |
confirm_password = request.form.get('confirm_password')
|
| 340 |
|
|
@@ -342,7 +387,7 @@ def register():
|
|
| 342 |
flash("Las contraseñas no coinciden", "red")
|
| 343 |
return render_template('register.html')
|
| 344 |
|
| 345 |
-
if user_mgr.add_user(username, password, status="PENDING"):
|
| 346 |
# Notificar a Telegram sobre el nuevo registro
|
| 347 |
if bot and TG_CHAT_ID:
|
| 348 |
try:
|
|
@@ -370,6 +415,56 @@ def logout():
|
|
| 370 |
flash("Has cerrado sesión", "blue")
|
| 371 |
return redirect(url_for('index'))
|
| 372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
@app.route('/prestamos')
|
| 374 |
def prestamos():
|
| 375 |
loans = loan_mgr.get_all()
|
|
|
|
| 62 |
with open(self.filename, "w") as f:
|
| 63 |
json.dump(self.users, f, indent=4)
|
| 64 |
|
| 65 |
+
def add_user(self, username, password, email, status="PENDING"):
|
| 66 |
+
"""Registra un nuevo usuario con email, contraseña hasheada y estado pendiente."""
|
| 67 |
+
if self.get_by_username(username) or self.get_by_email(email):
|
| 68 |
return False
|
| 69 |
|
| 70 |
user_id = str(uuid.uuid4())
|
|
|
|
| 72 |
self.users.append({
|
| 73 |
"id": user_id,
|
| 74 |
"username": username,
|
| 75 |
+
"email": email,
|
| 76 |
"password": hashed_pw,
|
| 77 |
+
"status": status,
|
| 78 |
+
"reset_token": None,
|
| 79 |
+
"reset_expiry": None
|
| 80 |
})
|
| 81 |
self.save()
|
| 82 |
return True
|
|
|
|
| 103 |
return user
|
| 104 |
return None
|
| 105 |
|
| 106 |
+
def get_by_email(self, email):
|
| 107 |
+
"""Busca un usuario por su email."""
|
| 108 |
+
for user in self.users:
|
| 109 |
+
if user.get("email") == email:
|
| 110 |
+
return user
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
def get_by_id(self, user_id):
|
| 114 |
"""Busca un usuario por su ID."""
|
| 115 |
for user in self.users:
|
|
|
|
| 117 |
return user
|
| 118 |
return None
|
| 119 |
|
| 120 |
+
def generate_reset_token(self, email):
|
| 121 |
+
"""Genera un token de recuperación de contraseña."""
|
| 122 |
+
user = self.get_by_email(email)
|
| 123 |
+
if user:
|
| 124 |
+
token = str(uuid.uuid4())
|
| 125 |
+
expiry = (datetime.datetime.now() + datetime.timedelta(hours=1)).isoformat()
|
| 126 |
+
user["reset_token"] = token
|
| 127 |
+
user["reset_expiry"] = expiry
|
| 128 |
+
self.save()
|
| 129 |
+
return token
|
| 130 |
+
return None
|
| 131 |
+
|
| 132 |
+
def verify_reset_token(self, token):
|
| 133 |
+
"""Verifica un token de recuperación y retorna el usuario si es válido."""
|
| 134 |
+
for user in self.users:
|
| 135 |
+
if user.get("reset_token") == token:
|
| 136 |
+
expiry_str = user.get("reset_expiry")
|
| 137 |
+
if expiry_str:
|
| 138 |
+
expiry = datetime.datetime.fromisoformat(expiry_str)
|
| 139 |
+
if datetime.datetime.now() < expiry:
|
| 140 |
+
return user
|
| 141 |
+
return None
|
| 142 |
+
|
| 143 |
+
def update_password(self, user_id, new_password):
|
| 144 |
+
"""Actualiza la contraseña de un usuario y limpia el token."""
|
| 145 |
+
user = self.get_by_id(user_id)
|
| 146 |
+
if user:
|
| 147 |
+
user["password"] = generate_password_hash(new_password)
|
| 148 |
+
user["reset_token"] = None
|
| 149 |
+
user["reset_expiry"] = None
|
| 150 |
+
self.save()
|
| 151 |
+
return True
|
| 152 |
+
return False
|
| 153 |
+
|
| 154 |
user_mgr = UserManager()
|
| 155 |
|
| 156 |
@login_manager.user_loader
|
|
|
|
| 379 |
|
| 380 |
if request.method == 'POST':
|
| 381 |
username = request.form.get('username')
|
| 382 |
+
email = request.form.get('email')
|
| 383 |
password = request.form.get('password')
|
| 384 |
confirm_password = request.form.get('confirm_password')
|
| 385 |
|
|
|
|
| 387 |
flash("Las contraseñas no coinciden", "red")
|
| 388 |
return render_template('register.html')
|
| 389 |
|
| 390 |
+
if user_mgr.add_user(username, password, email, status="PENDING"):
|
| 391 |
# Notificar a Telegram sobre el nuevo registro
|
| 392 |
if bot and TG_CHAT_ID:
|
| 393 |
try:
|
|
|
|
| 415 |
flash("Has cerrado sesión", "blue")
|
| 416 |
return redirect(url_for('index'))
|
| 417 |
|
| 418 |
+
@app.route('/forgot-password', methods=['GET', 'POST'])
|
| 419 |
+
def forgot_password():
|
| 420 |
+
if current_user.is_authenticated:
|
| 421 |
+
return redirect(url_for('repos'))
|
| 422 |
+
|
| 423 |
+
if request.method == 'POST':
|
| 424 |
+
email = request.form.get('email')
|
| 425 |
+
token = user_mgr.generate_reset_token(email)
|
| 426 |
+
|
| 427 |
+
if token:
|
| 428 |
+
reset_url = url_for('reset_password', token=token, _external=True)
|
| 429 |
+
# Simulación de envío de correo
|
| 430 |
+
print(f"\n[EMAIL SIMULATION] Para: {email}")
|
| 431 |
+
print(f"[EMAIL SIMULATION] Enlace de recuperación: {reset_url}\n")
|
| 432 |
+
|
| 433 |
+
flash("Si el correo está registrado, recibirás un enlace de recuperación.", "blue")
|
| 434 |
+
else:
|
| 435 |
+
# Por seguridad, no revelamos si el email existe
|
| 436 |
+
flash("Si el correo está registrado, recibirás un enlace de recuperación.", "blue")
|
| 437 |
+
|
| 438 |
+
return redirect(url_for('login'))
|
| 439 |
+
|
| 440 |
+
return render_template('forgot_password.html', title="Recuperar Contraseña")
|
| 441 |
+
|
| 442 |
+
@app.route('/reset-password/<token>', methods=['GET', 'POST'])
|
| 443 |
+
def reset_password(token):
|
| 444 |
+
if current_user.is_authenticated:
|
| 445 |
+
return redirect(url_for('repos'))
|
| 446 |
+
|
| 447 |
+
user = user_mgr.verify_reset_token(token)
|
| 448 |
+
if not user:
|
| 449 |
+
flash("El enlace de recuperación es inválido o ha expirado.", "red")
|
| 450 |
+
return redirect(url_for('forgot_password'))
|
| 451 |
+
|
| 452 |
+
if request.method == 'POST':
|
| 453 |
+
password = request.form.get('password')
|
| 454 |
+
confirm_password = request.form.get('confirm_password')
|
| 455 |
+
|
| 456 |
+
if password != confirm_password:
|
| 457 |
+
flash("Las contraseñas no coinciden", "red")
|
| 458 |
+
return render_template('reset_password.html', title="Nueva Contraseña")
|
| 459 |
+
|
| 460 |
+
if user_mgr.update_password(user['id'], password):
|
| 461 |
+
flash("Contraseña actualizada correctamente. Ya puedes iniciar sesión.", "green")
|
| 462 |
+
return redirect(url_for('login'))
|
| 463 |
+
else:
|
| 464 |
+
flash("Error al actualizar la contraseña.", "red")
|
| 465 |
+
|
| 466 |
+
return render_template('reset_password.html', title="Nueva Contraseña")
|
| 467 |
+
|
| 468 |
@app.route('/prestamos')
|
| 469 |
def prestamos():
|
| 470 |
loans = loan_mgr.get_all()
|
app/templates/forgot_password.html
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="hero section" style="text-align: center; margin-bottom: 3rem;">
|
| 5 |
+
<h1>RECUPERAR CONTRASEÑA</h1>
|
| 6 |
+
<p class="text-dim">Ingresa tu correo electrónico para recibir un enlace de recuperación.</p>
|
| 7 |
+
</div>
|
| 8 |
+
|
| 9 |
+
<div class="glass" style="max-width: 400px; margin: 0 auto; padding: 2.5rem; border-radius: 24px;">
|
| 10 |
+
<form method="POST">
|
| 11 |
+
<div class="form-group">
|
| 12 |
+
<label for="email">Correo Electrónico</label>
|
| 13 |
+
<input type="email" id="email" name="email" placeholder="tu@email.com" required autofocus>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;">
|
| 17 |
+
ENVIAR ENLACE <i class="fas fa-paper-plane" style="margin-left: 0.5rem;"></i>
|
| 18 |
+
</button>
|
| 19 |
+
</form>
|
| 20 |
+
|
| 21 |
+
<div style="margin-top: 2rem; text-align: center; font-size: 0.85rem;" class="text-dim">
|
| 22 |
+
<p><a href="/login" style="color: #38bdf8; text-decoration: none;">Volver al inicio de sesión</a></p>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
{% endblock %}
|
app/templates/login.html
CHANGED
|
@@ -19,6 +19,10 @@
|
|
| 19 |
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;">
|
| 20 |
ENTRAR <i class="fas fa-sign-in-alt" style="margin-left: 0.5rem;"></i>
|
| 21 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
</form>
|
| 23 |
|
| 24 |
<div style="margin-top: 2rem; text-align: center; font-size: 0.85rem;" class="text-dim">
|
|
|
|
| 19 |
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;">
|
| 20 |
ENTRAR <i class="fas fa-sign-in-alt" style="margin-left: 0.5rem;"></i>
|
| 21 |
</button>
|
| 22 |
+
<div style="margin-top: 1rem; text-align: center;">
|
| 23 |
+
<a href="/forgot-password" style="color: #38bdf8; text-decoration: none; font-size: 0.85rem;">¿Olvidaste tu
|
| 24 |
+
contraseña?</a>
|
| 25 |
+
</div>
|
| 26 |
</form>
|
| 27 |
|
| 28 |
<div style="margin-top: 2rem; text-align: center; font-size: 0.85rem;" class="text-dim">
|
app/templates/register.html
CHANGED
|
@@ -16,6 +16,12 @@
|
|
| 16 |
<input type="text" id="username" name="username" placeholder="Elige un usuario" required autofocus>
|
| 17 |
</div>
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
<!-- Campo: Contraseña (Mínimo 8 caracteres por seguridad) -->
|
| 20 |
<div class="form-group">
|
| 21 |
<label for="password">Contraseña</label>
|
|
|
|
| 16 |
<input type="text" id="username" name="username" placeholder="Elige un usuario" required autofocus>
|
| 17 |
</div>
|
| 18 |
|
| 19 |
+
<!-- Campo: Correo Electrónico -->
|
| 20 |
+
<div class="form-group">
|
| 21 |
+
<label for="email">Correo Electrónico</label>
|
| 22 |
+
<input type="email" id="email" name="email" placeholder="tu@email.com" required>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
<!-- Campo: Contraseña (Mínimo 8 caracteres por seguridad) -->
|
| 26 |
<div class="form-group">
|
| 27 |
<label for="password">Contraseña</label>
|
app/templates/reset_password.html
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="hero section" style="text-align: center; margin-bottom: 3rem;">
|
| 5 |
+
<h1>NUEVA CONTRASEÑA</h1>
|
| 6 |
+
<p class="text-dim">Ingresa tu nueva contraseña para acceder a Maker Space.</p>
|
| 7 |
+
</div>
|
| 8 |
+
|
| 9 |
+
<div class="glass" style="max-width: 400px; margin: 0 auto; padding: 2.5rem; border-radius: 24px;">
|
| 10 |
+
<form method="POST">
|
| 11 |
+
<div class="form-group">
|
| 12 |
+
<label for="password">Nueva Contraseña</label>
|
| 13 |
+
<input type="password" id="password" name="password" placeholder="Mínimo 8 caracteres" required
|
| 14 |
+
minlength="8" autofocus>
|
| 15 |
+
</div>
|
| 16 |
+
|
| 17 |
+
<div class="form-group">
|
| 18 |
+
<label for="confirm_password">Confirmar Contraseña</label>
|
| 19 |
+
<input type="password" id="confirm_password" name="confirm_password" placeholder="Repite tu contraseña"
|
| 20 |
+
required minlength="8">
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;">
|
| 24 |
+
REABLECER CONTRASEÑA <i class="fas fa-key" style="margin-left: 0.5rem;"></i>
|
| 25 |
+
</button>
|
| 26 |
+
</form>
|
| 27 |
+
</div>
|
| 28 |
+
{% endblock %}
|