Files
Metrix/server/app/services/auth.py
T

149 lines
5.9 KiB
Python

from sqlalchemy.orm import Session
from app.core.exceptions import bad_request
from app.core.permissions import USER_ROLE
from app.core.security import create_access_token, hash_password, verify_password
from app.core.time import utc_now
from app.models import Role, User
from app.repositories.roles import RoleRepository
from app.repositories.users import UserRepository
from app.schemas.auth import ChangePasswordRequest, LoginRequest, ProfileUpdateRequest, RegisterRequest
from app.schemas.settings import PublicSettings
from app.services.audit import audit_changes, audit_detail, record_audit
from app.services.permissions import get_user_permission_codes
from app.services.settings import SettingService
from app.services.users import user_profile_snapshot
class AuthService:
def __init__(self, db: Session):
self.db = db
self.users = UserRepository(db)
def register(self, payload: RegisterRequest) -> User:
settings = self._guard_registration(payload)
if self.users.get_by_username(payload.username):
raise bad_request("error.usernameExists", "Username already exists")
approval_required = settings.registration_approval_required
approval_status = "pending" if approval_required else "approved"
user = User(
username=payload.username,
full_name=payload.full_name,
phone=payload.phone,
email=payload.email,
company=payload.company,
department=payload.department,
password_hash=hash_password(payload.password),
approval_status=approval_status,
approved_at=None if approval_required else utc_now(),
is_active=True,
is_builtin=False,
roles=[] if approval_required else self._default_user_roles(),
)
self.users.create(user)
record_audit(
self.db,
None,
"user.register",
"user",
str(user.id),
user.username,
audit_detail(
user.username,
meta={
"username": user.username,
"full_name": user.full_name,
"phone": user.phone,
"email": user.email,
"company": user.company,
"department": user.department,
"approval_status": user.approval_status,
"registration_approval_required": approval_required,
},
),
)
self.db.commit()
return user
def _guard_registration(self, payload: RegisterRequest) -> PublicSettings:
settings = SettingService(self.db).public_settings()
if not settings.registration_enabled:
raise bad_request("error.registrationDisabled", "Registration is disabled")
required = settings.registration_required_fields
missing_fields = [
field
for field, required_flag in {
"phone": required.phone,
"email": required.email,
"company": required.company,
"department": required.department,
}.items()
if required_flag and not getattr(payload, field).strip()
]
if missing_fields:
raise bad_request("error.registrationFieldRequired", "Required registration field is missing", field=missing_fields[0])
return settings
def _default_user_roles(self) -> list[Role]:
role = RoleRepository(self.db).get_by_code(USER_ROLE)
return [role] if role else []
def login(self, payload: LoginRequest) -> tuple[str, User, list[str]]:
user = self.users.get_by_username(payload.username)
if not user or not verify_password(payload.password, user.password_hash):
record_audit(
self.db,
None,
"auth.login_failed",
"user",
"",
payload.username,
audit_detail(payload.username, meta={"reason": "invalid_credentials"}),
)
self.db.commit()
raise bad_request("error.invalidCredentials", "Invalid username or password")
if user.approval_status != "approved":
raise bad_request("error.accountPending", "Account approval is pending")
if not user.is_active:
raise bad_request("error.accountDisabled", "Account is disabled")
user.last_login_at = utc_now()
token = create_access_token(str(user.id))
permissions = sorted(get_user_permission_codes(user))
record_audit(self.db, user.id, "auth.login", "user", str(user.id), user.username, audit_detail(user.username))
self.db.commit()
return token, user, permissions
def update_profile(self, user: User, payload: ProfileUpdateRequest) -> User:
before = user_profile_snapshot(user)
user.full_name = payload.full_name
user.phone = payload.phone
user.email = payload.email
user.company = payload.company
user.department = payload.department
record_audit(
self.db,
user.id,
"auth.profile_update",
"user",
str(user.id),
user.username,
audit_detail(user.username, audit_changes(before, user_profile_snapshot(user))),
)
self.db.commit()
return user
def change_password(self, user: User, payload: ChangePasswordRequest) -> None:
if not verify_password(payload.old_password, user.password_hash):
raise bad_request("error.oldPasswordIncorrect", "Old password is incorrect")
user.password_hash = hash_password(payload.new_password)
record_audit(
self.db,
user.id,
"auth.change_password",
"user",
str(user.id),
user.username,
audit_detail(user.username, meta={"password_changed": True}),
)
self.db.commit()