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"""
|
"""remove invite from invite list"""
|
||||||
await self.invites.delete_one({"_id": invite_token})
|
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):
|
def accept_user_invite(self, user, invite_token: str):
|
||||||
"""remove invite from user, if valid token, throw if not"""
|
"""remove invite from user, if valid token, throw if not"""
|
||||||
invite = user.invites.pop(invite_token, "")
|
invite = user.invites.pop(invite_token, "")
|
||||||
|
@ -3,6 +3,7 @@ Organization API handling
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from typing import Dict, Union, Literal, Optional
|
from typing import Dict, Union, Literal, Optional
|
||||||
@ -39,6 +40,11 @@ class RemoveFromOrg(InviteRequest):
|
|||||||
"""Remove this user from org"""
|
"""Remove this user from org"""
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
class RemovePendingInvite(InviteRequest):
|
||||||
|
"""Delete pending invite to org by email"""
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
class RenameOrg(BaseModel):
|
class RenameOrg(BaseModel):
|
||||||
"""Request to invite another user"""
|
"""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)
|
pending_invites = await user_manager.invites.get_pending_invites(org)
|
||||||
return {"pending_invites": pending_invites}
|
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"])
|
@router.post("/remove", tags=["invites"])
|
||||||
async def remove_user_from_org(
|
async def remove_user_from_org(
|
||||||
remove: RemoveFromOrg, org: Organization = Depends(org_owner_dep)
|
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)
|
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"])
|
@users_router.get("/invites", tags=["invites"])
|
||||||
async def get_pending_invites(user: User = Depends(current_active_user)):
|
async def get_pending_invites(user: User = Depends(current_active_user)):
|
||||||
if not user.is_superuser:
|
if not user.is_superuser:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
|
import uuid
|
||||||
|
|
||||||
import pytest
|
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
|
user for user in users.values() if user["email"] == expected_stored_email
|
||||||
]
|
]
|
||||||
assert len(users_with_invited_email) == 1
|
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