feat(auth): add user registration and response DTOs, implement registration endpoint in AuthService
feat(auth): create registration route and integrate with AuthService feat(auth): implement user registration logic in AuthService with Keycloak integration fix(config): add Keycloak admin credentials to settings fix(middleware): update tenant middleware to include new auth routes chore(docs): remove outdated testing guide and add project architecture documentation feat(frontend): implement user registration page and integrate with API feat(frontend): create login page with tenant selection and error handling refactor(frontend): update layout to use custom auth store and improve loading states
This commit is contained in:
@@ -69,3 +69,43 @@ class UserInfoResponseDTO(BaseModel):
|
||||
class LogoutRequestDTO(BaseModel):
|
||||
"""DTO para solicitud de logout"""
|
||||
refresh_token: str = Field(..., description="Refresh token para invalidar")
|
||||
|
||||
|
||||
class RegisterRequestDTO(BaseModel):
|
||||
"""DTO para solicitud de registro"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="Nombre de usuario")
|
||||
email: EmailStr = Field(..., description="Email del usuario")
|
||||
password: str = Field(..., min_length=8, description="Contraseña")
|
||||
first_name: str = Field(..., min_length=2, max_length=50, description="Nombre")
|
||||
last_name: str = Field(..., min_length=2, max_length=50, description="Apellido")
|
||||
tenant_slug: str = Field(..., description="Slug del tenant")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"username": "jperez",
|
||||
"email": "jperez@ejemplo.com",
|
||||
"password": "MiPassword123!",
|
||||
"first_name": "Juan",
|
||||
"last_name": "Pérez",
|
||||
"tenant_slug": "empresa-abc"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RegisterResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de registro"""
|
||||
user_id: str
|
||||
username: str
|
||||
email: str
|
||||
message: str
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"username": "jperez",
|
||||
"email": "jperez@ejemplo.com",
|
||||
"message": "User registered successfully"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ from .dto import (
|
||||
TokenResponseDTO,
|
||||
RefreshTokenRequestDTO,
|
||||
UserInfoResponseDTO,
|
||||
LogoutRequestDTO
|
||||
LogoutRequestDTO,
|
||||
RegisterRequestDTO,
|
||||
RegisterResponseDTO
|
||||
)
|
||||
from .service import AuthService
|
||||
|
||||
@@ -20,6 +22,31 @@ router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
@router.post("/register", response_model=RegisterResponseDTO, status_code=201)
|
||||
async def register(
|
||||
register_data: RegisterRequestDTO,
|
||||
db: Session = Depends(get_core_db)
|
||||
):
|
||||
"""
|
||||
Registra un nuevo usuario en Keycloak
|
||||
|
||||
El usuario debe proporcionar:
|
||||
- username: Nombre de usuario único
|
||||
- email: Email único
|
||||
- password: Contraseña (mínimo 8 caracteres)
|
||||
- first_name: Nombre
|
||||
- last_name: Apellido
|
||||
- tenant_slug: Slug del tenant al que pertenece
|
||||
|
||||
El usuario se crea automáticamente en Keycloak con:
|
||||
- Cuenta habilitada
|
||||
- Rol 'user' asignado por defecto
|
||||
- Atributos de tenant
|
||||
"""
|
||||
service = AuthService(db)
|
||||
return service.register(register_data)
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponseDTO)
|
||||
async def login(
|
||||
login_data: LoginRequestDTO,
|
||||
|
||||
@@ -13,7 +13,9 @@ from .dto import (
|
||||
TokenResponseDTO,
|
||||
RefreshTokenRequestDTO,
|
||||
UserInfoResponseDTO,
|
||||
LogoutRequestDTO
|
||||
LogoutRequestDTO,
|
||||
RegisterRequestDTO,
|
||||
RegisterResponseDTO
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -56,13 +58,19 @@ class AuthService:
|
||||
if not tenant.is_active:
|
||||
raise HTTPException(status_code=403, detail="Tenant is not active")
|
||||
|
||||
# Cambiar realm al del tenant
|
||||
self.keycloak_openid.realm_name = tenant.keycloak_realm
|
||||
# Crear nueva instancia de KeycloakOpenID con el realm del tenant
|
||||
keycloak_client = KeycloakOpenID(
|
||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
||||
client_id=settings.KEYCLOAK_CLIENT_ID,
|
||||
realm_name=tenant.keycloak_realm,
|
||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET
|
||||
)
|
||||
|
||||
# Obtener token de Keycloak
|
||||
token_response = self.keycloak_openid.token(
|
||||
token_response = keycloak_client.token(
|
||||
username=login_data.username,
|
||||
password=login_data.password
|
||||
password=login_data.password,
|
||||
grant_type=["password"]
|
||||
)
|
||||
|
||||
logger.info(f"User logged in: {login_data.username} (tenant: {tenant.slug})")
|
||||
@@ -173,3 +181,97 @@ class AuthService:
|
||||
except Exception as e:
|
||||
logger.error(f"Logout error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Logout error")
|
||||
|
||||
def register(self, register_data: RegisterRequestDTO) -> RegisterResponseDTO:
|
||||
"""
|
||||
Registra un nuevo usuario en Keycloak
|
||||
|
||||
Args:
|
||||
register_data: Datos del usuario a registrar
|
||||
|
||||
Returns:
|
||||
RegisterResponseDTO con información del usuario creado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si el registro falla
|
||||
"""
|
||||
try:
|
||||
# Verificar que el tenant existe
|
||||
from api.v1.modules.a76.tenants.service import TenantService
|
||||
tenant_service = TenantService(self.db)
|
||||
tenant = tenant_service.get_tenant_by_slug(register_data.tenant_slug)
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
|
||||
if not tenant.is_active:
|
||||
raise HTTPException(status_code=403, detail="Tenant is not active")
|
||||
|
||||
# Crear instancia de KeycloakAdmin para gestión de usuarios
|
||||
keycloak_admin = KeycloakAdmin(
|
||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
||||
username=settings.KEYCLOAK_ADMIN_USERNAME,
|
||||
password=settings.KEYCLOAK_ADMIN_PASSWORD,
|
||||
realm_name=tenant.keycloak_realm,
|
||||
user_realm_name="master", # El admin suele estar en master realm
|
||||
verify=True
|
||||
)
|
||||
|
||||
# Preparar datos del usuario para Keycloak
|
||||
user_data = {
|
||||
"username": register_data.username,
|
||||
"email": register_data.email,
|
||||
"firstName": register_data.first_name,
|
||||
"lastName": register_data.last_name,
|
||||
"enabled": True,
|
||||
"emailVerified": False,
|
||||
"credentials": [{
|
||||
"type": "password",
|
||||
"value": register_data.password,
|
||||
"temporary": False
|
||||
}],
|
||||
"attributes": {
|
||||
"tenant_id": str(tenant.id),
|
||||
"tenant_slug": tenant.slug
|
||||
}
|
||||
}
|
||||
|
||||
# Crear usuario en Keycloak
|
||||
user_id = keycloak_admin.create_user(user_data)
|
||||
|
||||
# Asignar rol por defecto (user) - opcional, solo si existe
|
||||
try:
|
||||
user_role = keycloak_admin.get_realm_role("user")
|
||||
if user_role:
|
||||
keycloak_admin.assign_realm_roles(user_id, [user_role])
|
||||
logger.info(f"Assigned 'user' role to {register_data.username}")
|
||||
except KeycloakError as e:
|
||||
# El rol 'user' no existe, no es un error crítico
|
||||
logger.warning(f"Could not assign 'user' role: {str(e)}")
|
||||
|
||||
logger.info(f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})")
|
||||
|
||||
return RegisterResponseDTO(
|
||||
user_id=user_id,
|
||||
username=register_data.username,
|
||||
email=register_data.email,
|
||||
message="User registered successfully"
|
||||
)
|
||||
|
||||
except KeycloakError as e:
|
||||
error_message = str(e)
|
||||
logger.warning(f"Keycloak registration failed: {error_message}")
|
||||
|
||||
# Mensajes de error más específicos
|
||||
if "User exists" in error_message or "409" in error_message:
|
||||
raise HTTPException(status_code=409, detail="Username or email already exists")
|
||||
elif "Invalid" in error_message:
|
||||
raise HTTPException(status_code=400, detail="Invalid user data")
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Registration error")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Registration error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Registration error")
|
||||
|
||||
Reference in New Issue
Block a user