Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ dist/*
!.eslintrc
!.env.example
.now
migrations/

# backend stuff
.venv
Expand Down
5 changes: 3 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ verify_ssl = true
[dev-packages]

[packages]
flask = "*"
flask-sqlalchemy = "*"
flask-migrate = "*"
flask-swagger = "*"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/api/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
122 changes: 121 additions & 1 deletion src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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))
Expand Down
Binary file added src/front/assets/img/BG4j.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 44 additions & 17 deletions src/front/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className="navbar navbar-light bg-light">
<div className="container">
<Link to="/">
<span className="navbar-brand mb-0 h1">React Boilerplate</span>
</Link>
<div className="ml-auto">
<Link to="/demo">
<button className="btn btn-primary">Check the Context in action</button>
</Link>
</div>
</div>
</nav>
);
};
const handleLogout = () => {
sessionStorage.removeItem("token");
navigate("/login", { replace: true });
};

return (
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div className="container">
<Link className="navbar-brand" to="/">JWT Autentification</Link>

<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#mainNavbar"
aria-controls="mainNavbar"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon" />
</button>

<div className="collapse navbar-collapse" id="mainNavbar">
<div className="ms-auto d-flex align-items-center">
{!token && (
<>
<Link className="btn btn-outline-light me-2" to="/register">Registrate</Link>
<Link className="btn btn-outline-light me-2" to="/login">Login</Link>
</>
)}
{token && (
<button className="btn btn-light" onClick={handleLogout}>Cerrar sesión</button>
)}
</div>
</div>
</div>
</nav>
);
}
24 changes: 12 additions & 12 deletions src/front/pages/Layout.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<ScrollToTop>
<Navbar />
<Outlet />
<Footer />
</ScrollToTop>
)
}
return (
<>
<Navbar />
<Outlet />

</>
);
};
98 changes: 98 additions & 0 deletions src/front/pages/Login.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";


export default function Login() {

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const navigate = useNavigate();

const backendUrl = import.meta.env.VITE_BACKEND_URL;

const handleLogin = async (e) => {
e.preventDefault(),
setError("");

try {
const resp = await fetch(
`${backendUrl}/api/login`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
}
);

const data = await resp.json();


if (resp.ok && data.token) {
sessionStorage.setItem("token", data.token);
navigate("/private");
} else {
setError(data.msg || "Credenciales incorrectas");
}

} catch {
setError("Error de red");
}
};

return (
<div className="container py-4">
<div className="row justify-content-center">
<div className="col-12 col-sm-10 col-md-6 col-lg-4">
<div className="card shadow-sm">
<div className="card-body">
<h4 className="card-title mb-3 text-center">Iniciar sesión</h4>

<form onSubmit={handleLogin}>
<div className="mb-3">
<label className="form-label" for="email" >Email</label><br />
<input
className="form-control"
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label className="form-label" for="password">Password</label>
<input
className="form-control"
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className="d-grid">
<button className="mt-2 btn btn-primary" type="submit">Inicia sesión</button>
</div>
</form>

<div className="card-footer text-center bg-white">
<small className="text-muted">¿No tienes cuenta?
<a href="#" onClick={(e) => {
e.preventDefault();
navigate("/register");
}
}>Regístrate</a>
</small>
</div>
{error && <div style={{ color: "red" }}>{error}</div>}
</div>
</div>
</div>
</div>
</div>

);
}

Loading