216 lines
6.6 KiB
Python
216 lines
6.6 KiB
Python
"""Authentication service."""
|
|
from datetime import datetime, timedelta
|
|
from flask import request, g
|
|
from app.extensions import db, bcrypt
|
|
from app.models.user import User
|
|
from app.models.authentication_method import AuthenticationMethod
|
|
from app.models.session import Session
|
|
from app.utils.constants import AuthMethodType, SessionStatus, UserStatus, AuditAction
|
|
from app.exceptions.auth_exceptions import InvalidCredentialsError, AccountSuspendedError, AccountInactiveError
|
|
from app.exceptions.validation_exceptions import EmailAlreadyExistsError
|
|
from app.services.audit_service import AuditService
|
|
import secrets
|
|
|
|
|
|
class AuthService:
|
|
"""Service for authentication operations."""
|
|
|
|
@staticmethod
|
|
def register_user(email, password, full_name=None):
|
|
"""
|
|
Register a new user with email/password.
|
|
|
|
Args:
|
|
email: User email address
|
|
password: Plain text password
|
|
full_name: Optional full name
|
|
|
|
Returns:
|
|
User instance
|
|
|
|
Raises:
|
|
EmailAlreadyExistsError: If email is already registered
|
|
"""
|
|
# Check if email already exists
|
|
existing_user = User.query.filter_by(email=email.lower()).first()
|
|
if existing_user and existing_user.deleted_at is None:
|
|
raise EmailAlreadyExistsError()
|
|
|
|
# Create user
|
|
user = User(
|
|
email=email.lower(),
|
|
full_name=full_name,
|
|
status=UserStatus.ACTIVE,
|
|
)
|
|
user.save()
|
|
|
|
# Create password authentication method
|
|
password_hash = bcrypt.generate_password_hash(password).decode("utf-8")
|
|
auth_method = AuthenticationMethod(
|
|
user_id=user.id,
|
|
method_type=AuthMethodType.PASSWORD,
|
|
password_hash=password_hash,
|
|
is_primary=True,
|
|
verified=True,
|
|
)
|
|
auth_method.save()
|
|
|
|
# Log the registration
|
|
AuditService.log_action(
|
|
action=AuditAction.USER_REGISTER,
|
|
user_id=user.id,
|
|
resource_type="user",
|
|
resource_id=user.id,
|
|
description=f"User registered with email: {email}",
|
|
)
|
|
|
|
return user
|
|
|
|
@staticmethod
|
|
def authenticate(email, password):
|
|
"""
|
|
Authenticate user with email/password.
|
|
|
|
Args:
|
|
email: User email
|
|
password: Plain text password
|
|
|
|
Returns:
|
|
User instance if authentication succeeds
|
|
|
|
Raises:
|
|
InvalidCredentialsError: If credentials are invalid
|
|
AccountSuspendedError: If account is suspended
|
|
AccountInactiveError: If account is inactive
|
|
"""
|
|
# Find user
|
|
user = User.query.filter_by(email=email.lower(), deleted_at=None).first()
|
|
if not user:
|
|
raise InvalidCredentialsError()
|
|
|
|
# Check account status
|
|
if user.status == UserStatus.SUSPENDED:
|
|
raise AccountSuspendedError()
|
|
if user.status == UserStatus.INACTIVE:
|
|
raise AccountInactiveError()
|
|
|
|
# Find password auth method
|
|
auth_method = AuthenticationMethod.query.filter_by(
|
|
user_id=user.id,
|
|
method_type=AuthMethodType.PASSWORD,
|
|
deleted_at=None,
|
|
).first()
|
|
|
|
if not auth_method or not auth_method.password_hash:
|
|
raise InvalidCredentialsError()
|
|
|
|
# Verify password
|
|
if not bcrypt.check_password_hash(auth_method.password_hash, password):
|
|
raise InvalidCredentialsError()
|
|
|
|
# Update last login
|
|
user.last_login_at = datetime.utcnow()
|
|
user.last_login_ip = request.remote_addr
|
|
auth_method.last_used_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
return user
|
|
|
|
@staticmethod
|
|
def create_session(user, duration_seconds=86400):
|
|
"""
|
|
Create a new session for the user.
|
|
|
|
Args:
|
|
user: User instance
|
|
duration_seconds: Session duration in seconds
|
|
|
|
Returns:
|
|
Session instance
|
|
"""
|
|
# Generate session token
|
|
token = secrets.token_urlsafe(32)
|
|
|
|
# Create session
|
|
session = Session(
|
|
user_id=user.id,
|
|
token=token,
|
|
status=SessionStatus.ACTIVE,
|
|
ip_address=request.remote_addr,
|
|
user_agent=request.headers.get("User-Agent"),
|
|
expires_at=datetime.utcnow() + timedelta(seconds=duration_seconds),
|
|
last_activity_at=datetime.utcnow(),
|
|
)
|
|
session.save()
|
|
|
|
# Log session creation
|
|
AuditService.log_action(
|
|
action=AuditAction.SESSION_CREATE,
|
|
user_id=user.id,
|
|
resource_type="session",
|
|
resource_id=session.id,
|
|
description="User session created",
|
|
)
|
|
|
|
return session
|
|
|
|
@staticmethod
|
|
def change_password(user, current_password, new_password):
|
|
"""
|
|
Change user password.
|
|
|
|
Args:
|
|
user: User instance
|
|
current_password: Current password
|
|
new_password: New password
|
|
|
|
Raises:
|
|
InvalidCredentialsError: If current password is incorrect
|
|
"""
|
|
# Find password auth method
|
|
auth_method = AuthenticationMethod.query.filter_by(
|
|
user_id=user.id,
|
|
method_type=AuthMethodType.PASSWORD,
|
|
deleted_at=None,
|
|
).first()
|
|
|
|
if not auth_method or not auth_method.password_hash:
|
|
raise InvalidCredentialsError("No password authentication method found")
|
|
|
|
# Verify current password
|
|
if not bcrypt.check_password_hash(auth_method.password_hash, current_password):
|
|
raise InvalidCredentialsError("Current password is incorrect")
|
|
|
|
# Update password
|
|
auth_method.password_hash = bcrypt.generate_password_hash(new_password).decode("utf-8")
|
|
db.session.commit()
|
|
|
|
# Log password change
|
|
AuditService.log_action(
|
|
action=AuditAction.PASSWORD_CHANGE,
|
|
user_id=user.id,
|
|
description="User changed password",
|
|
)
|
|
|
|
@staticmethod
|
|
def revoke_session(session_id, reason=None):
|
|
"""
|
|
Revoke a session.
|
|
|
|
Args:
|
|
session_id: Session ID to revoke
|
|
reason: Optional revocation reason
|
|
"""
|
|
session = Session.query.get(session_id)
|
|
if session:
|
|
session.revoke(reason=reason)
|
|
|
|
# Log session revocation
|
|
AuditService.log_action(
|
|
action=AuditAction.SESSION_REVOKE,
|
|
user_id=session.user_id,
|
|
resource_type="session",
|
|
resource_id=session.id,
|
|
description=f"Session revoked: {reason or 'User logout'}",
|
|
)
|