node and account names
This commit is contained in:
@@ -27,9 +27,24 @@ DEFAULT_ACCOUNTS_DB_PATH = "accounts.sqlite"
|
||||
SESSION_TTL = 7 * 86400.0 # seconds
|
||||
PBKDF2_ITERATIONS = 200_000
|
||||
MIN_PASSWORD_LENGTH = 8
|
||||
_MAX_NICKNAME_LENGTH = 64
|
||||
API_KEY_PREFIX = "sk-mesh-"
|
||||
|
||||
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
_UNSET = object()
|
||||
|
||||
|
||||
def _normalize_nickname(value: object) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("nickname must be a string")
|
||||
nickname = value.strip()
|
||||
if not nickname:
|
||||
return None
|
||||
if len(nickname) > _MAX_NICKNAME_LENGTH:
|
||||
raise ValueError(f"nickname must be at most {_MAX_NICKNAME_LENGTH} characters")
|
||||
return nickname
|
||||
|
||||
|
||||
def _hash_password(password: str, salt: str) -> str:
|
||||
@@ -64,6 +79,7 @@ class AccountStore:
|
||||
email: str | None = None,
|
||||
wallet: str | None = None,
|
||||
password: str,
|
||||
nickname: str | None = None,
|
||||
) -> dict:
|
||||
"""Create an account. The first account becomes the admin.
|
||||
|
||||
@@ -77,6 +93,7 @@ class AccountStore:
|
||||
raise ValueError("invalid email address")
|
||||
if len(password or "") < MIN_PASSWORD_LENGTH:
|
||||
raise ValueError(f"password must be at least {MIN_PASSWORD_LENGTH} characters")
|
||||
nickname = _normalize_nickname(nickname)
|
||||
with self._lock:
|
||||
for identifier in filter(None, (email, wallet)):
|
||||
if identifier.lower() in self._by_identifier:
|
||||
@@ -91,11 +108,30 @@ class AccountStore:
|
||||
"role": "admin" if not self._accounts else "user",
|
||||
"password_hash": _hash_password(password, salt),
|
||||
"salt": salt,
|
||||
"nickname": nickname,
|
||||
"ts": time.time(),
|
||||
}
|
||||
self._apply_locked(event)
|
||||
return self._public_view(self._accounts[event["account_id"]])
|
||||
|
||||
def update_profile(self, account_id: str, *, nickname: str | None = _UNSET) -> dict:
|
||||
"""Update display fields for an account. Pass nickname=None to clear."""
|
||||
if nickname is not _UNSET:
|
||||
nickname = _normalize_nickname(nickname)
|
||||
with self._lock:
|
||||
if account_id not in self._accounts:
|
||||
raise ValueError("unknown account")
|
||||
event = {
|
||||
"id": f"profile-{uuid.uuid4().hex}",
|
||||
"type": "update_profile",
|
||||
"account_id": account_id,
|
||||
"ts": time.time(),
|
||||
}
|
||||
if nickname is not _UNSET:
|
||||
event["nickname"] = nickname
|
||||
self._apply_locked(event)
|
||||
return self._public_view(self._accounts[account_id])
|
||||
|
||||
def verify_login(self, identifier: str, password: str) -> dict | None:
|
||||
"""Return the public account view when credentials match, else None."""
|
||||
with self._lock:
|
||||
@@ -196,6 +232,7 @@ class AccountStore:
|
||||
"account_id": record["account_id"],
|
||||
"email": record.get("email"),
|
||||
"wallet": record.get("wallet"),
|
||||
"nickname": record.get("nickname"),
|
||||
"role": record["role"],
|
||||
"created_ts": record.get("ts", 0.0),
|
||||
}
|
||||
@@ -249,11 +286,19 @@ class AccountStore:
|
||||
"role": event.get("role", "user"),
|
||||
"password_hash": event["password_hash"],
|
||||
"salt": event["salt"],
|
||||
"nickname": event.get("nickname"),
|
||||
"ts": float(event.get("ts", 0.0)),
|
||||
}
|
||||
self._accounts[account_id] = record
|
||||
for identifier in filter(None, (record["email"], record["wallet"])):
|
||||
self._by_identifier.setdefault(identifier.lower(), account_id)
|
||||
elif etype == "update_profile":
|
||||
account_id = event["account_id"]
|
||||
record = self._accounts.get(account_id)
|
||||
if record is None:
|
||||
return
|
||||
if "nickname" in event:
|
||||
record["nickname"] = event.get("nickname")
|
||||
elif etype == "create_key":
|
||||
api_key = event["api_key"]
|
||||
if api_key not in self._revoked_keys:
|
||||
|
||||
Reference in New Issue
Block a user