feat: add auth service with local bcrypt password authentication

This commit is contained in:
Oliver Hofmann 2026-04-27 09:42:33 +02:00
parent e72e4ec466
commit f93793a1d8
3 changed files with 132 additions and 0 deletions

View File

@ -0,0 +1,18 @@
from datetime import datetime
from pydantic import BaseModel
class UserOut(BaseModel):
id: int
username: str
full_name: str
email: str | None
department: str | None
role: str | None
is_active: bool
is_admin: bool
last_login: datetime | None
created_at: datetime
model_config = {"from_attributes": True}

View File

@ -0,0 +1,45 @@
from datetime import datetime, timezone
from typing import Optional
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from app.modules.auth.models import User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain: str) -> str:
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def get_user(db: Session, username: str) -> Optional[User]:
return db.query(User).filter(User.username == username).first()
def authenticate_user(
db: Session, username: str, password: str, ldap_enabled: bool
) -> Optional[User]:
user = get_user(db, username)
if user is None or not user.is_active:
return None
local_ok = user.pw_hash is not None and verify_password(password, user.pw_hash)
if local_ok:
_touch_last_login(db, user)
return user
if ldap_enabled:
# LDAP auth implemented in Part 2
pass
return None
def _touch_last_login(db: Session, user: User) -> None:
user.last_login = datetime.now(timezone.utc)
db.commit()

View File

@ -0,0 +1,69 @@
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.database import Base
from app.modules.auth.models import User
from app.modules.auth.service import authenticate_user, get_user, hash_password, verify_password
@pytest.fixture
def db():
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def alice(db):
user = User(username="alice", full_name="Alice Smith", pw_hash=hash_password("secret123"))
db.add(user)
db.commit()
db.refresh(user)
return user
def test_hash_and_verify_password():
hashed = hash_password("mypassword")
assert verify_password("mypassword", hashed) is True
assert verify_password("wrong", hashed) is False
def test_get_user_found(db, alice):
assert get_user(db, "alice") is not None
def test_get_user_not_found(db):
assert get_user(db, "nobody") is None
def test_authenticate_correct_password(db, alice):
user = authenticate_user(db, "alice", "secret123", ldap_enabled=False)
assert user is not None
assert user.last_login is not None
def test_authenticate_wrong_password(db, alice):
assert authenticate_user(db, "alice", "wrong", ldap_enabled=False) is None
def test_authenticate_unknown_user(db):
assert authenticate_user(db, "ghost", "any", ldap_enabled=False) is None
def test_authenticate_inactive_user(db):
user = User(username="inactive", full_name="X", pw_hash=hash_password("pw"), is_active=False)
db.add(user)
db.commit()
assert authenticate_user(db, "inactive", "pw", ldap_enabled=False) is None
def test_authenticate_no_pw_hash_no_ldap(db):
user = User(username="ldaponly", full_name="Y", pw_hash=None)
db.add(user)
db.commit()
assert authenticate_user(db, "ldaponly", "any", ldap_enabled=False) is None