diff --git a/.gitignore b/.gitignore index 80704f4378..85ca896945 100755 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ dist/* !.eslintrc !.env.example .now +migrations/ # backend stuff .venv diff --git a/Pipfile b/Pipfile index 4d377014ae..5675b6c95f 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,6 @@ verify_ssl = true [dev-packages] [packages] -flask = "*" flask-sqlalchemy = "*" flask-migrate = "*" flask-swagger = "*" @@ -17,9 +16,11 @@ gunicorn = "*" cloudinary = "*" flask-admin = "==2.0.0" typing-extensions = "*" -flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask = "*" +flask-jwt-extended = "*" +flask-bcrypt = "*" [requires] python_version = "3.13" diff --git a/src/api/commands.py b/src/api/commands.py index 19806164d3..78ae930da0 100644 --- a/src/api/commands.py +++ b/src/api/commands.py @@ -11,7 +11,7 @@ def setup_commands(app): """ This is an example command "insert-test-users" that you can run from the command line - by typing: $ flask insert-test-users 5 + by typing: $ pipenv run flask insert-test-users 5 ---numero de tablas que quieras crear Note: 5 is the number of users to add """ @app.cli.command("insert-test-users") # name of our command diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..b047513a08 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -5,15 +5,21 @@ db = SQLAlchemy() class User(db.Model): + __tablename__= 'user' id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(90), unique=False, nullable=False) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) password: Mapped[str] = mapped_column(nullable=False) is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + def __repr__(self): + return f'Usuario {self.name}' + def serialize(self): return { "id": self.id, + "name": self.name, "email": self.email, # do not serialize the password, its a security breach } \ No newline at end of file diff --git a/src/app.py b/src/app.py index 1b3340c0fa..9d3c4300e5 100644 --- a/src/app.py +++ b/src/app.py @@ -6,10 +6,23 @@ from flask_migrate import Migrate from flask_swagger import swagger from api.utils import APIException, generate_sitemap -from api.models import db +from api.models import db, User from api.routes import api from api.admin import setup_admin from api.commands import setup_commands +from flask_cors import CORS +#from datetime import timedelta +#------import datetime para los refresh +from flask_jwt_extended import create_access_token +from flask_jwt_extended import get_jwt_identity +from flask_jwt_extended import jwt_required +from flask_jwt_extended import JWTManager +# Al importar Bcrypt se tiene que instalar la libreria con el comando \ +# : $ pip("pipenv" en este repo) install flask-bcrypt +from flask_bcrypt import Bcrypt +from sqlalchemy.exc import IntegrityError +#from flask_jwt_extended import create_refresh_token +#------import refresh------ # from models import Person @@ -19,6 +32,23 @@ app = Flask(__name__) app.url_map.strict_slashes = False +app.config["JWT_SECRET_KEY"] = os.getenv('JWT_KEY') + +bcrypt = Bcrypt(app) + +CORS(app) + +#ACCESS_MIN = int(os.getenv("JWT_ACCESS_TOKEN_EXPIRES_MIN", "60")) +#REFRESH_DAYS = int(os.getenv("JWT_REFRESH_TOKEN_EXPIRES_DAYS", "30")) +#---------------------------------------------- +# Pruevas para los refrsh tokens /\ \/ +#-------------------------------------------------- +#app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(minutes=ACCESS_MIN) +#app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=REFRESH_DAYS) + + +jwt = JWTManager(app) + # database condiguration db_url = os.getenv("DATABASE_URL") if db_url is not None: @@ -66,6 +96,96 @@ def serve_any_other_file(path): return response + + +@app.route('/api/login', methods=['POST']) +def login(): + body = request.get_json(silent=True) + if body is None: + return jsonify({'msg': ' Debes enviar informacion en el body'}), 400 + if 'email' not in body: + return jsonify({'msg': 'El campo email es obligatorio'}), 400 + if 'password' not in body: + return jsonify({'msg': ' El campo password es obligatorio'}), 400 + + user = User.query.filter_by(email=body['email']).first() + + # var = VarEnModels.query.filer_by(nombredelcampo=body['nombredelcampo']).first() + # para traer el usuario orphan en cascade + ##########!!!!! \/ + # user = User.query.filter_by(email=body['email'], password=body['password']).first() + # idea de Leon para ahorrarse el if de password!!! Idea alternativa + print(user) + if user is None: + return jsonify({'msg': 'Usuario o contraseña incorrecta'}), 400 + is_correct = bcrypt.check_password_hash(user.password, body['password']) + if is_correct == False: + return jsonify({'msg': 'Usuario o contraseña incorrecta'}), 400 + #if user.password != body['password']: + # return jsonify({'msg': 'Usuario o contraseña incorrecta'}), 400 + + acces_token = create_access_token(identity=user.email) + #refresh_token = create_refresh_token(identity=user.email) + #-----refresh token + #print(user) + return jsonify({'msg': 'Usuario logeado correctamente!', \ + 'token': acces_token}), 200 + #'refresh_token': refresh_token,------- + #'access_expires_minutes': ACCESS_MIN,------SOLO PRUEBAS REFRESH + #'refresh_expires_days': REFRESH_DAYS}), 200 ------ + + +@app.route('/api/register', methods=['POST']) +def register_user(): + body = request.get_json(silent=True) + if body is None: + return jsonify({'msg': 'Debes enviar informacion en el body'}), 400 + if 'email' not in body: + return jsonify({'msg': 'El campo email es obligatorio'}), 400 + if 'name' not in body: + return jsonify({'msg': 'Debes proporcionar un nombre'}), 400 + if 'password' not in body: + return jsonify({'msg': 'Debes proporcionar una contraseña'}), 400 + + new_register = User() # Otra opcion seria instanciar todo el body dentro del User \ + # así: new_register = User( email = body['email], name = body['name]...) \ + # En este caso lo instanciamos por separado + new_register.name = body['name'] + new_register.email = body['email'] + hash_password = bcrypt.generate_password_hash(body['password']).decode('utf-8') + #new_register.password = body['password'] + new_register.password = hash_password + + new_register.is_active = True + + try: + db.session.add(new_register) + db.session.commit() + + except IntegrityError: + db.session.rollback() + return jsonify({'msg': 'Ya existe un usuario con ese email'}), 400 + + + return jsonify({'msg': 'Usuario registrado!', 'register': new_register.serialize()}), 200 + + +#-----ENDPOINTS PROTEGIDOS \/ +@app.route('/api/private', methods=['GET']) +@jwt_required() +def privado(): + current_user_email = get_jwt_identity() + #print(current_user) + current_user = User.query.filter_by(email=current_user_email).first() + #----Para autorizar por primera vez un token en Postman /headers -> //crear key// -> Authorization y //Value -> Bearer (espacio) nuevo token + #---Una vez autorizado -> /Authorization/Bearer Token/ poner el token + if current_user is None: + return jsonify({'msg': 'Usuario no encontrado'}), 404 + + return jsonify({ + 'msg': 'Gracias por probar que estas logeado', + 'name': current_user.name}), 200 + # this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) diff --git a/src/front/assets/img/BG4j.gif b/src/front/assets/img/BG4j.gif new file mode 100644 index 0000000000..0e16310bf0 Binary files /dev/null and b/src/front/assets/img/BG4j.gif differ diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..fd3f45663f 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,46 @@ -import { Link } from "react-router-dom"; +import React from "react"; +import { Link, useNavigate } from "react-router-dom"; -export const Navbar = () => { +export default function Navbar() { + const navigate = useNavigate(); + const token = sessionStorage.getItem("token"); - return ( - - ); -}; \ No newline at end of file + const handleLogout = () => { + sessionStorage.removeItem("token"); + navigate("/login", { replace: true }); + }; + + return ( + + ); +} diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx index 9bfa31325c..8706dab9b7 100644 --- a/src/front/pages/Layout.jsx +++ b/src/front/pages/Layout.jsx @@ -1,15 +1,15 @@ -import { Outlet } from "react-router-dom/dist" -import ScrollToTop from "../components/ScrollToTop" -import { Navbar } from "../components/Navbar" -import { Footer } from "../components/Footer" +import { Outlet } from "react-router-dom"; +//import ScrollToTop from "../components/ScrollToTop" +import Navbar from "../components/Navbar"; +//import { Footer } from "../components/Footer" // Base component that maintains the navbar and footer throughout the page and the scroll to top functionality. export const Layout = () => { - return ( - - - -