`pip install fluksio && fluksio serve` on a machine with no Docker, no database and no configuration — which is the case this is for: a node on a cluster where ports cannot be opened. It makes its data directory, its key and an admin account, prints the password once, and serves. Pairing is `fluksio enroll <code> --portal …`, doing what the Settings screen does through the same function, before the engine starts and without one running — a machine nobody can route to has no browser pointed at it either. The portal serves the dashboard, so nothing is served here. Two things had to give way. `fastapi[standard]` pulls a cloud CLI that wants sentry-sdk 2.x while we pinned below it — no pip resolution existed, so the pin is lifted, which the comment beside it had been waiting for and which also lets the Python cap go. And `uv` is now a dependency rather than something to find on PATH: the Modules screen is how a data scientist installs torch, and it was quietly falling back to the engine's own interpreter. The CLI imports nothing from the engine before it has set DATA_DIR — the settings are built on the first import of core.config, and reaching it early put the database in the working directory. There is a test for that now, because the failure is silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
"""First run: an account to sign in with.
|
|
|
|
An installation started from the command line is given no environment, so the
|
|
superuser a deployment sets in `.env` has to come from somewhere. The session
|
|
key is the CLI's own business — it has to be set before this module can be
|
|
imported at all.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
from fluksio import crud
|
|
from fluksio.models import User, UserCreate
|
|
|
|
#: A generated admin's address. `example.com` is reserved for exactly this
|
|
#: (RFC 2606) — `@localhost` and `.local` are too, and `EmailStr` refuses those.
|
|
#: It is a name to sign in with, not somewhere mail is sent.
|
|
DEFAULT_ADMIN = "admin@example.com"
|
|
|
|
|
|
def ensure_superuser(
|
|
session: Session, *, email: str | None = None, password: str | None = None
|
|
) -> tuple[User, str | None]:
|
|
"""The account to sign in with, made on first run.
|
|
|
|
Returns the user and, when it was just created, the password in clear —
|
|
the caller prints it once. An installation that already has a superuser is
|
|
left alone: this is a first run, not a password reset.
|
|
"""
|
|
existing = session.exec(
|
|
select(User).where(User.is_superuser == True) # noqa: E712
|
|
).first()
|
|
if existing is not None:
|
|
return existing, None
|
|
|
|
generated = password or secrets.token_urlsafe(12)
|
|
user = crud.create_user(
|
|
session=session,
|
|
user_create=UserCreate(
|
|
email=email or DEFAULT_ADMIN, password=generated, is_superuser=True
|
|
),
|
|
)
|
|
return user, generated
|
|
|
|
|
|
def pick_superuser(session: Session, email: str | None = None) -> User:
|
|
"""The account an enrolment acts as; a portal session arrives as this one."""
|
|
if email:
|
|
user = session.exec(select(User).where(User.email == email)).first()
|
|
if user is None:
|
|
raise LookupError(f"No account here with the address {email}")
|
|
return user
|
|
users = session.exec(
|
|
select(User).where(User.is_superuser == True) # noqa: E712
|
|
).all()
|
|
if not users:
|
|
raise LookupError("This installation has no superuser to enrol as")
|
|
if len(users) > 1:
|
|
addresses = ", ".join(sorted(u.email for u in users))
|
|
raise LookupError(f"Several superusers here — name one with --as: {addresses}")
|
|
return users[0]
|