Few fixes
This commit is contained in:
@@ -15,7 +15,7 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy shared libraries
|
||||
COPY --from=shared /shared /app/shared
|
||||
COPY shared/ /app/shared/
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
@@ -3,7 +3,7 @@ Authentication service configuration
|
||||
"""
|
||||
|
||||
import os
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings"""
|
||||
|
||||
0
services/auth/app/models/__init__.py
Normal file
0
services/auth/app/models/__init__.py
Normal file
74
services/auth/app/models/users.py
Normal file
74
services/auth/app/models/users.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
User models for authentication service
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from shared.database.base import Base
|
||||
|
||||
class User(Base):
|
||||
"""User model"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String(255), nullable=False)
|
||||
full_name = Column(String(255), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
role = Column(String(50), default="user") # user, admin, super_admin
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
last_login = Column(DateTime)
|
||||
|
||||
# Profile fields
|
||||
phone = Column(String(20))
|
||||
language = Column(String(10), default="es")
|
||||
timezone = Column(String(50), default="Europe/Madrid")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(id={self.id}, email={self.email})>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert user to dictionary"""
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"email": self.email,
|
||||
"full_name": self.full_name,
|
||||
"is_active": self.is_active,
|
||||
"is_verified": self.is_verified,
|
||||
"tenant_id": str(self.tenant_id) if self.tenant_id else None,
|
||||
"role": self.role,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"last_login": self.last_login.isoformat() if self.last_login else None,
|
||||
"phone": self.phone,
|
||||
"language": self.language,
|
||||
"timezone": self.timezone
|
||||
}
|
||||
|
||||
|
||||
class UserSession(Base):
|
||||
"""User session model"""
|
||||
__tablename__ = "user_sessions"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
refresh_token_hash = Column(String(255), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
expires_at = Column(DateTime, nullable=False)
|
||||
|
||||
# Session metadata
|
||||
ip_address = Column(String(45))
|
||||
user_agent = Column(Text)
|
||||
device_info = Column(Text)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserSession(id={self.id}, user_id={self.user_id})>"
|
||||
@@ -15,7 +15,7 @@ class UserRegistration(BaseModel):
|
||||
password: str = Field(..., min_length=settings.PASSWORD_MIN_LENGTH)
|
||||
full_name: str = Field(..., min_length=2, max_length=100)
|
||||
phone: Optional[str] = None
|
||||
language: str = Field(default="es", regex="^(es|en)$")
|
||||
language: str = Field(default="es", pattern="^(es|en)$")
|
||||
|
||||
@validator('password')
|
||||
def validate_password(cls, v):
|
||||
@@ -97,7 +97,7 @@ class UserUpdate(BaseModel):
|
||||
"""User update schema"""
|
||||
full_name: Optional[str] = Field(None, min_length=2, max_length=100)
|
||||
phone: Optional[str] = None
|
||||
language: Optional[str] = Field(None, regex="^(es|en)$")
|
||||
language: Optional[str] = Field(None, pattern="^(es|en)$")
|
||||
timezone: Optional[str] = None
|
||||
|
||||
@validator('phone')
|
||||
|
||||
284
services/auth/app/services/auth_service.py
Normal file
284
services/auth/app/services/auth_service.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
Authentication service business logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.models.users import User, UserSession
|
||||
from app.schemas.auth import UserRegistration, UserLogin, TokenResponse, UserResponse
|
||||
from app.core.security import security_manager
|
||||
from app.services.messaging import message_publisher
|
||||
from shared.messaging.events import UserRegisteredEvent, UserLoginEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AuthService:
|
||||
"""Authentication service business logic"""
|
||||
|
||||
@staticmethod
|
||||
async def register_user(user_data: UserRegistration, db: AsyncSession) -> UserResponse:
|
||||
"""Register a new user"""
|
||||
|
||||
# Check if user already exists
|
||||
result = await db.execute(
|
||||
select(User).where(User.email == user_data.email)
|
||||
)
|
||||
existing_user = result.scalar_one_or_none()
|
||||
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already registered"
|
||||
)
|
||||
|
||||
# Hash password
|
||||
hashed_password = security_manager.hash_password(user_data.password)
|
||||
|
||||
# Create user
|
||||
user = User(
|
||||
email=user_data.email,
|
||||
hashed_password=hashed_password,
|
||||
full_name=user_data.full_name,
|
||||
phone=user_data.phone,
|
||||
language=user_data.language,
|
||||
is_active=True,
|
||||
is_verified=False # Email verification required
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
# Publish user registered event
|
||||
await message_publisher.publish_event(
|
||||
"user_events",
|
||||
"user.registered",
|
||||
UserRegisteredEvent(
|
||||
event_id="",
|
||||
service_name="auth-service",
|
||||
timestamp=datetime.utcnow(),
|
||||
data={
|
||||
"user_id": str(user.id),
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"language": user.language
|
||||
}
|
||||
).__dict__
|
||||
)
|
||||
|
||||
logger.info(f"User registered: {user.email}")
|
||||
return UserResponse(**user.to_dict())
|
||||
|
||||
@staticmethod
|
||||
async def login_user(login_data: UserLogin, db: AsyncSession, ip_address: str, user_agent: str) -> TokenResponse:
|
||||
"""Authenticate user and create tokens"""
|
||||
|
||||
# Check login attempts
|
||||
if not await security_manager.check_login_attempts(login_data.email):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="Too many login attempts. Please try again later."
|
||||
)
|
||||
|
||||
# Get user
|
||||
result = await db.execute(
|
||||
select(User).where(User.email == login_data.email)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not security_manager.verify_password(login_data.password, user.hashed_password):
|
||||
await security_manager.increment_login_attempts(login_data.email)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid email or password"
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Account is inactive"
|
||||
)
|
||||
|
||||
# Clear login attempts
|
||||
await security_manager.clear_login_attempts(login_data.email)
|
||||
|
||||
# Update last login
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id)
|
||||
.values(last_login=datetime.utcnow())
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# Create tokens
|
||||
token_data = {
|
||||
"user_id": str(user.id),
|
||||
"email": user.email,
|
||||
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
|
||||
"role": user.role
|
||||
}
|
||||
|
||||
access_token = security_manager.create_access_token(token_data)
|
||||
refresh_token = security_manager.create_refresh_token(token_data)
|
||||
|
||||
# Store refresh token
|
||||
await security_manager.store_refresh_token(str(user.id), refresh_token)
|
||||
|
||||
# Create session record
|
||||
session = UserSession(
|
||||
user_id=user.id,
|
||||
refresh_token_hash=security_manager.hash_password(refresh_token),
|
||||
expires_at=datetime.utcnow() + timedelta(days=7),
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
db.add(session)
|
||||
await db.commit()
|
||||
|
||||
# Publish login event
|
||||
await message_publisher.publish_event(
|
||||
"user_events",
|
||||
"user.login",
|
||||
UserLoginEvent(
|
||||
event_id="",
|
||||
service_name="auth-service",
|
||||
timestamp=datetime.utcnow(),
|
||||
data={
|
||||
"user_id": str(user.id),
|
||||
"email": user.email,
|
||||
"ip_address": ip_address,
|
||||
"user_agent": user_agent
|
||||
}
|
||||
).__dict__
|
||||
)
|
||||
|
||||
logger.info(f"User logged in: {user.email}")
|
||||
|
||||
return TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def refresh_token(refresh_token: str, db: AsyncSession) -> TokenResponse:
|
||||
"""Refresh access token"""
|
||||
|
||||
# Verify refresh token
|
||||
payload = security_manager.verify_token(refresh_token)
|
||||
if not payload or payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
# Verify refresh token is stored
|
||||
if not await security_manager.verify_refresh_token(user_id, refresh_token):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
# Get user
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found or inactive"
|
||||
)
|
||||
|
||||
# Create new tokens
|
||||
token_data = {
|
||||
"user_id": str(user.id),
|
||||
"email": user.email,
|
||||
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
|
||||
"role": user.role
|
||||
}
|
||||
|
||||
new_access_token = security_manager.create_access_token(token_data)
|
||||
new_refresh_token = security_manager.create_refresh_token(token_data)
|
||||
|
||||
# Update stored refresh token
|
||||
await security_manager.store_refresh_token(str(user.id), new_refresh_token)
|
||||
|
||||
logger.info(f"Token refreshed for user: {user.email}")
|
||||
|
||||
return TokenResponse(
|
||||
access_token=new_access_token,
|
||||
refresh_token=new_refresh_token,
|
||||
expires_in=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def verify_token(token: str, db: AsyncSession) -> Dict[str, Any]:
|
||||
"""Verify access token"""
|
||||
|
||||
# Verify token
|
||||
payload = security_manager.verify_token(token)
|
||||
if not payload or payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired token"
|
||||
)
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token"
|
||||
)
|
||||
|
||||
# Get user
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found or inactive"
|
||||
)
|
||||
|
||||
return {
|
||||
"user_id": str(user.id),
|
||||
"email": user.email,
|
||||
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
|
||||
"role": user.role,
|
||||
"full_name": user.full_name,
|
||||
"is_verified": user.is_verified
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def logout_user(user_id: str, db: AsyncSession):
|
||||
"""Logout user and revoke tokens"""
|
||||
|
||||
# Revoke refresh token
|
||||
await security_manager.revoke_refresh_token(user_id)
|
||||
|
||||
# Deactivate user sessions
|
||||
await db.execute(
|
||||
update(UserSession)
|
||||
.where(UserSession.user_id == user_id)
|
||||
.values(is_active=False)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"User logged out: {user_id}")
|
||||
@@ -3,7 +3,7 @@ Shared JWT Authentication Handler
|
||||
Used across all microservices for consistent authentication
|
||||
"""
|
||||
|
||||
import jwt
|
||||
from jose import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
@@ -53,6 +53,6 @@ class JWTHandler:
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("Token has expired")
|
||||
return None
|
||||
except jwt.InvalidTokenError:
|
||||
except jwt.JWTError:
|
||||
logger.warning("Invalid token")
|
||||
return None
|
||||
Reference in New Issue
Block a user