Add new frontend - fix 8

This commit is contained in:
Urtzi Alfaro
2025-07-22 13:46:05 +02:00
parent d04359eca5
commit 5959eb6e15
9 changed files with 873 additions and 688 deletions

View File

@@ -1,142 +1,244 @@
# services/auth/app/services/auth_service.py - FIXED VERSION
# services/auth/app/services/auth_service.py - UPDATED WITH NEW REGISTRATION METHOD
"""
Authentication service - FIXED
Handles user authentication without cross-service dependencies
Authentication Service - Updated to support registration with direct token issuance
"""
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional
from datetime import datetime, timezone, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from fastapi import HTTPException, status
import httpx
from typing import Dict, Any, Optional
import structlog
from app.models.users import User, RefreshToken
from app.core.security import SecurityManager
from app.core.config import settings
from app.services.messaging import publish_user_registered, publish_user_login, publish_user_logout
logger = logging.getLogger(__name__)
logger = structlog.get_logger()
class AuthService:
"""Authentication service"""
"""Enhanced Authentication service with unified token response"""
@staticmethod
async def authenticate_user(email: str, password: str, db: AsyncSession) -> Optional[User]:
"""Authenticate user with email and password"""
try:
# Get user from database
result = await db.execute(
select(User).where(
User.email == email,
User.is_active == True
)
)
user = result.scalar_one_or_none()
if not user:
logger.warning(f"User not found: {email}")
return None
if not SecurityManager.verify_password(password, user.hashed_password):
logger.warning(f"Invalid password for user: {email}")
return None
# Update last login
user.last_login = datetime.now(timezone.utc)
await db.commit()
logger.info(f"User authenticated successfully: {email}")
return user
except Exception as e:
logger.error(f"Authentication error for {email}: {e}")
await db.rollback()
return None
@staticmethod
async def create_user(email: str, password: str, full_name: str, db: AsyncSession) -> User:
"""Create a new user"""
async def register_user_with_tokens(
email: str,
password: str,
full_name: str,
db: AsyncSession
) -> Dict[str, Any]:
"""
Register new user and return tokens directly (NEW METHOD)
Follows industry best practices for immediate authentication
"""
try:
# Check if user already exists
result = await db.execute(
select(User).where(User.email == email)
)
result = await db.execute(select(User).where(User.email == email))
existing_user = result.scalar_one_or_none()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
status_code=status.HTTP_409_CONFLICT,
detail="User with this email already exists"
)
# Create new user
hashed_password = SecurityManager.hash_password(password)
user = User(
new_user = User(
email=email,
hashed_password=hashed_password,
full_name=full_name,
is_active=True,
is_verified=False
is_verified=False, # Will be verified via email
created_at=datetime.now(timezone.utc)
)
db.add(user)
await db.commit()
await db.refresh(user)
db.add(new_user)
await db.flush() # Get user ID without committing
logger.info(f"User created successfully: {email}")
return user
# Generate tokens immediately (shorter lifespan for unverified users)
access_token = SecurityManager.create_access_token(
user_data={
"user_id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"is_verified": new_user.is_verified
}
)
refresh_token_value = SecurityManager.create_refresh_token(
user_data={"user_id": str(new_user.id)}
)
# Store refresh token in database
refresh_token = RefreshToken(
user_id=new_user.id,
token=refresh_token_value,
expires_at=datetime.now(timezone.utc) + timedelta(days=7), # Shorter for new users
is_revoked=False
)
db.add(refresh_token)
await db.commit()
# Publish registration event (async)
try:
await publish_user_registered(
{
"user_id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"registered_at": new_user.created_at.isoformat()
}
)
except Exception as e:
logger.warning(f"Failed to publish registration event: {e}")
logger.info(f"User registered with tokens: {email}")
# Return unified token response format
return {
"access_token": access_token,
"refresh_token": refresh_token_value,
"token_type": "bearer",
"expires_in": 1800, # 30 minutes
"user": {
"id": str(new_user.id),
"email": new_user.email,
"full_name": new_user.full_name,
"is_active": new_user.is_active,
"is_verified": new_user.is_verified,
"created_at": new_user.created_at.isoformat()
}
}
except HTTPException:
await db.rollback()
raise
except Exception as e:
logger.error(f"User creation error: {e}")
await db.rollback()
logger.error(f"Registration with tokens failed for {email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user"
detail="Registration failed"
)
@staticmethod
async def login(email: str, password: str, db: AsyncSession) -> dict:
"""Login user and return tokens"""
async def create_user(
email: str,
password: str,
full_name: str,
db: AsyncSession
) -> User:
"""
Create user without tokens (LEGACY METHOD - kept for compatibility)
Use register_user_with_tokens() for new implementations
"""
try:
# Authenticate user
user = await AuthService.authenticate_user(email, password, db)
if not user:
# Check if user already exists
result = await db.execute(select(User).where(User.email == email))
existing_user = result.scalar_one_or_none()
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with this email already exists"
)
# Create new user
hashed_password = SecurityManager.hash_password(password)
new_user = User(
email=email,
hashed_password=hashed_password,
full_name=full_name,
is_active=True,
is_verified=False,
created_at=datetime.now(timezone.utc)
)
db.add(new_user)
await db.commit()
await db.refresh(new_user)
logger.info(f"User created (legacy): {email}")
return new_user
except HTTPException:
await db.rollback()
raise
except Exception as e:
await db.rollback()
logger.error(f"User creation failed for {email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="User creation failed"
)
@staticmethod
async def login(email: str, password: str, db: AsyncSession) -> Dict[str, Any]:
"""Login user and return tokens (UNCHANGED)"""
try:
# Get user
result = await db.execute(select(User).where(User.email == email))
user = result.scalar_one_or_none()
if not user or not SecurityManager.verify_password(password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials"
)
# Create tokens
# Create tokens (standard lifespan for verified login)
access_token = SecurityManager.create_access_token(
user_data={
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name
"full_name": user.full_name,
"is_verified": user.is_verified
}
)
refresh_token_value = SecurityManager.create_refresh_token(user_data={"user_id": str(user.id)})
refresh_token_value = SecurityManager.create_refresh_token(
user_data={"user_id": str(user.id)}
)
# Store refresh token in database
refresh_token = RefreshToken(
user_id=user.id,
token=refresh_token_value,
expires_at=datetime.now(timezone.utc) + timedelta(days=30)
expires_at=datetime.now(timezone.utc) + timedelta(days=30),
is_revoked=False
)
db.add(refresh_token)
await db.commit()
# Publish login event
try:
await publish_user_login(
{
"user_id": str(user.id),
"email": user.email,
"login_at": datetime.now(timezone.utc).isoformat()
}
)
except Exception as e:
logger.warning(f"Failed to publish login event: {e}")
logger.info(f"User logged in successfully: {email}")
return {
"access_token": access_token,
"refresh_token": refresh_token_value,
"token_type": "bearer",
"user": user.to_dict()
"expires_in": 3600, # 1 hour
"user": {
"id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"is_active": user.is_active,
"is_verified": user.is_verified,
"created_at": user.created_at.isoformat()
}
}
except HTTPException:
@@ -147,10 +249,10 @@ class AuthService:
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Login failed"
)
@staticmethod
async def refresh_access_token(refresh_token: str, db: AsyncSession) -> dict:
"""Refresh access token using refresh token"""
async def refresh_access_token(refresh_token: str, db: AsyncSession) -> Dict[str, Any]:
"""Refresh access token using refresh token (UNCHANGED)"""
try:
# Verify refresh token
payload = SecurityManager.verify_token(refresh_token)
@@ -164,14 +266,13 @@ class AuthService:
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
detail="Invalid token payload"
)
# Check if refresh token exists and is valid
# Check if refresh token exists and is not revoked
result = await db.execute(
select(RefreshToken).where(
RefreshToken.token == refresh_token,
RefreshToken.user_id == user_id,
RefreshToken.is_revoked == False,
RefreshToken.expires_at > datetime.now(timezone.utc)
)
@@ -184,10 +285,8 @@ class AuthService:
detail="Invalid or expired refresh token"
)
# Get user
result = await db.execute(
select(User).where(User.id == user_id, User.is_active == True)
)
# Get user info
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
@@ -201,13 +300,17 @@ class AuthService:
user_data={
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name
"full_name": user.full_name,
"is_verified": user.is_verified
}
)
logger.info(f"Token refreshed successfully for user {user_id}")
return {
"access_token": access_token,
"token_type": "bearer"
"token_type": "bearer",
"expires_in": 3600
}
except HTTPException:
@@ -221,7 +324,7 @@ class AuthService:
@staticmethod
async def logout(refresh_token: str, db: AsyncSession) -> bool:
"""Logout user by revoking refresh token"""
"""Logout user by revoking refresh token (UNCHANGED)"""
try:
# Revoke refresh token
result = await db.execute(
@@ -242,8 +345,8 @@ class AuthService:
return False
@staticmethod
async def verify_user_token(token: str) -> dict:
"""Verify access token and return user info"""
async def verify_user_token(token: str) -> Dict[str, Any]:
"""Verify access token and return user info (UNCHANGED)"""
try:
payload = SecurityManager.verify_token(token)
if not payload: