rebase
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -4,6 +4,17 @@ All notable changes to AI-RPG are documented here.
|
|||||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.4] — 2026-06-20
|
||||||
|
|
||||||
|
### Fixed (proper fix for CORS env parsing)
|
||||||
|
- **app/config.py**: replaced the broken `Annotated[list[str], NoDecode]` approach with a simpler, more robust one:
|
||||||
|
- `cors_origins` is now declared as a plain `str` (comma-separated, e.g. `"http://a,http://b"` or `"*"`).
|
||||||
|
- Added a `cors_origins_list` property that splits the string into a `list[str]` on demand.
|
||||||
|
- This sidesteps the entire `EnvSettingsSource.decode_complex_value` / JSON-parsing codepath — pydantic-settings sees a `str` field, takes the env value as-is, no JSON parsing attempted.
|
||||||
|
- **app/main.py**: updated `CORSMiddleware(allow_origins=cfg.cors_origins_list)` to use the new property.
|
||||||
|
- **Why v1.0.3 didn't work**: in pydantic-settings 2.7.0, `NoDecode` is importable but `_annotation_is_complex()` doesn't actually check for it (only checks for `Json`). The marker was added to the package surface but the inner logic was only wired up in a later version. Switching to a plain `str` field is the most reliable fix and works across all pydantic-settings 2.x versions.
|
||||||
|
- All 68 unit tests still pass.
|
||||||
|
|
||||||
## [1.0.3] — 2026-06-20
|
## [1.0.3] — 2026-06-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field
|
||||||
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
@@ -38,10 +37,11 @@ class Settings(BaseSettings):
|
|||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
access_token_expire_minutes: int = 60 * 24 # 24 hours
|
access_token_expire_minutes: int = 60 * 24 # 24 hours
|
||||||
refresh_token_expire_minutes: int = 60 * 24 * 7 # 7 days
|
refresh_token_expire_minutes: int = 60 * 24 * 7 # 7 days
|
||||||
# `NoDecode` tells pydantic-settings to NOT JSON-parse the env var value
|
# Comma-separated list of allowed CORS origins, e.g.
|
||||||
# before passing it to our `_split_cors` validator. Without it, the value
|
# CORS_ORIGINS=http://localhost:8080,http://localhost:5173
|
||||||
# `CORS_ORIGINS=http://a,http://b` would be rejected as invalid JSON.
|
# Special value "*" allows all origins.
|
||||||
cors_origins: Annotated[list[str], NoDecode] = Field(default_factory=lambda: ["*"])
|
# Use `settings.cors_origins_list` to access the parsed list.
|
||||||
|
cors_origins: str = "*"
|
||||||
|
|
||||||
# === Admin setup ===
|
# === Admin setup ===
|
||||||
admin_setup_token: str = "" # if empty, will be auto-generated and stored in DB
|
admin_setup_token: str = "" # if empty, will be auto-generated and stored in DB
|
||||||
@@ -109,12 +109,16 @@ class Settings(BaseSettings):
|
|||||||
if not self.assets_dir:
|
if not self.assets_dir:
|
||||||
self.assets_dir = str(Path(self.data_dir) / "assets")
|
self.assets_dir = str(Path(self.data_dir) / "assets")
|
||||||
|
|
||||||
@field_validator("cors_origins", mode="before")
|
@property
|
||||||
@classmethod
|
def cors_origins_list(self) -> list[str]:
|
||||||
def _split_cors(cls, v):
|
"""Parse `cors_origins` (comma-separated string) into a list.
|
||||||
if isinstance(v, str):
|
|
||||||
return [item.strip() for item in v.split(",") if item.strip()]
|
Whitespace is stripped from each entry, empty entries are dropped.
|
||||||
return v
|
Returns `["*"]` if the field is empty or just whitespace.
|
||||||
|
"""
|
||||||
|
if not self.cors_origins or not self.cors_origins.strip():
|
||||||
|
return ["*"]
|
||||||
|
return [o.strip() for o in self.cors_origins.split(",") if o.strip()]
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ def create_app() -> FastAPI:
|
|||||||
# CORS
|
# CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=cfg.cors_origins,
|
allow_origins=cfg.cors_origins_list,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
Reference in New Issue
Block a user