feat: Implement user management functionality with login and CRUD operations
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from src.Usuario.usuario_mmr import UsuarioRepo, Usuario, UsuarioModel
|
||||||
|
from backend.db.conexion import get_conexion
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/usuarios/", response_model=dict)
|
||||||
|
def crear_usuario(nombre: str, email: str, db: Session = Depends(get_conexion)):
|
||||||
|
repo = UsuarioRepo(db)
|
||||||
|
usuario = Usuario(id=None, nombre=nombre, email=email)
|
||||||
|
usuario_id = repo.add(usuario)
|
||||||
|
return {"id": usuario_id}
|
||||||
|
|
||||||
|
@router.get("/usuarios/{usuario_id}", response_model=dict)
|
||||||
|
def obtener_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
|
||||||
|
repo = UsuarioRepo(db)
|
||||||
|
usuario = repo.get_by_id(usuario_id)
|
||||||
|
if not usuario:
|
||||||
|
raise HTTPException(status_code=404, detail="Usuario no encontrado")
|
||||||
|
return {"id": usuario.id, "nombre": usuario.nombre, "email": usuario.email, "activo": usuario.activo}
|
||||||
|
|
||||||
|
@router.get("/usuarios/", response_model=list)
|
||||||
|
def listar_usuarios(db: Session = Depends(get_conexion)):
|
||||||
|
repo = UsuarioRepo(db)
|
||||||
|
usuarios = repo.get_all()
|
||||||
|
return [{"id": u.id, "nombre": u.nombre, "email": u.email, "activo": u.activo} for u in usuarios]
|
||||||
|
|
||||||
|
@router.delete("/usuarios/{usuario_id}", response_model=dict)
|
||||||
|
def eliminar_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
|
||||||
|
repo = UsuarioRepo(db)
|
||||||
|
exito = repo.delete_by_id(usuario_id)
|
||||||
|
if not exito:
|
||||||
|
raise HTTPException(status_code=404, detail="Usuario no encontrado")
|
||||||
|
return {"ok": True}
|
||||||
@@ -5,9 +5,11 @@ from backend.domains.experiments import charts_examples_endpoint_v1 as charts
|
|||||||
from backend.domains.experiments import ping_endpoint_v1
|
from backend.domains.experiments import ping_endpoint_v1
|
||||||
from backend.domains.text_manager import text_manager_endpoint_v1
|
from backend.domains.text_manager import text_manager_endpoint_v1
|
||||||
from backend.domains.llms import llm_chat_endpoint_v1
|
from backend.domains.llms import llm_chat_endpoint_v1
|
||||||
|
from backend.domains.usuarios_endpoint_v1 import router as usuarios_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
router.include_router(ping_endpoint_v1.router, prefix="/ping")
|
router.include_router(ping_endpoint_v1.router, prefix="/ping")
|
||||||
router.include_router(text_manager_endpoint_v1.router, prefix="/text_manager")
|
router.include_router(text_manager_endpoint_v1.router, prefix="/text_manager")
|
||||||
router.include_router(charts.router, prefix="/charts")
|
router.include_router(charts.router, prefix="/charts")
|
||||||
router.include_router(llm_chat_endpoint_v1.router, prefix="/llm", tags=["Agente LLM"]) # ← Nuevo router
|
router.include_router(llm_chat_endpoint_v1.router, prefix="/llm", tags=["Agente LLM"])
|
||||||
|
router.include_router(usuarios_router, prefix="/usuarios", tags=["Usuarios"])
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { VisualizacionesRandom } from './domains/Experiments/Visualizaciones_Ran
|
|||||||
import { Camara_noir } from './domains/CamaraNoir/Camaras_noir';
|
import { Camara_noir } from './domains/CamaraNoir/Camaras_noir';
|
||||||
import EditorTest from "./domains/TextEditor/Editor_Test";
|
import EditorTest from "./domains/TextEditor/Editor_Test";
|
||||||
import { ChatPage } from './domains/Llms/Chat/ChatPage';
|
import { ChatPage } from './domains/Llms/Chat/ChatPage';
|
||||||
|
import { LoginPage } from './domains/Usuarios/Login.page';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
|
||||||
@@ -69,6 +70,12 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Login
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <LoginPage />,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// FitzStudio Pages -------------------------------------------------------
|
// FitzStudio Pages -------------------------------------------------------
|
||||||
// Error 404
|
// Error 404
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { TextInput, PasswordInput, Button, Paper, Title, Container, Group, Alert } from '@mantine/core';
|
||||||
|
import { IconUser, IconLock } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export function LoginPage() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
// Aquí deberías llamar a tu endpoint de login (ajusta la URL y payload)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/usuarios/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Credenciales incorrectas');
|
||||||
|
// Aquí puedes guardar el usuario/token en el estado global o localStorage
|
||||||
|
window.location.href = '/';
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={420} my={40}>
|
||||||
|
<Title align="center" mb={20}>Iniciar sesión</Title>
|
||||||
|
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="tucorreo@ejemplo.com"
|
||||||
|
icon={<IconUser size={18} />}
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
mb={10}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Contraseña"
|
||||||
|
placeholder="Tu contraseña"
|
||||||
|
icon={<IconLock size={18} />}
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
mb={20}
|
||||||
|
/>
|
||||||
|
{error && <Alert color="red" mb={10}>{error}</Alert>}
|
||||||
|
<Group mt="md">
|
||||||
|
<Button type="submit" loading={loading} fullWidth>
|
||||||
|
Entrar
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
class Usuario:
|
||||||
|
def __init__(self, id: int, nombre: str, email: str, activo: bool = True):
|
||||||
|
self.id = id
|
||||||
|
self.nombre = nombre
|
||||||
|
self.email = email
|
||||||
|
self.activo = activo
|
||||||
|
|
||||||
|
def activar(self):
|
||||||
|
self.activo = True
|
||||||
|
|
||||||
|
def desactivar(self):
|
||||||
|
self.activo = False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Usuario(id={self.id}, nombre='{self.nombre}', email='{self.email}', activo={self.activo})"
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Boolean
|
||||||
|
from src.ArquitectureLayer.Model import Model_base
|
||||||
|
from src.ArquitectureLayer.Mapper import Mapper_base
|
||||||
|
from src.ArquitectureLayer.Repo import Repo_base
|
||||||
|
from src.Usuario.usuario import Usuario
|
||||||
|
|
||||||
|
# ----------------------
|
||||||
|
# MODELO (SQLAlchemy)
|
||||||
|
# ----------------------
|
||||||
|
|
||||||
|
class UsuarioModel(Model_base):
|
||||||
|
__tablename__ = 'usuarios'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nombre = Column(String, nullable=False)
|
||||||
|
email = Column(String, unique=True, nullable=False)
|
||||||
|
activo = Column(Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
|
# ----------------------
|
||||||
|
# MAPPER
|
||||||
|
# ----------------------
|
||||||
|
|
||||||
|
class UsuarioMapper(Mapper_base[Usuario, UsuarioModel]):
|
||||||
|
@staticmethod
|
||||||
|
def to_model(obj: Usuario) -> UsuarioModel:
|
||||||
|
return UsuarioModel(
|
||||||
|
id=obj.id,
|
||||||
|
nombre=obj.nombre,
|
||||||
|
email=obj.email,
|
||||||
|
activo=obj.activo
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_model(model: UsuarioModel) -> Usuario:
|
||||||
|
return Usuario(
|
||||||
|
id=model.id,
|
||||||
|
nombre=model.nombre,
|
||||||
|
email=model.email,
|
||||||
|
activo=model.activo
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_dict(obj: Usuario) -> dict:
|
||||||
|
return {
|
||||||
|
'id': obj.id,
|
||||||
|
'nombre': obj.nombre,
|
||||||
|
'email': obj.email,
|
||||||
|
'activo': obj.activo
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict) -> Usuario:
|
||||||
|
return Usuario(
|
||||||
|
id=data['id'],
|
||||||
|
nombre=data['nombre'],
|
||||||
|
email=data['email'],
|
||||||
|
activo=data.get('activo', True)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_model_list(models: list[UsuarioModel]) -> list[Usuario]:
|
||||||
|
return [UsuarioMapper.from_model(m) for m in models]
|
||||||
|
|
||||||
|
# ----------------------
|
||||||
|
# REPO
|
||||||
|
# ----------------------
|
||||||
|
|
||||||
|
class UsuarioRepo(Repo_base[UsuarioModel, Usuario]):
|
||||||
|
def __init__(self, session):
|
||||||
|
super().__init__(
|
||||||
|
session=session,
|
||||||
|
modelo=UsuarioModel,
|
||||||
|
mapper=UsuarioMapper
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_by_email(self, email: str) -> Usuario | None:
|
||||||
|
model = (
|
||||||
|
self.session.query(self.Modelo)
|
||||||
|
.filter_by(email=email, sys_deleted_at=None)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return self.Mapper.from_model(model) if model else None
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
# Próximas tareas
|
# Próximas tareas
|
||||||
|
|
||||||
## Dominio: Usuarios
|
|
||||||
- [ ] Añadir gestión de usuarios (crear, editar, eliminar) (backend)
|
|
||||||
- [ ] Añadir tests a la gestión de usuarios (backend)
|
|
||||||
- [ ] Añadir gestión y visualización del usuario actual en la parte inferior izquierda del AppShell (frontend)
|
|
||||||
- [ ] Añadir descripciones a la gestión de usuarios en `frontend/src/data/` y `backend/router_v1.py`
|
|
||||||
|
|
||||||
## Dominio: Navegación y Menús
|
## Dominio: Navegación y Menús
|
||||||
- [ ] Añadir buscador al AppShell (frontend)
|
- [ ] Añadir buscador al AppShell (frontend)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Dominio: Usuarios
|
||||||
|
- [x] Añadir gestión de usuarios (crear, editar, eliminar) (backend)
|
||||||
|
- [ ] Añadir tests a la gestión de usuarios (backend)
|
||||||
|
- [ ] Añadir gestión y visualización del usuario actual en la parte inferior izquierda del AppShell (frontend)
|
||||||
|
- [ ] Añadir descripciones a la gestión de usuarios en `frontend/src/data/` y `backend/router_v1.py`
|
||||||
Reference in New Issue
Block a user