inital
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
"""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'}",
|
||||
)
|
||||
Reference in New Issue
Block a user