|
8 | 8 |
|
9 | 9 | from fastapi import FastAPI |
10 | 10 | from fastapi_users.password import PasswordHelper |
| 11 | +from simple_module_core.dotenv import parse_dotenv |
11 | 12 | from sqlalchemy import func, select |
12 | 13 | from sqlalchemy.ext.asyncio import AsyncSession |
13 | 14 |
|
|
22 | 23 | from users.models import Role, User, UserRole |
23 | 24 | from users.settings import UsersSettings |
24 | 25 |
|
| 26 | +# Maps a UsersSettings attribute to the env var that seeds it on first boot. |
| 27 | +# Shared between the bootstrap function and the test fixture that isolates it. |
| 28 | +BOOTSTRAP_ENV_KEYS: dict[str, str] = { |
| 29 | + "bootstrap_email": "SM_USERS_BOOTSTRAP_EMAIL", |
| 30 | + "bootstrap_password": "SM_USERS_BOOTSTRAP_PASSWORD", |
| 31 | + "bootstrap_user_email": "SM_USERS_BOOTSTRAP_USER_EMAIL", |
| 32 | + "bootstrap_user_password": "SM_USERS_BOOTSTRAP_USER_PASSWORD", |
| 33 | +} |
| 34 | + |
25 | 35 | logger = logging.getLogger("users.bootstrap") |
26 | 36 |
|
27 | 37 | _EVT_CREATED = "users.bootstrap.created" |
@@ -178,29 +188,41 @@ async def _user_table_is_empty(db: AsyncSession) -> bool: |
178 | 188 | return count == 0 |
179 | 189 |
|
180 | 190 |
|
| 191 | +def _read_dotenv_bootstrap_vars() -> dict[str, str]: |
| 192 | + """Return SM_USERS_BOOTSTRAP_* entries from ``.env`` (ignore everything else). |
| 193 | +
|
| 194 | + ``UsersSettings`` deliberately doesn't use ``env_file`` — runtime fields |
| 195 | + come from the DB, and pulling the whole ``.env`` in would re-expose every |
| 196 | + SMTP/cookie/token secret as an env knob. So we re-read ``.env`` just for |
| 197 | + the four documented seed keys. |
| 198 | + """ |
| 199 | + wanted = set(BOOTSTRAP_ENV_KEYS.values()) |
| 200 | + return {k: v for k, v in parse_dotenv().items() if k in wanted} |
| 201 | + |
| 202 | + |
181 | 203 | async def bootstrap_admin_from_env(app: FastAPI) -> None: |
182 | 204 | """On-startup hook: create admin from env vars iff users table is empty. |
183 | 205 |
|
184 | | - Reads ``SM_USERS_BOOTSTRAP_EMAIL`` + ``SM_USERS_BOOTSTRAP_PASSWORD`` via |
185 | | - `UsersSettings`. If either is blank, returns silently. If the table |
186 | | - already has users, returns silently (so restarts don't try to re-bootstrap). |
| 206 | + Resolves each of the four bootstrap fields in order: ``UsersSettings`` |
| 207 | + (tests), then ``os.environ`` (docker/systemd), then ``.env`` (documented |
| 208 | + dev path). If the admin email or password is still blank, returns |
| 209 | + silently — same if the users table already has rows (so restarts don't |
| 210 | + try to re-bootstrap). |
187 | 211 |
|
188 | 212 | Optionally also creates a non-admin user from |
189 | 213 | ``SM_USERS_BOOTSTRAP_USER_EMAIL`` + ``SM_USERS_BOOTSTRAP_USER_PASSWORD`` — |
190 | 214 | useful in dev for testing non-admin flows alongside the admin account. |
191 | 215 | """ |
192 | 216 | settings: UsersSettings = app.state.users.settings |
193 | | - # Bootstrap fields are one-shot seed inputs, not runtime DB settings — |
194 | | - # prefer the settings value if a test set one explicitly, otherwise fall |
195 | | - # back to the env var so production deployments keep working. |
196 | | - email = settings.bootstrap_email or os.environ.get("SM_USERS_BOOTSTRAP_EMAIL", "") |
197 | | - password = settings.bootstrap_password or os.environ.get("SM_USERS_BOOTSTRAP_PASSWORD", "") |
198 | | - user_email = settings.bootstrap_user_email or os.environ.get( |
199 | | - "SM_USERS_BOOTSTRAP_USER_EMAIL", "" |
200 | | - ) |
201 | | - user_password = settings.bootstrap_user_password or os.environ.get( |
202 | | - "SM_USERS_BOOTSTRAP_USER_PASSWORD", "" |
203 | | - ) |
| 217 | + dotenv_vars = _read_dotenv_bootstrap_vars() |
| 218 | + resolved = { |
| 219 | + attr: getattr(settings, attr) or os.environ.get(env_key) or dotenv_vars.get(env_key, "") |
| 220 | + for attr, env_key in BOOTSTRAP_ENV_KEYS.items() |
| 221 | + } |
| 222 | + email = resolved["bootstrap_email"] |
| 223 | + password = resolved["bootstrap_password"] |
| 224 | + user_email = resolved["bootstrap_user_email"] |
| 225 | + user_password = resolved["bootstrap_user_password"] |
204 | 226 |
|
205 | 227 | if not email or not password: |
206 | 228 | return |
|
0 commit comments