diff --git a/src/mt_odoo_mcp/api/routes.py b/src/mt_odoo_mcp/api/routes.py index ba20472..c4b4811 100644 --- a/src/mt_odoo_mcp/api/routes.py +++ b/src/mt_odoo_mcp/api/routes.py @@ -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}", + }, } diff --git a/src/mt_odoo_mcp/auth/service.py b/src/mt_odoo_mcp/auth/service.py index cbbe940..ab8f8fc 100644 --- a/src/mt_odoo_mcp/auth/service.py +++ b/src/mt_odoo_mcp/auth/service.py @@ -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]