diff --git a/Pipfile b/Pipfile
index 44e04f14ff..a9f8a4983a 100644
--- a/Pipfile
+++ b/Pipfile
@@ -20,6 +20,7 @@ typing-extensions = "*"
flask-jwt-extended = "==4.6.0"
wtforms = "==3.1.2"
sqlalchemy = "*"
+flask-bcrypt = "*"
[requires]
python_version = "3.13"
diff --git a/Pipfile.lock b/Pipfile.lock
index b201c3decc..aa26182cc9 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5"
+ "sha256": "660e3c19b0c84670819deeffe92b04973852c5f06f34d9be89a2fb2b1bb8534a"
},
"pipfile-spec": 6,
"requires": {
@@ -24,6 +24,63 @@
"markers": "python_version >= '3.8'",
"version": "==1.14.1"
},
+ "bcrypt": {
+ "hashes": [
+ "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f",
+ "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d",
+ "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24",
+ "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3",
+ "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c",
+ "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d",
+ "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd",
+ "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f",
+ "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f",
+ "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d",
+ "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe",
+ "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231",
+ "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef",
+ "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18",
+ "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f",
+ "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e",
+ "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732",
+ "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304",
+ "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0",
+ "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8",
+ "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938",
+ "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62",
+ "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180",
+ "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af",
+ "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669",
+ "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761",
+ "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51",
+ "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23",
+ "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09",
+ "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505",
+ "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4",
+ "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753",
+ "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59",
+ "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b",
+ "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d",
+ "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a",
+ "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b",
+ "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a",
+ "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90",
+ "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492",
+ "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce",
+ "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb",
+ "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb",
+ "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1",
+ "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676",
+ "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b",
+ "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe",
+ "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281",
+ "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1",
+ "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef",
+ "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.3.0"
+ },
"blinker": {
"hashes": [
"sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf",
@@ -42,11 +99,11 @@
},
"click": {
"hashes": [
- "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
- "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
+ "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202",
+ "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"
],
- "markers": "python_version >= '3.7'",
- "version": "==8.1.8"
+ "markers": "python_version >= '3.10'",
+ "version": "==8.2.1"
},
"cloudinary": {
"hashes": [
@@ -58,11 +115,12 @@
},
"flask": {
"hashes": [
- "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac",
- "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"
+ "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c",
+ "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"
],
"index": "pypi",
- "version": "==3.1.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.1.1"
},
"flask-admin": {
"hashes": [
@@ -72,6 +130,14 @@
"index": "pypi",
"version": "==1.6.1"
},
+ "flask-bcrypt": {
+ "hashes": [
+ "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a",
+ "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369"
+ ],
+ "index": "pypi",
+ "version": "==1.0.1"
+ },
"flask-cors": {
"hashes": [
"sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c",
@@ -209,11 +275,11 @@
},
"jinja2": {
"hashes": [
- "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb",
- "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"
+ "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
+ "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
],
"markers": "python_version >= '3.7'",
- "version": "==3.1.5"
+ "version": "==3.1.6"
},
"mako": {
"hashes": [
diff --git a/migrations/versions/4517993f5dfe_.py b/migrations/versions/4517993f5dfe_.py
new file mode 100644
index 0000000000..f4404db8b7
--- /dev/null
+++ b/migrations/versions/4517993f5dfe_.py
@@ -0,0 +1,41 @@
+"""empty message
+
+Revision ID: 4517993f5dfe
+Revises: 727b004dfad3
+Create Date: 2025-07-25 15:48:44.313778
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '4517993f5dfe'
+down_revision = '727b004dfad3'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('admin',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('email', sa.String(length=120), nullable=False),
+ sa.Column('password', sa.String(length=128), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('email')
+ )
+ op.create_table('token_blocked_list',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('jti', sa.String(length=50), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('jti')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('token_blocked_list')
+ op.drop_table('admin')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/876cb350dd08_.py b/migrations/versions/876cb350dd08_.py
new file mode 100644
index 0000000000..a37ba2ae01
--- /dev/null
+++ b/migrations/versions/876cb350dd08_.py
@@ -0,0 +1,42 @@
+"""empty message
+
+Revision ID: 876cb350dd08
+Revises: 4517993f5dfe
+Create Date: 2025-07-25 16:00:47.587108
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '876cb350dd08'
+down_revision = '4517993f5dfe'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('ct_admin',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('email', sa.String(length=120), nullable=False),
+ sa.Column('password', sa.String(length=128), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('email')
+ )
+ op.drop_table('admin')
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('admin',
+ sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
+ sa.Column('email', sa.VARCHAR(length=120), autoincrement=False, nullable=False),
+ sa.Column('password', sa.VARCHAR(length=128), autoincrement=False, nullable=False),
+ sa.PrimaryKeyConstraint('id', name='admin_pkey'),
+ sa.UniqueConstraint('email', name='admin_email_key')
+ )
+ op.drop_table('ct_admin')
+ # ### end Alembic commands ###
diff --git a/public/index.html b/public/index.html
index 9462644fe9..97fd77c5c5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1 +1,35 @@
-
Hello Rigo with Vanilla.js
\ No newline at end of file
+
+
+
+
+
+ CloudTech
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/rigo-baby.jpg b/public/rigo-baby.jpg
deleted file mode 100644
index da566a74a0..0000000000
Binary files a/public/rigo-baby.jpg and /dev/null differ
diff --git a/src/api/admin.py b/src/api/admin.py
index d312ff7990..c05ef5be2f 100644
--- a/src/api/admin.py
+++ b/src/api/admin.py
@@ -1,17 +1,36 @@
-
+
import os
from flask_admin import Admin
-from .models import db, Lead
+from .models import db, Lead, CTAdmin, TokenBlockedList
from flask_admin.contrib.sqla import ModelView
+from wtforms import PasswordField
+
+
+class CTAdminModelView(ModelView):
+ column_list = ('id', 'email')
+ form_columns = ('email', 'password_field')
+ column_exclude_list = ['_password']
+ form_extra_fields = {
+ 'password_field': PasswordField('Password')
+ }
+
+ def on_model_change(self, form, model, is_created):
+ if form.password_field.data:
+ model.password = form.password_field.data
+ elif is_created and not form.password_field.data:
+ raise ValueError(
+ 'El password es obligatorio para nuevos administradores')
+
def setup_admin(app):
app.secret_key = os.environ.get('FLASK_APP_KEY', 'sample key')
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
- admin = Admin(app, name='4Geeks Admin', template_mode='bootstrap3')
+ admin = Admin(app, name='CloudTech Admin', template_mode='bootstrap3')
-
# Add your models here, for example this is how we add a the User model to the admin
admin.add_view(ModelView(Lead, db.session))
+ admin.add_view(CTAdminModelView(CTAdmin, db.session))
+ admin.add_view(ModelView(TokenBlockedList, db.session))
# You can duplicate that line to add mew models
- # admin.add_view(ModelView(YourModelName, db.session))
\ No newline at end of file
+ # admin.add_view(ModelView(YourModelName, db.session))
diff --git a/src/api/models.py b/src/api/models.py
index 68dcb82cfd..16c780cc5f 100644
--- a/src/api/models.py
+++ b/src/api/models.py
@@ -5,6 +5,35 @@
db = SQLAlchemy()
+class CTAdmin(db.Model):
+ id: Mapped[int] = mapped_column(primary_key=True)
+ email: Mapped[str] = mapped_column(
+ String(120), unique=True, nullable=False)
+ _password: Mapped[str] = mapped_column(
+ "password", String(128), nullable=False)
+
+ @property
+ def password(self):
+ raise AttributeError('Password is not a readable attribute.')
+
+ @password.setter
+ def password(self, password):
+ from app import bcrypt
+ self._password = bcrypt.generate_password_hash(
+ password).decode('utf-8')
+
+ # Método para verificar el password
+ def check_password(self, password):
+ from app import bcrypt
+ return bcrypt.check_password_hash(self._password, password)
+
+ def serialize(self):
+ return {
+ "id": self.id,
+ "email": self.email
+ }
+
+
class Lead(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(
@@ -25,3 +54,11 @@ def serialize(self):
"company": self.company,
"message": self.message
}
+
+
+class TokenBlockedList(db.Model):
+ id: Mapped[int] = mapped_column(primary_key=True)
+ jti: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
+
+ def __repr__(self):
+ return f''
diff --git a/src/api/routes.py b/src/api/routes.py
index da7a2296b4..ba2d39559f 100644
--- a/src/api/routes.py
+++ b/src/api/routes.py
@@ -2,10 +2,12 @@
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 api.models import db, Lead
+from api.models import db, Lead, CTAdmin
from api.utils import generate_sitemap, APIException
+from sqlalchemy import select
from flask_cors import CORS
from sqlalchemy.exc import IntegrityError
+from flask_jwt_extended import create_access_token
api = Blueprint('api', __name__)
@@ -52,6 +54,49 @@ def validate_lead_data(data):
}
+@api.route('/admin/login', methods=['POST'])
+def admin_login():
+ admin_data = request.get_json()
+
+ if not admin_data:
+ return jsonify({"message": "Invalid Json or empty request body"}), 400
+
+ email = admin_data.get("email")
+ password = admin_data.get("password")
+
+ if not email:
+ return jsonify({"message": "No email entered"}), 400
+ if not password:
+ return jsonify({"message": "Password is required"}), 400
+
+ ct_admin = None
+
+ try:
+ ct_admin = db.session.execute(select(CTAdmin).where(
+ CTAdmin.email == email)).scalar_one_or_none()
+
+ if ct_admin is None:
+ return jsonify({"message": "Invalid credentials"}), 401
+
+ if not ct_admin.check_password(password):
+ return jsonify({"message": "Invalid credentials"}), 401
+
+ token = create_access_token(
+ identity=ct_admin.id,
+ additional_claims={"role": "ct_admin"}
+ )
+
+ return jsonify({
+ "token": token,
+ "user_id": ct_admin.id,
+ "message": "Login successful"
+ }), 200
+
+ except Exception as e:
+ print(f"Login error: {e}")
+ return jsonify({"message": "Failed login. Please try again later"}), 500
+
+
@api.route('/contact', methods=['POST'])
def add_lead():
lead_data = request.get_json()
diff --git a/src/app.py b/src/app.py
index ca25ac026e..f09503b59a 100644
--- a/src/app.py
+++ b/src/app.py
@@ -5,6 +5,8 @@
from flask import Flask, request, jsonify, url_for, send_from_directory
from flask_migrate import Migrate
from flask_swagger import swagger
+from flask_bcrypt import Bcrypt
+from flask_jwt_extended import JWTManager
from api.utils import APIException, generate_sitemap
from api.models import db
from api.routes import api
@@ -17,6 +19,9 @@
static_file_dir = os.path.join(os.path.dirname(
os.path.realpath(__file__)), '../dist/')
app = Flask(__name__)
+bcrypt = Bcrypt(app)
+jwt = JWTManager(app)
+app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY", "super-secret-jwt-key")
app.url_map.strict_slashes = False
# database condiguration
@@ -42,11 +47,13 @@
# Handle/serialize errors like a JSON object
+
@app.before_request
def handle_options_request():
if request.method == 'OPTIONS':
return '', 204
+
@app.errorhandler(APIException)
def handle_invalid_usage(error):
return jsonify(error.to_dict()), error.status_code
@@ -61,6 +68,8 @@ def sitemap():
return send_from_directory(static_file_dir, 'index.html')
# any other endpoint will try to serve it like a static file
+
+
@app.route('/', methods=['GET'])
def serve_any_other_file(path):
if not os.path.isfile(os.path.join(static_file_dir, path)):
diff --git a/src/front/index.css b/src/front/index.css
index d8498c7c60..8b8dd4f5b4 100644
--- a/src/front/index.css
+++ b/src/front/index.css
@@ -296,7 +296,7 @@ h6 {
background-color: rgba(10, 25, 30, 0.5);
}
-.hero-title-home{
+.hero-title-home {
color: #fbff06 !important;
}
@@ -345,7 +345,7 @@ h6 {
color: transparent;
}
/* header contacto */
-.header{
+.header {
margin-bottom: 4rem !important;
}
.form-label-contact {
@@ -381,3 +381,15 @@ h6 {
.adminOption:hover {
color: #fbff06;
}
+
+/* Login styles */
+
+@media (max-width: 780px) {
+ .login-logo {
+ min-width: 280px;
+ }
+}
+
+.login-logo {
+ min-width: 200px;
+}
diff --git a/src/front/main.jsx b/src/front/main.jsx
index 6f413bf07c..15c01ecd4d 100644
--- a/src/front/main.jsx
+++ b/src/front/main.jsx
@@ -17,7 +17,21 @@ const Main = () => {
);
return (
- Loading language...}>
+
+
+ }>
{/* Provide global state to all components */}
{/* Set up routing for the application */}
diff --git a/src/front/pages/Admin.jsx b/src/front/pages/Admin.jsx
index dbd9543d3f..959c7b0b0d 100644
--- a/src/front/pages/Admin.jsx
+++ b/src/front/pages/Admin.jsx
@@ -1,9 +1,10 @@
import { useState, useEffect, useContext } from "react"
-import { Link } from "react-router-dom"
+import { Link, useNavigate } from "react-router-dom"
import { Leads } from "../components/Admin/Leads"
import { AppContext } from "./Layout"
export const Admin = () => {
+ const navigate = useNavigate();
const { setShowNavbar, setShowFooter } = useContext(AppContext)
const [activeContent, setActiveContent] = useState("");
@@ -24,6 +25,11 @@ export const Admin = () => {
setActiveContent(contentName);
}
+ const handleLogout = () => {
+ localStorage.removeItem('accessToken')
+ navigate('/')
+ }
+
const renderContent = () => {
switch (activeContent) {
case 'leads':
@@ -36,7 +42,7 @@ export const Admin = () => {
return (
-
+
Panel de Administrador
¡Bienvenido!
@@ -53,6 +59,14 @@ export const Admin = () => {
+
+
+ Cerrar sesión
+
+
{renderContent()}
diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx
new file mode 100644
index 0000000000..eb919ca2f2
--- /dev/null
+++ b/src/front/pages/Login.jsx
@@ -0,0 +1,134 @@
+import { useState, useEffect, useReducer, useContext } from "react"
+import storeReducer, { initialStore } from "../store";
+import { useNavigate } from "react-router-dom"
+import { AppContext } from "./Layout"
+import yellowLogo from '../assets/img/LogoNavbar.svg'
+
+const adminLogin = async (dispatch, loginData) => {
+ const apiUrl = import.meta.env.VITE_BACKEND_URL;
+ dispatch({ type: 'ADMIN_LOGIN_START' })
+ try {
+ const response = await fetch(`${apiUrl}/api/admin/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(loginData)
+ })
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || "Error al loguearse");
+ }
+ dispatch({ type: 'ADMIN_LOGIN_SUCCESS', payload: data });
+ return data;
+ } catch (error) {
+ console.error("Error al loguearse:", error);
+ dispatch({ type: 'ADMIN_LOGIN_FAILURE', payload: error.message });
+ throw error;
+ }
+}
+
+export const Login = () => {
+ const navigate = useNavigate();
+ const { setShowNavbar, setShowFooter } = useContext(AppContext)
+
+ const [store, dispatch] = useReducer(storeReducer, initialStore());
+ const { loginStatus } = store;
+
+ useEffect(() => {
+ if (setShowNavbar || setShowFooter) {
+ setShowNavbar(false);
+ setShowFooter(false);
+ }
+ return () => {
+ if (setShowNavbar || setShowFooter) {
+ setShowNavbar(true);
+ setShowFooter(true);
+ }
+ }
+ }, [setShowNavbar, setShowFooter])
+
+ const [loginData, setLoginData] = useState({
+ email: "",
+ password: ""
+ })
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setLoginData(prevLoginData => ({ ...prevLoginData, [name]: value }));
+ }
+
+ const handleLogin = async (e) => {
+ e.preventDefault();
+ try {
+ const response = await adminLogin(dispatch, loginData);
+ console.log("Successful login", response)
+ alert("Successful login!")
+ if (response.token) {
+ localStorage.setItem('accessToken', response.token);
+ }
+ setLoginData({
+ email: "",
+ password: ""
+ })
+ navigate("/admin")
+ } catch (error) {
+ console.error("Error de logueo", error)
+ alert("Credenciales incorrectas")
+ }
+ }
+
+ return (
+
+
+
+
+

+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/front/routes.jsx b/src/front/routes.jsx
index 7625f041d0..8b7b25f62b 100644
--- a/src/front/routes.jsx
+++ b/src/front/routes.jsx
@@ -11,6 +11,7 @@ import { About } from "./pages/About";
import { ServicesPage } from "./pages/Services";
import { Portfolio } from "./pages/Projects";
import { Contact } from "./pages/Contact";
+import { Login } from "./pages/Login";
import { Admin } from "./pages/Admin";
import { ProtectedRoute } from "./components/ProtectedRoute";
@@ -31,8 +32,9 @@ export const router = createBrowserRouter(
} />
} />
} />
+
} />
{/*
} /> */}
- {/*
} /> */}
+ } />
)
);
\ No newline at end of file
diff --git a/src/front/store.js b/src/front/store.js
index edaad5a0e3..6005e5a75f 100644
--- a/src/front/store.js
+++ b/src/front/store.js
@@ -17,6 +17,33 @@ const initialContactFormState = {
export default function storeReducer(store, action = {}) {
switch (action.type) {
+ case "ADMIN_LOGIN_START":
+ return {
+ ...store,
+ loginStatus: {
+ status: "loading",
+ error: null,
+ },
+ };
+
+ case "ADMIN_LOGIN_SUCCESS":
+ return {
+ ...store,
+ loginStatus: {
+ status: "success",
+ error: null,
+ },
+ };
+
+ case "ADMIN_LOGIN_FAILURE":
+ return {
+ ...store,
+ loginStatus: {
+ status: "error",
+ error: action.payload,
+ },
+ };
+
case "GET_ALL_LEADS_START":
return {
...store,