feat: replace admin auth with cookie-based profile picker

Remove all authentication (login, sessions, bcrypt, itsdangerous) since
the app runs on a private homelab LAN. Replace with a profile picker
landing page and cookie-based profile selection (1-year expiry).

- Add Alembic migration to drop password_hash/is_admin columns
- Delete auth service, auth routes, login template, and auth tests
- Rewrite app/utils/auth.py with NoProfileSelectedError and
  require_active_profile dependency
- Add profile creation flow (GET/POST /profiles/create)
- Rewrite home page as profile picker with card layout
- Update all route files to use profile dependency instead of admin auth
- Remove bcrypt and itsdangerous from requirements
- Remove admin_username/admin_password from config
- Update all tests for new profile-based access model

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 12:40:54 -05:00
parent 3dc0171639
commit 576d3bbb68
44 changed files with 523 additions and 1024 deletions

View File

@@ -27,77 +27,48 @@ class UserService:
def create_user(
self,
username: str,
password_hash: str,
display_name: str,
height: Optional[str] = None,
weight: Optional[str] = None,
goals: Optional[str] = None,
is_admin: bool = False,
) -> User:
"""Create a new user profile.
Args:
username: Unique login identifier.
password_hash: Pre-hashed password string.
username: Unique identifier.
display_name: Human-readable name.
height: User height as string.
weight: User weight as string.
goals: Free-text goals.
is_admin: Whether user has admin privileges.
Returns:
The newly created User record.
"""
user = User(
username=username,
password_hash=password_hash,
display_name=display_name,
height=height,
weight=weight,
goals=goals,
is_admin=is_admin,
)
self._session.add(user)
self._session.commit()
self._session.refresh(user)
logger.info("user_created", username=username, is_admin=is_admin)
logger.info("user_created", username=username)
return user
def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Retrieve a user by primary key.
Args:
user_id: The user's ID.
Returns:
The User record, or None if not found.
"""
"""Retrieve a user by primary key."""
return self._session.get(User, user_id)
def get_user_by_username(self, username: str) -> Optional[User]:
"""Retrieve a user by username.
Args:
username: The username to look up.
Returns:
The User record, or None if not found.
"""
"""Retrieve a user by username."""
statement = select(User).where(User.username == username)
return self._session.exec(statement).first()
def list_users(self, exclude_admin: bool = False) -> list[User]:
"""List all user profiles.
Args:
exclude_admin: If True, omit admin users from the result.
Returns:
List of User records.
"""
def list_users(self) -> list[User]:
"""List all user profiles."""
statement = select(User)
if exclude_admin:
statement = statement.where(User.is_admin == False) # noqa: E712
return list(self._session.exec(statement).all())
def update_user(self, user_id: int, **kwargs) -> User: