diff --git a/INSTALLATION.md b/INSTALLATION.md index 8c740292..7a0102e3 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -456,7 +456,7 @@ DEFAULT_REPO_NAME="" # Default GitHub repo (e.g. "my-repo") DASHBOARD_API_BASE_URL="http://localhost:2024" # Public base URL of the dashboard frontend (the ui/ app). Default post-login redirect. DASHBOARD_BASE_URL="http://localhost:3000" -# HMAC secret for all dashboard JWTs (session cookie, OAuth state, account-link tokens). +# HMAC secret for dashboard JWTs (session cookie and OAuth state). DASHBOARD_JWT_SECRET="" # Generate with: openssl rand -hex 32 # Comma-separated origins allowed for credentialed CORS and post-login redirects. # Required whenever the frontend and API are on different origins — including local diff --git a/agent/dashboard/oauth.py b/agent/dashboard/oauth.py index ae1671cd..24b2003c 100644 --- a/agent/dashboard/oauth.py +++ b/agent/dashboard/oauth.py @@ -10,7 +10,7 @@ import secrets import time from datetime import UTC, datetime, timedelta from typing import Any -from urllib.parse import quote, urlparse +from urllib.parse import urlparse import httpx import jwt @@ -148,7 +148,7 @@ def hash_state_nonce(nonce: str) -> str: return hmac.new(_secret().encode(), nonce.encode(), hashlib.sha256).hexdigest() -def issue_state(*, redirect_to: str, nonce_hash: str, link: str | None = None) -> str: +def issue_state(*, redirect_to: str, nonce_hash: str) -> str: now = int(time.time()) payload: dict[str, Any] = { "nonce_hash": nonce_hash, @@ -156,8 +156,6 @@ def issue_state(*, redirect_to: str, nonce_hash: str, link: str | None = None) - "iat": now, "exp": now + STATE_TTL_SECONDS, } - if link: - payload["link"] = link return jwt.encode(payload, _secret(), algorithm=JWT_ALG) @@ -168,43 +166,10 @@ def decode_state(state: str) -> dict[str, Any]: raise HTTPException(400, f"invalid state: {e}") from e -LINK_TTL_SECONDS = 7 * 24 * 60 * 60 - # Dashboard route where users manage their GitHub↔Slack link. PROFILE_SETTINGS_PATH = "/my-settings" -def issue_account_link(*, slack_user_id: str | None, work_email: str | None) -> str: - """Sign a short-lived token carrying the Slack identity to map after login. - - Threaded through the OAuth ``state`` so the callback can attach the - resolved ``github_login`` to the originating Slack user/email in one step. - """ - now = int(time.time()) - payload = { - "kind": "account_link", - "slack_user_id": slack_user_id or None, - "work_email": work_email or None, - "iat": now, - "exp": now + LINK_TTL_SECONDS, - } - return jwt.encode(payload, _secret(), algorithm=JWT_ALG) - - -def decode_account_link(token: str) -> dict[str, Any] | None: - """Decode an account-link token; return ``None`` if absent/invalid/expired.""" - if not token: - return None - try: - payload = jwt.decode(token, _secret(), algorithms=[JWT_ALG]) - except jwt.PyJWTError as e: - logger.warning("invalid account-link token: %s", e) - return None - if payload.get("kind") != "account_link": - return None - return payload - - def build_settings_url() -> str | None: """Return the dashboard Profile Settings URL, or ``None`` if not configured. @@ -218,26 +183,6 @@ def build_settings_url() -> str | None: return f"{frontend_base}{PROFILE_SETTINGS_PATH}" -def build_account_link_url(*, slack_user_id: str | None, work_email: str | None) -> str | None: - """Return the dashboard login URL that links a Slack identity on completion. - - Returns ``None`` when ``DASHBOARD_API_BASE_URL`` isn't configured (login - can't be initiated), so callers can skip the prompt cleanly. - """ - api_base = os.environ.get("DASHBOARD_API_BASE_URL", "").rstrip("/") - if not api_base: - return None - token = issue_account_link(slack_user_id=slack_user_id, work_email=work_email) - url = f"{api_base}/dashboard/api/auth/login?link={quote(token, safe='')}" - # Land the user on the Profile Settings page so they can review/complete - # their GitHub↔Slack link after re-authenticating. - frontend_base = os.environ.get("DASHBOARD_BASE_URL", "").rstrip("/") - if frontend_base: - redirect_to = f"{frontend_base}{PROFILE_SETTINGS_PATH}" - url += f"&redirect_to={quote(redirect_to, safe='')}" - return url - - def require_session(request: Request) -> dict[str, Any]: token = request.cookies.get(COOKIE_NAME) if not token: diff --git a/agent/dashboard/routes.py b/agent/dashboard/routes.py index 0888886c..bfb7fff1 100644 --- a/agent/dashboard/routes.py +++ b/agent/dashboard/routes.py @@ -23,7 +23,6 @@ from .oauth import ( SESSION_TTL_SECONDS, STATE_COOKIE_NAME, STATE_TTL_SECONDS, - decode_account_link, decode_state, enforce_org_login_gate, exchange_code, @@ -198,21 +197,16 @@ def _clear_slack_state_cookie(response: Response) -> None: async def auth_login( request: Request, redirect_to: str | None = None, - link: str | None = None, ) -> RedirectResponse: client_id = os.environ.get("GITHUB_APP_CLIENT_ID", "") if not client_id: raise HTTPException(500, "GITHUB_APP_CLIENT_ID not configured") safe_redirect = sanitize_redirect_to(redirect_to) or _frontend_base_url() - # Only carry a structurally valid account-link token onward. - link_token = link if (link and decode_account_link(link)) else None - nonce = new_state_nonce() state = issue_state( redirect_to=safe_redirect, nonce_hash=hash_state_nonce(nonce), - link=link_token, ) redirect_uri = f"{_api_base_url()}/dashboard/api/auth/callback" url = ( @@ -254,7 +248,6 @@ async def auth_callback(request: Request, code: str, state: str) -> RedirectResp await enforce_org_login_gate(login) await upsert_access_token_from_github_response(login, email or "", token_data) - await _complete_account_mapping(login, email, state_payload.get("link")) session_jwt = issue_session(login=login, email=email, avatar_url=user.get("avatar_url")) response = RedirectResponse(redirect_to, status_code=302) @@ -263,33 +256,6 @@ async def auth_callback(request: Request, code: str, state: str) -> RedirectResp return response -async def _complete_account_mapping(login: str, github_email: str | None, link_token: Any) -> None: - """Create/refresh the user mapping after a successful org-gated login. - - Self-service signup: the user is already org-gated (only members reach - here), so we record a ``source="self"`` mapping. The Slack identity (user - id + work email) is carried in the signed account-link token when the flow - started from an unmapped Slack mention; otherwise we fall back to the - user's verified GitHub email. - """ - link = decode_account_link(link_token) if isinstance(link_token, str) else None - slack_user_id = link.get("slack_user_id") if link else None - work_email = (link.get("work_email") if link else None) or github_email - if not work_email: - logger.warning("No work email available to map GitHub login %r", login) - return - try: - await upsert_mapping( - github_login=login, - work_email=work_email, - slack_user_id=slack_user_id if isinstance(slack_user_id, str) else None, - source="self", - status="active", - ) - except Exception: # noqa: BLE001 - logger.warning("Failed to persist self-service mapping for %r", login, exc_info=True) - - @router.post("/auth/logout") async def auth_logout() -> Response: response = Response(status_code=204) @@ -440,12 +406,6 @@ async def api_set_enabled_review_repo( return {"repos": repos} -class UserMappingUpsert(BaseModel): - github_login: str - work_email: str - slack_user_id: str | None = None - - @router.get("/admin/user-mappings") async def admin_list_user_mappings( page: int = 1, @@ -466,23 +426,6 @@ async def admin_list_user_mappings( } -@router.put("/admin/user-mappings") -async def admin_upsert_user_mapping( - body: UserMappingUpsert, - _admin: dict[str, Any] = _ADMIN_DEP, -) -> dict[str, Any]: - try: - return await upsert_mapping( - github_login=body.github_login, - work_email=body.work_email, - slack_user_id=body.slack_user_id, - source="admin", - status="active", - ) - except ValueError as e: - raise HTTPException(400, str(e)) from e - - @router.delete("/admin/user-mappings/{github_login}") async def admin_delete_user_mapping( github_login: str, diff --git a/agent/dashboard/user_mappings.py b/agent/dashboard/user_mappings.py index f4056c47..8568bdc4 100644 --- a/agent/dashboard/user_mappings.py +++ b/agent/dashboard/user_mappings.py @@ -7,7 +7,7 @@ keyed by GitHub login in the ``["user_mappings"]`` LangGraph Store namespace:: "github_login": "octocat", "work_email": "octo@example.com", "slack_user_id": "U123" | None, - "source": "hardcoded" | "self" | "admin", + "source": "slack_oauth", "status": "active" | "pending", "created_at": "...", "updated_at": "...", } @@ -35,7 +35,7 @@ logger = logging.getLogger(__name__) USER_MAPPINGS_NAMESPACE: list[str] = ["user_mappings"] -MappingSource = Literal["hardcoded", "self", "admin", "slack_oauth"] +MappingSource = Literal["slack_oauth"] MappingStatus = Literal["active", "pending"] @@ -259,7 +259,7 @@ async def upsert_mapping( github_login: str, work_email: str, slack_user_id: str | None = None, - source: MappingSource = "admin", + source: MappingSource = "slack_oauth", status: MappingStatus = "active", ) -> dict[str, Any]: """Create or update a mapping keyed by GitHub login.""" @@ -299,28 +299,3 @@ async def delete_mapping(github_login: str) -> bool: raise _deindex_login(login) return True - - -async def bulk_import(mapping: dict[str, str], *, source: MappingSource = "hardcoded") -> int: - """Seed the Store from a ``{github_login: work_email}`` dict. - - Existing records are left untouched so re-running the import never - downgrades a richer (self/admin) record back to ``hardcoded``. Returns the - number of records newly created. - """ - created = 0 - for raw_login, raw_email in mapping.items(): - login = _norm_login(raw_login) - email = _norm_email(raw_email) - if not login or not email: - continue - if await get_mapping(login): - continue - await upsert_mapping( - github_login=login, - work_email=email, - source=source, - status="active", - ) - created += 1 - return created diff --git a/agent/webapp.py b/agent/webapp.py index 247668e7..63e0b086 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -1097,10 +1097,14 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st langgraph_client = get_client(url=LANGGRAPH_URL) is_first_mention = not await _thread_exists(thread_id) await _upsert_slack_thread_repo_metadata(thread_id, repo_config, langgraph_client) + # Pass the login resolved above (from the stable Slack user id) so the thread is + # always tagged with github_login — the key the dashboard searches by. Without + # it, upsert re-resolves from the Slack profile email, which can miss. await upsert_agent_thread_owner_metadata( thread_id, source="slack", repo_config=repo_config, + github_login=mapped_login or "", user_email=user_email or "", title=clean_text if is_first_mention else "", source_context={"slack_thread": configurable["slack_thread"]}, diff --git a/tests/test_account_link.py b/tests/test_account_link.py index dda3dda6..f5225d02 100644 --- a/tests/test_account_link.py +++ b/tests/test_account_link.py @@ -1,69 +1,15 @@ -"""Tests for the Slack→GitHub account-link OAuth threading.""" +"""Tests for the Slack account-link prompt.""" from __future__ import annotations import pytest -from agent.dashboard import oauth - @pytest.fixture(autouse=True) def _jwt_secret(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DASHBOARD_JWT_SECRET", "test-secret") -def test_account_link_round_trip() -> None: - token = oauth.issue_account_link(slack_user_id="U123", work_email="dev@x.com") - payload = oauth.decode_account_link(token) - assert payload is not None - assert payload["slack_user_id"] == "U123" - assert payload["work_email"] == "dev@x.com" - assert payload["kind"] == "account_link" - - -def test_decode_account_link_rejects_garbage() -> None: - assert oauth.decode_account_link("") is None - assert oauth.decode_account_link("not-a-jwt") is None - - -def test_decode_account_link_rejects_wrong_kind() -> None: - # A session token is a valid JWT but not an account-link token. - session = oauth.issue_session(login="x", email="x@x.com", avatar_url=None) - assert oauth.decode_account_link(session) is None - - -def test_build_account_link_url(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("DASHBOARD_API_BASE_URL", "https://api.example.com/") - monkeypatch.delenv("DASHBOARD_BASE_URL", raising=False) - url = oauth.build_account_link_url(slack_user_id="U1", work_email="d@x.com") - assert url is not None - assert url.startswith("https://api.example.com/dashboard/api/auth/login?link=") - # The embedded token must decode back to the same identity. - token = url.split("link=", 1)[1].split("&", 1)[0] - from urllib.parse import unquote - - payload = oauth.decode_account_link(unquote(token)) - assert payload["slack_user_id"] == "U1" - - -def test_build_account_link_url_redirects_to_profile_settings( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("DASHBOARD_API_BASE_URL", "https://api.example.com") - monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com") - url = oauth.build_account_link_url(slack_user_id="U1", work_email="d@x.com") - assert url is not None - from urllib.parse import parse_qs, urlparse - - query = parse_qs(urlparse(url).query) - assert query["redirect_to"] == ["https://app.example.com/my-settings"] - - -def test_build_account_link_url_none_without_base(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("DASHBOARD_API_BASE_URL", raising=False) - assert oauth.build_account_link_url(slack_user_id="U1", work_email="d@x.com") is None - - def test_account_link_prompt_posts_generic_token_free_link( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/test_account_link_completion.py b/tests/test_account_link_completion.py deleted file mode 100644 index 4a4bdeac..00000000 --- a/tests/test_account_link_completion.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Tests for self-service mapping completion in the OAuth callback.""" - -from __future__ import annotations - -from unittest.mock import AsyncMock - -import pytest - -from agent.dashboard import oauth, routes - - -@pytest.fixture(autouse=True) -def _jwt_secret(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("DASHBOARD_JWT_SECRET", "test-secret") - - -@pytest.mark.asyncio -async def test_completion_uses_link_token_slack_identity(monkeypatch: pytest.MonkeyPatch) -> None: - mock_upsert = AsyncMock() - monkeypatch.setattr(routes, "upsert_mapping", mock_upsert) - link = oauth.issue_account_link(slack_user_id="U999", work_email="slack@x.com") - - await routes._complete_account_mapping("octo", "gh@x.com", link) - - mock_upsert.assert_awaited_once() - kwargs = mock_upsert.await_args.kwargs - assert kwargs["github_login"] == "octo" - # Slack work email from the link token wins over the GitHub email. - assert kwargs["work_email"] == "slack@x.com" - assert kwargs["slack_user_id"] == "U999" - assert kwargs["source"] == "self" - - -@pytest.mark.asyncio -async def test_completion_falls_back_to_github_email(monkeypatch: pytest.MonkeyPatch) -> None: - mock_upsert = AsyncMock() - monkeypatch.setattr(routes, "upsert_mapping", mock_upsert) - - await routes._complete_account_mapping("octo", "gh@x.com", None) - - kwargs = mock_upsert.await_args.kwargs - assert kwargs["work_email"] == "gh@x.com" - assert kwargs["slack_user_id"] is None - assert kwargs["source"] == "self" - - -@pytest.mark.asyncio -async def test_completion_noop_without_any_email(monkeypatch: pytest.MonkeyPatch) -> None: - mock_upsert = AsyncMock() - monkeypatch.setattr(routes, "upsert_mapping", mock_upsert) - - await routes._complete_account_mapping("octo", None, None) - - mock_upsert.assert_not_awaited() diff --git a/tests/test_dashboard_run_email.py b/tests/test_dashboard_run_email.py index 6c2eb779..eedbf29c 100644 --- a/tests/test_dashboard_run_email.py +++ b/tests/test_dashboard_run_email.py @@ -43,7 +43,6 @@ async def test_run_email_prefers_github_mapping(fake_store: _FakeStore) -> None: await um.upsert_mapping( github_login="johannes117", work_email="johannes@langchain.dev", - source="admin", ) # OAuth profile carries a personal account that isn't an org member. profile = {"email": "johannesduplessis117@gmail.com"} diff --git a/tests/test_slack_context.py b/tests/test_slack_context.py index 973a8f22..99c61e29 100644 --- a/tests/test_slack_context.py +++ b/tests/test_slack_context.py @@ -863,8 +863,14 @@ def test_process_slack_mention_mapped_user_with_token_runs_as_user( async def fake_login_for_slack_id(slack_user_id): return "mason-gh" if slack_user_id == "U123" else None + owner_meta: dict[str, object] = {} + + async def fake_upsert_owner(thread_id: str, **kwargs: object) -> None: + owner_meta.update(kwargs) + monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists) monkeypatch.setattr(webapp, "login_for_slack_id", fake_login_for_slack_id) + monkeypatch.setattr(webapp, "upsert_agent_thread_owner_metadata", fake_upsert_owner) asyncio.run( webapp.process_slack_mention( @@ -883,6 +889,10 @@ def test_process_slack_mention_mapped_user_with_token_runs_as_user( run_create = captured["run_create"] configurable = run_create["kwargs"]["config"]["configurable"] assert configurable["github_login"] == "mason-gh" + # The thread is tagged with the login resolved from the Slack user id, so it + # surfaces in the web Agents UI even when the Slack profile email does not + # resolve to a mapping (login_for_email returns None in this harness). + assert owner_meta["github_login"] == "mason-gh" assert "use_installation_token_fallback" not in configurable assert "prompt" not in captured diff --git a/tests/test_user_mappings.py b/tests/test_user_mappings.py index e7d152f8..9036e6ed 100644 --- a/tests/test_user_mappings.py +++ b/tests/test_user_mappings.py @@ -48,7 +48,6 @@ async def test_upsert_and_bidirectional_lookup(fake_store: _FakeStore) -> None: github_login="Octocat", work_email="OCTO@example.com", slack_user_id="U123", - source="admin", ) # Login lookups are case-insensitive; email is normalized to lowercase. assert await um.email_for_login("octocat") == "octo@example.com" @@ -58,7 +57,7 @@ async def test_upsert_and_bidirectional_lookup(fake_store: _FakeStore) -> None: @pytest.mark.asyncio async def test_cache_readers_after_refresh(fake_store: _FakeStore) -> None: - await um.upsert_mapping(github_login="dev", work_email="dev@x.com", source="admin") + await um.upsert_mapping(github_login="dev", work_email="dev@x.com") um.clear_cache() await um.refresh_cache() assert um.cached_email_for_login("dev") == "dev@x.com" @@ -69,9 +68,7 @@ async def test_cache_readers_after_refresh(fake_store: _FakeStore) -> None: @pytest.mark.asyncio async def test_pending_status_not_trusted(fake_store: _FakeStore) -> None: - await um.upsert_mapping( - github_login="newbie", work_email="n@x.com", source="self", status="pending" - ) + await um.upsert_mapping(github_login="newbie", work_email="n@x.com", status="pending") um.clear_cache() await um.refresh_cache() assert um.is_login_mapped("newbie") is False @@ -79,7 +76,7 @@ async def test_pending_status_not_trusted(fake_store: _FakeStore) -> None: @pytest.mark.asyncio async def test_delete_removes_record_and_indexes(fake_store: _FakeStore) -> None: - await um.upsert_mapping(github_login="gone", work_email="g@x.com", source="admin") + await um.upsert_mapping(github_login="gone", work_email="g@x.com") assert await um.email_for_login("gone") == "g@x.com" deleted = await um.delete_mapping("gone") assert deleted is True @@ -96,7 +93,7 @@ async def test_resolve_login_from_email_async_cold_cache( from agent.dashboard import agent_overrides monkeypatch.setattr(agent_overrides, "login_for_email", um.login_for_email) - await um.upsert_mapping(github_login="cold", work_email="cold@x.com", source="admin") + await um.upsert_mapping(github_login="cold", work_email="cold@x.com") um.clear_cache() assert await agent_overrides.resolve_login_from_email_async("cold@x.com") == "cold" @@ -110,13 +107,11 @@ async def test_update_deindexes_stale_email_and_slack_id(fake_store: _FakeStore) github_login="mover", work_email="old@x.com", slack_user_id="UOLD", - source="admin", ) await um.upsert_mapping( github_login="mover", work_email="new@x.com", slack_user_id="UNEW", - source="admin", ) assert um.cached_login_for_email("old@x.com") is None @@ -125,22 +120,6 @@ async def test_update_deindexes_stale_email_and_slack_id(fake_store: _FakeStore) assert um.cached_login_for_slack_id("UNEW") == "mover" -@pytest.mark.asyncio -async def test_bulk_import_skips_existing(fake_store: _FakeStore) -> None: - # Pre-existing richer record must survive a legacy re-import. - await um.upsert_mapping(github_login="keep", work_email="keep@x.com", source="self") - created = await um.bulk_import( - {"keep": "legacy@x.com", "fresh": "fresh@x.com", "bad": ""}, - source="hardcoded", - ) - assert created == 1 - keep = await um.get_mapping("keep") - assert keep["work_email"] == "keep@x.com" - assert keep["source"] == "self" - fresh = await um.get_mapping("fresh") - assert fresh["source"] == "hardcoded" - - @pytest.mark.asyncio async def test_upsert_requires_login_and_email(fake_store: _FakeStore) -> None: with pytest.raises(ValueError): @@ -151,7 +130,7 @@ async def test_upsert_requires_login_and_email(fake_store: _FakeStore) -> None: @pytest.mark.asyncio async def test_list_mappings_sorted(fake_store: _FakeStore) -> None: - await um.upsert_mapping(github_login="zeta", work_email="z@x.com", source="admin") - await um.upsert_mapping(github_login="alpha", work_email="a@x.com", source="admin") + await um.upsert_mapping(github_login="zeta", work_email="z@x.com") + await um.upsert_mapping(github_login="alpha", work_email="a@x.com") listed = await um.list_mappings() assert [m["github_login"] for m in listed] == ["alpha", "zeta"] diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index 85a099c1..2bc2fd5f 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -122,12 +122,6 @@ export interface UserMapping { updated_at?: string; } -export interface UserMappingUpsert { - github_login: string; - work_email: string; - slack_user_id?: string | null; -} - export interface UserMappingsPage { items: Array; total: number; @@ -218,11 +212,6 @@ export const api = { request( `/admin/user-mappings?page=${page}&page_size=${pageSize}`, ), - adminSaveUserMapping: (body: UserMappingUpsert) => - request("/admin/user-mappings", { - method: "PUT", - body: JSON.stringify(body), - }), adminDeleteUserMapping: (github_login: string) => request<{ deleted: boolean }>( `/admin/user-mappings/${encodeURIComponent(github_login)}`, diff --git a/ui/src/routes/admin.tsx b/ui/src/routes/admin.tsx index 757629a8..7a2ef557 100644 --- a/ui/src/routes/admin.tsx +++ b/ui/src/routes/admin.tsx @@ -5,7 +5,6 @@ import { useEffect, useState } from "react"; import type { ModelOption, TeamSettings, UserMapping } from "@/lib/api"; import { AppShell, SettingsRow, SettingsSection } from "@/components/AppShell"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -54,10 +53,6 @@ function AdminPage() { const PAGE_SIZE = 20; function UserMappingsSection({ enabled }: { enabled: boolean }) { - const qc = useQueryClient(); - const [login, setLogin] = useState(""); - const [email, setEmail] = useState(""); - const [slackId, setSlackId] = useState(""); const [error, setError] = useState(null); const [page, setPage] = useState(1); @@ -76,29 +71,9 @@ function UserMappingsSection({ enabled }: { enabled: boolean }) { } }, [mappings.isFetching, page, pageCount]); - const invalidate = () => - void qc.invalidateQueries({ queryKey: ["adminUserMappings"] }); - - const save = useMutation({ - mutationFn: () => - api.adminSaveUserMapping({ - github_login: login.trim(), - work_email: email.trim(), - slack_user_id: slackId.trim() || null, - }), - onSuccess: () => { - setLogin(""); - setEmail(""); - setSlackId(""); - setError(null); - invalidate(); - }, - onError: (e: Error) => setError(e.message), - }); - const remove = useMutation({ mutationFn: (gh: string) => api.adminDeleteUserMapping(gh), - onSuccess: invalidate, + onSuccess: () => void mappings.refetch(), onError: (e: Error) => setError(e.message), }); @@ -107,33 +82,9 @@ function UserMappingsSection({ enabled }: { enabled: boolean }) { return (
-
- setLogin(e.target.value)} - /> - setEmail(e.target.value)} - /> - setSlackId(e.target.value)} - /> - -
- {error && {error}}