Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ typing-extensions = "*"
flask-jwt-extended = "*"
wtforms = "==3.1.2"
flask-bcrypt = "*"
flask-mail = "*"

[requires]
python_version = "3.10"
Expand Down
10 changes: 9 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""empty message

Revision ID: 3388104734f6
Revision ID: dda1fcda5948
Revises:
Create Date: 2025-03-17 11:09:47.021713
Create Date: 2025-03-18 16:04:42.679001

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '3388104734f6'
revision = 'dda1fcda5948'
down_revision = None
branch_labels = None
depends_on = None
Expand Down Expand Up @@ -44,11 +44,20 @@ def upgrade():
sa.Column('url', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('password_reset',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('token', sa.String(length=255), nullable=False),
sa.Column('expires_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('token')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=False),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('password', sa.String(length=80), nullable=False),
sa.Column('is_temporary_password', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
Expand Down Expand Up @@ -82,6 +91,7 @@ def downgrade():
op.drop_table('pet')
op.drop_table('order')
op.drop_table('user')
op.drop_table('password_reset')
op.drop_table('foods')
op.drop_table('accessories')
# ### end Alembic commands ###
21 changes: 20 additions & 1 deletion src/api/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import ForeignKey

from datetime import datetime



Expand All @@ -27,6 +27,25 @@ def serialize(self):
"name": self.name
# do not serialize the password, its a security breach
}

class PasswordReset(db.Model):
__tablename__ = 'password_reset'

id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), nullable=False)
token = db.Column(db.String(255), nullable=False, unique=True)
expires_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)

def serialize(self):
return {
"id": self.id,
"email": self.email,
"token": self.token,
"expires_at": self.expires_at.strftime("%Y-%m-%d %H:%M:%S")
}

def __repr__(self):
return f'<PasswordReset {self.email}>'


