Support Invite Info APIs (#82)

* backend: support exposing info about a particular invite, fixes part of #35
new apis are:
- GET /users/invite/{token}?email={email} - no auth needed, get invite to new user
- GET /users/me/invite/{token} - with auth, to get invite to join an archive for an existing user

* get archive.name as well if invite is adding to an archive

* first camelCase typo
This commit is contained in:
Ilya Kreymer 2022-01-14 22:53:02 -08:00 committed by GitHub
parent b9622df9e6
commit c561fe3af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 57 additions and 27 deletions

View File

@ -158,10 +158,10 @@ class ArchiveOps:
return [Archive.from_dict(res) for res in results] return [Archive.from_dict(res) for res in results]
async def get_archive_for_user_by_id( async def get_archive_for_user_by_id(
self, uid: str, user: User, role: UserRole = UserRole.VIEWER self, aid: str, user: User, role: UserRole = UserRole.VIEWER
): ):
"""Get an archive for user by unique id""" """Get an archive for user by unique id"""
query = {f"users.{user.id}": {"$gte": role.value}, "_id": uid} query = {f"users.{user.id}": {"$gte": role.value}, "_id": aid}
res = await self.archives.find_one(query) res = await self.archives.find_one(query)
return Archive.from_dict(res) return Archive.from_dict(res)
@ -184,7 +184,7 @@ class ArchiveOps:
async def handle_new_user_invite(self, invite_token: str, user: User): async def handle_new_user_invite(self, invite_token: str, user: User):
"""Handle invite from a new user""" """Handle invite from a new user"""
new_user_invite = await self.invites.get_valid_invite(invite_token, user) new_user_invite = await self.invites.get_valid_invite(invite_token, user.email)
await self.add_user_by_invite(new_user_invite, user) await self.add_user_by_invite(new_user_invite, user)
await self.invites.remove_invite(invite_token) await self.invites.remove_invite(invite_token)
return True return True

View File

@ -22,12 +22,14 @@ class UserRole(IntEnum):
# ============================================================================ # ============================================================================
class InvitePending(BaseModel): class InvitePending(BaseMongoModel):
"""Pending Request to join""" """An invite for a new user, with an email and invite token as id"""
created: datetime created: datetime
inviterEmail: str
aid: Optional[str] aid: Optional[str]
role: Optional[UserRole] = UserRole.VIEWER role: Optional[UserRole] = UserRole.VIEWER
email: Optional[str]
# ============================================================================ # ============================================================================
@ -44,13 +46,6 @@ class InviteToArchiveRequest(InviteRequest):
role: UserRole role: UserRole
# ============================================================================
class NewUserInvite(InvitePending, BaseMongoModel):
"""An invite for a new user, with an email and invite token as id"""
email: str
# ============================================================================ # ============================================================================
class InviteOps: class InviteOps:
""" invite users (optionally to an archive), send emails and delete invites """ """ invite users (optionally to an archive), send emails and delete invites """
@ -61,8 +56,7 @@ class InviteOps:
async def add_new_user_invite( async def add_new_user_invite(
self, self,
new_user_invite: NewUserInvite, new_user_invite: InvitePending,
inviter_email: str,
archive_name: Optional[str], archive_name: Optional[str],
headers: Optional[dict], headers: Optional[dict],
): ):
@ -78,23 +72,21 @@ class InviteOps:
self.email.send_new_user_invite( self.email.send_new_user_invite(
new_user_invite.email, new_user_invite.email,
inviter_email, new_user_invite.inviterEmail,
archive_name, archive_name,
new_user_invite.id, new_user_invite.id,
headers, headers,
) )
async def get_valid_invite(self, invite_token: str, user): async def get_valid_invite(self, invite_token: str, email):
""" Retrieve a valid invite data from db, or throw if invalid""" """ Retrieve a valid invite data from db, or throw if invalid"""
invite_data = await self.invites.find_one({"_id": invite_token}) invite_data = await self.invites.find_one({"_id": invite_token})
if not invite_data: if not invite_data:
print("NO DATA", flush=True)
raise HTTPException(status_code=400, detail="Invalid Invite Code") raise HTTPException(status_code=400, detail="Invalid Invite Code")
new_user_invite = NewUserInvite.from_dict(invite_data) new_user_invite = InvitePending.from_dict(invite_data)
print(new_user_invite, flush=True)
if user.email != new_user_invite.email: if email != new_user_invite.email:
raise HTTPException(status_code=400, detail="Invalid Invite Code") raise HTTPException(status_code=400, detail="Invalid Invite Code")
return new_user_invite return new_user_invite
@ -133,19 +125,19 @@ class InviteOps:
archive_name = archive.name archive_name = archive.name
invite_pending = InvitePending( invite_pending = InvitePending(
id=invite_code,
aid=aid, aid=aid,
created=datetime.utcnow(), created=datetime.utcnow(),
role=invite.role if hasattr(invite, "role") else None, role=invite.role if hasattr(invite, "role") else None,
email=invite.email,
inviterEmail=user.email
) )
other_user = await user_manager.user_db.get_by_email(invite.email) other_user = await user_manager.user_db.get_by_email(invite.email)
if not other_user: if not other_user:
await self.add_new_user_invite( await self.add_new_user_invite(
NewUserInvite( invite_pending,
id=invite_code, email=invite.email, **invite_pending.dict()
),
user.email,
archive_name, archive_name,
headers, headers,
) )
@ -162,6 +154,8 @@ class InviteOps:
status_code=400, detail="User already a member of this archive." status_code=400, detail="User already a member of this archive."
) )
# no need to store our own email as adding invite to user
invite_pending.email = None
other_user.invites[invite_code] = invite_pending other_user.invites[invite_code] = invite_pending
await user_manager.user_db.update(other_user) await user_manager.user_db.update(other_user)

