Add org-specific delete invite endpoint (#575)
Adds POST /orgs/{oid}/invites/delete, which expects the invited email address in the POST body. This endpoint will also delete duplicate invites with the same email/oid combination if env var ALLOW_DUPE_INVITES allows dupes.
This commit is contained in:
parent
3261e7d666
commit
a7a18b9db0
@ -118,6 +118,15 @@ class InviteOps:
|
||||
"""remove invite from invite list"""
|
||||
await self.invites.delete_one({"_id": invite_token})
|
||||
|
||||
async def remove_invite_by_email(self, email: str, oid: str = None):
|
||||
"""remove invite from invite list by email"""
|
||||
query = {"email": email}
|
||||
if oid:
|
||||
query["oid"] = oid
|
||||
# Use delete_many rather than delete_one to clean up any duplicate
|
||||
# invites as well.
|
||||
return await self.invites.delete_many(query)
|
||||
|
||||
def accept_user_invite(self, user, invite_token: str):
|
||||
"""remove invite from user, if valid token, throw if not"""
|
||||
invite = user.invites.pop(invite_token, "")
|
||||
|
@ -3,6 +3,7 @@ Organization API handling
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
from typing import Dict, Union, Literal, Optional
|
||||
@ -39,6 +40,11 @@ class RemoveFromOrg(InviteRequest):
|
||||
"""Remove this user from org"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
class RemovePendingInvite(InviteRequest):
|
||||
"""Delete pending invite to org by email"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
class RenameOrg(BaseModel):
|
||||
"""Request to invite another user"""
|
||||
@ -468,6 +474,20 @@ def init_orgs_api(app, mdb, user_manager, invites, user_dep: User):
|
||||
pending_invites = await user_manager.invites.get_pending_invites(org)
|
||||
return {"pending_invites": pending_invites}
|
||||
|
||||
@router.post("/invites/delete", tags=["invites"])
|
||||
async def delete_invite(
|
||||
invite: RemovePendingInvite, org: Organization = Depends(org_owner_dep)
|
||||
):
|
||||
# URL decode email just in case
|
||||
email = urllib.parse.unquote(invite.email)
|
||||
result = await user_manager.invites.remove_invite_by_email(email, org.id)
|
||||
if result.deleted_count > 0:
|
||||
return {
|
||||
"removed": True,
|
||||
"count": result.deleted_count,
|
||||
}
|
||||
raise HTTPException(status_code=404, detail="invite_not_found")
|
||||
|
||||
@router.post("/remove", tags=["invites"])
|
||||
async def remove_user_from_org(
|
||||
remove: RemoveFromOrg, org: Organization = Depends(org_owner_dep)
|
||||
|
@ -483,11 +483,6 @@ def init_users_api(app, user_manager):
|
||||
|
||||
return await user_manager.format_invite(invite)
|
||||
|
||||
@users_router.get("/invite-delete/{token}", tags=["invites"])
|
||||
async def delete_invite(token: str):
|
||||
await user_manager.invites.remove_invite(token)
|
||||
return {"removed": True}
|
||||
|
||||
@users_router.get("/invites", tags=["invites"])
|
||||
async def get_pending_invites(user: User = Depends(current_active_user)):
|
||||
if not user.is_superuser:
|
||||
|
@ -1,4 +1,5 @@
|
||||
import requests
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
@ -231,3 +232,50 @@ def test_send_and_accept_org_invite(
|
||||
user for user in users.values() if user["email"] == expected_stored_email
|
||||
]
|
||||
assert len(users_with_invited_email) == 1
|
||||
|
||||
|
||||
def test_delete_invite_by_email(admin_auth_headers, non_default_org_id):
|
||||
# Invite user to non-default org
|
||||
INVITE_EMAIL = "new-non-default-org-invite-by-email@example.com"
|
||||
r = requests.post(
|
||||
f"{API_PREFIX}/orgs/{non_default_org_id}/invite",
|
||||
headers=admin_auth_headers,
|
||||
json={"email": INVITE_EMAIL, "role": 20},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["invited"] == "new_user"
|
||||
|
||||
# Delete invite by email
|
||||
r = requests.post(
|
||||
f"{API_PREFIX}/orgs/{non_default_org_id}/invites/delete",
|
||||
headers=admin_auth_headers,
|
||||
json={"email": INVITE_EMAIL},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["removed"]
|
||||
assert data["count"] == 1
|
||||
|
||||
# Verify invite no longer exists
|
||||
r = requests.get(
|
||||
f"{API_PREFIX}/orgs/{non_default_org_id}/invites",
|
||||
headers=admin_auth_headers,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
invites_matching_email = [
|
||||
invite for invite in data["pending_invites"] if invite["email"] == INVITE_EMAIL
|
||||
]
|
||||
assert len(invites_matching_email) == 0
|
||||
|
||||
# Try to delete non-existent email and test we get 404
|
||||
bad_token = str(uuid.uuid4())
|
||||
r = requests.post(
|
||||
f"{API_PREFIX}/orgs/{non_default_org_id}/invites/delete",
|
||||
headers=admin_auth_headers,
|
||||
json={"email": "not-a-valid-invite@example.com"},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
data = r.json()
|
||||
assert data["detail"] == "invite_not_found"
|
||||
|
Loading…
Reference in New Issue
Block a user