class Food(db.Model):
Expand Down
33 changes: 32 additions & 1 deletion src/api/routes.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""
This module takes care of starting the API Server, Loading the DB and Adding the endpoints
"""
from flask import Flask, request, jsonify, url_for, Blueprint
from flask import Flask, request, jsonify, url_for, Blueprint, current_app
from api.models import db, User, Food, Pet, Accessories, Order
from api.utils import generate_sitemap, APIException
from flask_cors import CORS
from sqlalchemy import select, and_, or_
import json
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from flask_bcrypt import Bcrypt
from flask_mail import Message
import random, string


api = Blueprint('api', __name__)
bcrypt = Bcrypt()
Expand All @@ -27,6 +30,34 @@

# return jsonify(response_body), 200

@api.route("/forgotpassword", methods=["POST"])
def forgotpassword():
recover_email = request.json.get('email')

user = User.query.filter_by(email=recover_email).first()
if not user:
return jsonify({"msg": "El correo ingresado no existe en nuestros registros"}), 400

# Genera una nueva contraseña temporal (aleatoria)
recover_password = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(12))

# Guardar la nueva contraseña cifrada (muy importante)
bcrypt = Bcrypt()
hashed_password = bcrypt.generate_password_hash(recover_password).decode('utf-8')
user.password = hashed_password
db.session.commit()

# Enviar la contraseña temporal al correo del usuario
from flask_mail import Message
msg = Message(
subject="Recuperación de contraseña",
sender=current_app.config["MAIL_USERNAME"],
recipients=[recover_email],
body=f"Tu nueva contraseña temporal es: {recover_password}. Por favor inicia sesión y cámbiala inmediatamente."
)
current_app.mail.send(msg)

return jsonify({"msg": "Se ha enviado una nueva contraseña a tu correo electrónico"}), 200

@api.route('/')
def sitemap():
Expand Down
50 changes: 30 additions & 20 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@
import os
from flask import Flask, request, jsonify, url_for, send_from_directory
from flask_migrate import Migrate
from flask_swagger import swagger
from api.utils import APIException, generate_sitemap
from api.models import db
from api.routes import api
from api.admin import setup_admin
from api.commands import setup_commands
from flask_jwt_extended import JWTManager
from flask_bcrypt import Bcrypt
from flask_jwt_extended import JWTManager
from flask_bcrypt import Bcrypt
from datetime import timedelta

# from models import Person
from flask_mail import Mail

ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production"
static_file_dir = os.path.join(os.path.dirname(
os.path.realpath(__file__)), '../public/')

app = Flask(__name__)
app.url_map.strict_slashes = False

# database condiguration
# configuración base de datos
db_url = os.getenv("DATABASE_URL")
if db_url is not None:
app.config['SQLALCHEMY_DATABASE_URI'] = db_url.replace(
Expand All @@ -34,47 +33,58 @@
MIGRATE = Migrate(app, db, compare_type=True)
db.init_app(app)

app.config["JWT_ACCESS_TOKEN_EXPIRE"] = timedelta(hours=1)
# JWT y Bcrypt
app.config["JWT_SECRET_KEY"] = "cualquiercosa"
app.config["JWT_ACCESS_TOKEN_EXPIRE"] = timedelta(hours=1)
jwt = JWTManager(app)
bcrypt = Bcrypt(app)

# Configuración correcta y real de Flask-Mail 👇 (ajusta tus credenciales aquí)
mail_settings = {
"MAIL_SERVER": 'smtp.gmail.com',
"MAIL_PORT": 465,
"MAIL_USE_TLS": False,
"MAIL_USE_SSL": True,
"MAIL_USERNAME": '[email protected]', # el nuevo correo Gmail creado
"MAIL_PASSWORD": 'kxrv widw xqhr frgi' # la contraseña que generaste
}

app.config.update(mail_settings)
mail = Mail(app)
app.mail = mail


# add the admin
setup_admin(app)

# add the admin
# add custom commands
setup_commands(app)

# Add all endpoints form the API with a "api" prefix
# Registra todas tus rutas API
app.register_blueprint(api, url_prefix='/api')

# Handle/serialize errors like a JSON object


# Manejo de errores
@app.errorhandler(APIException)
def handle_invalid_usage(error):
return jsonify(error.to_dict()), error.status_code

# generate sitemap with all your endpoints


# Sitemap
@app.route('/')
def sitemap():
if ENV == "development":
return generate_sitemap(app)
return send_from_directory(static_file_dir, 'index.html')

# any other endpoint will try to serve it like a static file
# Cualquier otra ruta intenta servir un archivo estático
@app.route('/<path:path>', methods=['GET'])
def serve_any_other_file(path):
if not os.path.isfile(os.path.join(static_file_dir, path)):
path = 'index.html'
response = send_from_directory(static_file_dir, path)
response.cache_control.max_age = 0 # avoid cache memory
response.cache_control.max_age = 0
return response


# this only runs if `$ python src/main.py` is executed
# Arranca el servidor Flask
if __name__ == '__main__':
PORT = int(os.environ.get('PORT', 3001))
app.run(host='0.0.0.0', port=PORT, debug=True)
app.run(host='0.0.0.0', port=PORT, debug=True)
46 changes: 46 additions & 0 deletions src/front/js/component/recuperacionContraseña.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; // ✅ Importamos useNavigate

export const RecuperacionContraseña = () => {
const [email, setEmail] = useState('');
const navigate = useNavigate(); // ✅ Inicializamos el hook de navegación

const handleSubmit = async (e) => {
e.preventDefault();

try {
const response = await fetch(`${process.env.BACKEND_URL}/api/forgotpassword`, { // ✅ Asegúrate de que esta ruta es correcta
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});

const data = await response.json();

if (response.ok) {
alert("Correo enviado correctamente. Revisa tu bandeja de entrada."); // ✅ Alert con el mensaje
navigate("/loginSignup"); // ✅ Redirigir al inicio de sesión después de aceptar el alert
} else {
alert(`Error: ${data.msg}`);
}
} catch (error) {
alert("Error de conexión con el servidor.");
}
};

return (
<div>
<h2>Recuperar contraseña</h2>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Introduce tu correo electrónico"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit">Enviar</button>
</form>
</div>
);
};
3 changes: 2 additions & 1 deletion src/front/js/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import injectContext from "./store/appContext";

import { Navbar } from "./component/navbar";
import { Footer } from "./component/footer";
import { RecuperacionContraseña } from "./component/recuperacionContraseña";
import { VistaMascota } from "./pages/vistaMascota";
import { VistaProducto } from "./pages/VistaProducto";

import { LoginSignup } from "./pages/loginSignup";
import { RegistroMascota } from "./pages/RegistroMascota";
import { CarritoPago } from "./pages/CarritoPago";
Expand Down Expand Up @@ -58,6 +58,7 @@ const PageWithNavbar = () => {
<Route element={<Single />} path="/single/:theid" />
<Route element={<RegistroMascota />} path="/registro-mascota" />
<Route element={<CarritoPago />} path="/carrito" />
<Route element={<RecuperacionContraseña />} path="/recuperacionContraseña" />
<Route element={<h1>Not found!</h1>} path="*" />
</Routes>
</>
Expand Down
18 changes: 13 additions & 5 deletions src/front/js/pages/loginSignup.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "../../styles/home.css";
export const LoginSignup = () => {
const { actions, store } = useContext(Context);
const navigate = useNavigate();

const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
Expand All @@ -20,15 +20,15 @@ export const LoginSignup = () => {
setLoading(true);
setError('');
setSuccessMessage('');

try {
const dataUser = { name, email, password };
if (isSignup) {
await actions.signup(dataUser, navigate);
setSuccessMessage('¡Registro exitoso! Redirigiendo a inicio de sesión...');
setTimeout(() => {
setIsSignup(false);
}, 3000);
},);
} else {
await actions.login(email, password, navigate);
}
Expand All @@ -41,7 +41,7 @@ export const LoginSignup = () => {
setLoading(false);
}
};

return (
<div className="registration-view-container" style={{ background: "linear-gradient(to bottom, #FCE5CD, #FFFFFF)" }}>
<div className="form-container">
Expand All @@ -67,6 +67,14 @@ export const LoginSignup = () => {
</button>
{error && <p className="error-message">{error}</p>}
<p onClick={() => setIsSignup(true)}>¿No tienes cuenta? <strong>Regístrate</strong></p>

{/* NUEVO: Enlace para recuperación de contraseña */}
<p>
¿Olvidaste tu contraseña? <Link to="/RecuperacionContraseña">Recupérala aquí</Link>
</p>
<p>
<Link to="/">Volver</Link>
</p>
</form>
)}
{isSignup && (
Expand Down Expand Up @@ -104,4 +112,4 @@ export const LoginSignup = () => {
</div>
</div>
);
};
};