Fix login being permanently impossible without a pre-existing saved key
api_key_hash is a one-way hash, so /api/login could never hand back a previously-issued key — the frontend's fallback of reusing whatever key happened to already be in localStorage meant login only worked on the exact browser that originally signed up, and was completely broken on any new device, incognito window, or after clearing storage, even with the correct password. /api/login now issues a fresh 'Dashboard Login' named key on every successful login, replacing only that one key each time (no duplicate accumulation) and never touching the user's other named keys (e.g. an already-connected Claude Desktop/Codex client) or the legacy key. Also fixes a bug this exposed: list_api_keys' legacy-key backfill only ran when a user had zero rows in api_keys at all, so if login created a 'Dashboard Login' row first, the original signup key would never get backfilled and would silently vanish from the Tokens page (while still remaining fully valid for authentication). Backfill now checks specifically whether the legacy key's hash is already represented, independent of what other named keys exist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7384a6c05a
commit
9df12ac8c5
@ -5,7 +5,7 @@ from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
from ..config import settings
|
||||
from ..auth.service import (
|
||||
AuthError, signup, login, get_user_by_api_key,
|
||||
AuthError, signup, login_and_issue_key, get_user_by_api_key,
|
||||
upsert_odoo_credential, list_odoo_credentials,
|
||||
delete_odoo_credential, regenerate_api_key,
|
||||
create_api_key, list_api_keys, revoke_api_key,
|
||||
@ -115,17 +115,24 @@ def api_signup(body: SignupRequest):
|
||||
|
||||
@router.post("/login")
|
||||
def api_login(body: LoginRequest):
|
||||
"""Verify email + password. Returns account info (not the API key — use /api/api-key to regenerate)."""
|
||||
"""Verify email + password. Issues a fresh 'Dashboard Login' API key
|
||||
for this session — api_key_hash is a one-way hash, so a previously
|
||||
issued key can never be recovered here. Does not affect any other
|
||||
named keys (e.g. an already-connected Claude Desktop/Codex client)."""
|
||||
try:
|
||||
user = login(body.email, body.password)
|
||||
user, full_key = login_and_issue_key(body.email, body.password)
|
||||
except AuthError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"email": user.email,
|
||||
"api_key_prefix": user.api_key_prefix + "...",
|
||||
"created_at": user.created_at,
|
||||
"message": "Login successful. Use your API key to connect MCP clients.",
|
||||
"api_key": full_key,
|
||||
"warning": "Save this key — it will not be shown again.",
|
||||
"message": "Login successful.",
|
||||
"mcp_connection": {
|
||||
"url": f"{settings.public_url}/mcp/sse",
|
||||
"header": f"Authorization: Bearer {full_key}",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -71,6 +71,33 @@ def login(email: str, password: str) -> User:
|
||||
return _row_to_user(row)
|
||||
|
||||
|
||||
DASHBOARD_LOGIN_KEY_NAME = "Dashboard Login"
|
||||
|
||||
|
||||
def login_and_issue_key(email: str, password: str) -> tuple[User, str]:
|
||||
"""Verify email + password, then issue a usable API key for the
|
||||
session. Necessary because api_key_hash/key_hash are one-way hashes —
|
||||
the backend can never hand back a previously-issued key, so without
|
||||
this, logging in on any browser/device that doesn't already have a key
|
||||
saved in localStorage would be permanently impossible.
|
||||
|
||||
Replaces any existing 'Dashboard Login' key each call (so repeated
|
||||
logins don't accumulate duplicates) but never touches the user's other
|
||||
named keys (e.g. 'Claude Desktop', 'Codex') or the legacy key — an
|
||||
already-connected MCP client is never affected by a dashboard login
|
||||
elsewhere. Returns (user, full_key)."""
|
||||
user = login(email, password)
|
||||
old = db_fetchone(
|
||||
"SELECT id FROM api_keys WHERE user_id = ? AND name = ?",
|
||||
(user.id, DASHBOARD_LOGIN_KEY_NAME),
|
||||
)
|
||||
if old:
|
||||
db_execute("DELETE FROM api_keys WHERE id = ?", (old["id"],))
|
||||
db_commit()
|
||||
_, full_key = create_api_key(user.id, DASHBOARD_LOGIN_KEY_NAME)
|
||||
return user, full_key
|
||||
|
||||
|
||||
def get_user_by_api_key(api_key: str) -> Optional[User]:
|
||||
"""Look up a user by their API key. Returns None if not found/invalid.
|
||||
|
||||
@ -178,21 +205,28 @@ def create_api_key(user_id: int, name: str, instance_name: Optional[str] = None)
|
||||
def list_api_keys(user_id: int) -> list[ApiKeyInfo]:
|
||||
"""List all named API keys for a user. Lazily backfills a 'Default (legacy)'
|
||||
row mirroring users.api_key_hash the first time this is called, so the
|
||||
legacy signup key shows up in the UI like any other key."""
|
||||
rows = db_fetchall(
|
||||
"SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at", (user_id,)
|
||||
)
|
||||
if not rows:
|
||||
user_row = db_fetchone("SELECT api_key_hash, api_key_prefix FROM users WHERE id = ?", (user_id,))
|
||||
if user_row and user_row["api_key_hash"]:
|
||||
legacy signup key shows up in the UI like any other key.
|
||||
|
||||
Checks specifically whether the legacy key's hash is already represented
|
||||
as a row — not just "does this user have zero rows at all" — since
|
||||
login_and_issue_key() creates a 'Dashboard Login' row on first login,
|
||||
which would otherwise permanently prevent the legacy key from ever being
|
||||
backfilled (its row count would never be zero again)."""
|
||||
user_row = db_fetchone("SELECT api_key_hash, api_key_prefix FROM users WHERE id = ?", (user_id,))
|
||||
if user_row and user_row["api_key_hash"]:
|
||||
legacy_exists = db_fetchone(
|
||||
"SELECT 1 FROM api_keys WHERE user_id = ? AND key_hash = ?",
|
||||
(user_id, user_row["api_key_hash"]),
|
||||
)
|
||||
if not legacy_exists:
|
||||
db_execute(
|
||||
"INSERT INTO api_keys (user_id, name, key_hash, key_prefix) VALUES (?, ?, ?, ?)",
|
||||
(user_id, "Default (legacy)", user_row["api_key_hash"], user_row["api_key_prefix"]),
|
||||
)
|
||||
db_commit()
|
||||
rows = db_fetchall(
|
||||
"SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at", (user_id,)
|
||||
)
|
||||
rows = db_fetchall(
|
||||
"SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at", (user_id,)
|
||||
)
|
||||
return [_row_to_api_key(r) for r in rows]
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user