View File

@ -124,7 +124,7 @@ class UserManager(BaseUserManager[UserCreate, UserDB]):
raise HTTPException(status_code=400, detail="Invite Token Required") raise HTTPException(status_code=400, detail="Invite Token Required")
if user.inviteToken and not await self.invites.get_valid_invite( if user.inviteToken and not await self.invites.get_valid_invite(
user.inviteToken, user user.inviteToken, user.email
): ):
raise HTTPException(status_code=400, detail="Invalid Invite Token") raise HTTPException(status_code=400, detail="Invalid Invite Token")
@ -153,7 +153,13 @@ class UserManager(BaseUserManager[UserCreate, UserDB]):
try: try:
res = await self.create( res = await self.create(
UserCreate(email=email, password=password, is_superuser=True, newArchive=False, is_verified=True) UserCreate(
email=email,
password=password,
is_superuser=True,
newArchive=False,
is_verified=True,
)
) )
print(f"Super user {email} created", flush=True) print(f"Super user {email} created", flush=True)
print(res, flush=True) print(res, flush=True)
@ -207,7 +213,6 @@ class UserManager(BaseUserManager[UserCreate, UserDB]):
user.email, token, request and request.headers user.email, token, request and request.headers
) )
###pylint: disable=no-self-use, unused-argument
async def on_after_request_verify( async def on_after_request_verify(
self, user: UserDB, token: str, request: Optional[Request] = None self, user: UserDB, token: str, request: Optional[Request] = None
): ):
@ -215,6 +220,19 @@ class UserManager(BaseUserManager[UserCreate, UserDB]):
self.email.send_user_validation(user.email, token, request and request.headers) self.email.send_user_validation(user.email, token, request and request.headers)
async def format_invite(self, invite):
""" format an InvitePending to return via api, resolve name of inviter """
inviter = await self.get_by_email(invite.inviterEmail)
result = invite.serialize()
result["inviterName"] = inviter.name
if invite.aid:
archive = await self.archive_ops.get_archive_for_user_by_id(invite.aid, inviter)
result["archiveName"] = archive.name
return result
# ============================================================================ # ============================================================================
def init_user_manager(mdb, emailsender, invites): def init_user_manager(mdb, emailsender, invites):
@ -299,6 +317,24 @@ def init_users_api(app, user_manager):
return {"invited": "new_user"} return {"invited": "new_user"}
@users_router.get("/invite/{token}", tags=["invites"])
async def get_invite_info(token: str, email: str):
invite = await user_manager.invites.get_valid_invite(token, email)
return await user_manager.format_invite(invite)
@users_router.get("/me/invite/{token}", tags=["invites"])
async def get_existing_user_invite_info(token: str,
user: User = Depends(current_active_user)):
try:
invite = user.invites[token]
except:
# pylint: disable=raise-missing-from
raise HTTPException(status_code=400, detail="Invalid Invite Code")
return await user_manager.format_invite(invite)
app.include_router(users_router, prefix="/users", tags=["users"]) app.include_router(users_router, prefix="/users", tags=["users"])
asyncio.create_task(user_manager.create_super_user()) asyncio.create_task(user_manager.create_super_user())