feat(auth): add DB-backed login, token management, and idempotent auth migration

This commit is contained in:
2026-04-03 12:55:34 +00:00
parent 5168e52e4b
commit 06b1805b92
28 changed files with 1609 additions and 140 deletions
+24
View File
@@ -21,10 +21,15 @@ class Settings(BaseSettings):
app_version: str = "0.1.0"
environment: str = "development"
allowed_origins_raw: str = Field(default="http://localhost:3000", validation_alias="PORTFOLIOHUB_ALLOWED_ORIGINS")
bootstrap_username: str = Field(default="", validation_alias="PORTFOLIOHUB_BOOTSTRAP_USERNAME")
bootstrap_email: str = Field(default="", validation_alias="PORTFOLIOHUB_BOOTSTRAP_EMAIL")
bootstrap_password: str = Field(default="", validation_alias="PORTFOLIOHUB_BOOTSTRAP_PASSWORD")
read_tokens_raw: str = Field(default="", validation_alias="PORTFOLIOHUB_READ_TOKENS")
trade_write_tokens_raw: str = Field(default="", validation_alias="PORTFOLIOHUB_TRADE_WRITE_TOKENS")
admin_tokens_raw: str = Field(default="", validation_alias="PORTFOLIOHUB_ADMIN_TOKENS")
db_enabled: bool = Field(default=False, validation_alias="PORTFOLIOHUB_DB_ENABLED")
session_token_ttl_hours: int = Field(default=12, validation_alias="PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS")
client_token_default_ttl_days: int = Field(default=365, validation_alias="PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS")
# External services configuration
timescaledb_host: str = Field(default="localhost", validation_alias="TIMESCALEDB_HOST")
@@ -49,6 +54,7 @@ class Settings(BaseSettings):
env_prefix="PORTFOLIOHUB_",
case_sensitive=False,
extra="ignore",
populate_by_name=True,
)
@computed_field
@@ -56,6 +62,24 @@ class Settings(BaseSettings):
def allowed_origins(self) -> list[str]:
return [item.strip() for item in self.allowed_origins_raw.split(",") if item.strip()]
@computed_field
@property
def bootstrap_user_username(self) -> str | None:
normalized = self.bootstrap_username.strip()
return normalized or None
@computed_field
@property
def bootstrap_user_email(self) -> str | None:
normalized = self.bootstrap_email.strip()
return normalized or None
@computed_field
@property
def bootstrap_user_password(self) -> str | None:
normalized = self.bootstrap_password.strip()
return normalized or None
@computed_field
@property
def read_tokens(self) -> set[str]: