User Display Name Support + Token Refresh Support (#44)

* backend api/data model improvements:
- add 'name' property to user, can be set on registration, fixes #43
- in archive user list, include 'name' and 'role' for each user
- don't include is_* property in user create/register and update
- add /auth/jwt/refresh endpoint for refreshing token, fixes #34, support for #22

* allow jwt token lifetime to be settable via JWT_LIFETIME env var (default 3600)
This commit is contained in:
Ilya Kreymer 2021-12-01 18:55:10 -08:00 committed by GitHub
parent 8bcdc8877f
commit 081d6f8519
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 75 additions and 24 deletions

View File

@ -87,7 +87,7 @@ class Archive(BaseMongoModel):
return res >= value
def serialize_for_user(self, user: User):
async def serialize_for_user(self, user: User, user_manager):
"""Serialize based on current user access"""
exclude = {"storage"}
@ -97,13 +97,30 @@ class Archive(BaseMongoModel):
if not self.is_crawler(user):
exclude.add("usage")
return self.dict(
result = self.dict(
exclude_unset=True,
exclude_defaults=True,
exclude_none=True,
exclude=exclude,
)
if self.is_owner(user):
keys = list(result["users"].keys())
user_list = await user_manager.get_user_names_by_ids(keys)
for archive_user in user_list:
id_ = str(archive_user["id"])
role = result["users"].get(id_)
if not role:
continue
result["users"][id_] = {
"role": role,
"name": archive_user.get("name", ""),
}
return result
# ============================================================================
class ArchiveOps:
@ -235,7 +252,7 @@ class ArchiveOps:
# ============================================================================
def init_archives_api(app, mdb, users, email, user_dep: User):
def init_archives_api(app, mdb, user_manager, email, user_dep: User):
"""Init archives api router for /archives"""
ops = ArchiveOps(mdb, email)
@ -281,13 +298,17 @@ def init_archives_api(app, mdb, users, email, user_dep: User):
@app.get("/archives", tags=["archives"])
async def get_archives(user: User = Depends(user_dep)):
results = await ops.get_archives_for_user(user)
return {"archives": [res.serialize_for_user(user) for res in results]}
return {
"archives": [
await res.serialize_for_user(user, user_manager) for res in results
]
}
@router.get("", tags=["archives"])
async def get_archive(
archive: Archive = Depends(archive_dep), user: User = Depends(user_dep)
):
return archive.serialize_for_user(user)
return await archive.serialize_for_user(user, user_manager)
@router.post("/invite", tags=["invites"])
async def invite_user(
@ -301,7 +322,7 @@ def init_archives_api(app, mdb, users, email, user_dep: User):
aid=str(archive.id), created=datetime.utcnow(), role=invite.role
)
other_user = await users.user_db.get_by_email(invite.email)
other_user = await user_manager.user_db.get_by_email(invite.email)
if not other_user:
@ -318,14 +339,14 @@ def init_archives_api(app, mdb, users, email, user_dep: User):
if other_user.email == user.email:
raise HTTPException(status_code=400, detail="Can't invite ourselves!")
if archive.users.get(str(other_user.id)):
if archive.user_manager.get(str(other_user.id)):
raise HTTPException(
status_code=400, detail="User already a member of this archive."
)
other_user.invites[invite_code] = invite_pending
await users.user_db.update(other_user)
await user_manager.user_db.update(other_user)
return {
"invited": "existing_user",
@ -338,7 +359,7 @@ def init_archives_api(app, mdb, users, email, user_dep: User):
user: User = Depends(user_dep),
):
other_user = await users.user_db.get_by_email(update.email)
other_user = await user_manager.user_db.get_by_email(update.email)
if not other_user:
raise HTTPException(
status_code=400, detail="No user found for specified e-mail"
@ -359,7 +380,7 @@ def init_archives_api(app, mdb, users, email, user_dep: User):
raise HTTPException(status_code=400, detail="Invalid Invite Code")
await ops.add_user_by_invite(invite, user)
await users.user_db.update(user)
await user_manager.user_db.update(user)
return {"added": True}
return ops

View File

@ -36,9 +36,7 @@ def main():
current_active_user = fastapi_users.current_user(active=True)
archive_ops = init_archives_api(
app, mdb, user_manager, email, current_active_user
)
archive_ops = init_archives_api(app, mdb, user_manager, email, current_active_user)
user_manager.set_archive_ops(archive_ops)
@ -70,9 +68,7 @@ def main():
archive_ops,
)
coll_ops = init_collections_api(
mdb, crawls, archive_ops, crawl_manager
)
coll_ops = init_collections_api(mdb, crawls, archive_ops, crawl_manager)
crawl_config_ops.set_coll_ops(coll_ops)

View File

@ -11,10 +11,9 @@ from datetime import datetime
from typing import Dict, Optional
from enum import IntEnum
from pydantic import BaseModel, EmailStr, UUID4
from pydantic import BaseModel
from fastapi import Request, HTTPException
from fastapi import Request, Response, HTTPException, Depends
from fastapi_users import FastAPIUsers, models, BaseUserManager
from fastapi_users.authentication import JWTAuthentication
@ -24,6 +23,8 @@ from fastapi_users.db import MongoDBUserDatabase
# ============================================================================
PASSWORD_SECRET = os.environ.get("PASSWORD_SECRET", uuid.uuid4().hex)
JWT_LIFETIME = int(os.environ.get("JWT_LIFETIME", 3600))
# ============================================================================
class UserRole(IntEnum):
@ -49,23 +50,36 @@ class User(models.BaseUser):
Base User Model
"""
name: Optional[str] = ""
# ============================================================================
class UserCreate(models.BaseUserCreate):
# use custom model as model.BaseUserCreate includes is_* fields which should not be set
class UserCreate(models.CreateUpdateDictModel):
"""
User Creation Model
"""
email: EmailStr
password: str
name: Optional[str] = ""
inviteToken: Optional[str]
newArchive: bool
newArchiveName: Optional[str] = ""
# ============================================================================
class UserUpdate(User, models.BaseUserUpdate):
class UserUpdate(User, models.CreateUpdateDictModel):
"""
User Update Model
"""
password: Optional[str]
email: Optional[EmailStr]
# ============================================================================
class UserDB(User, models.BaseUserDB):
@ -85,6 +99,7 @@ class UserDBOps(MongoDBUserDatabase):
# ============================================================================
class UserManager(BaseUserManager[UserCreate, UserDB]):
""" Browsertrix UserManager """
user_db_model = UserDB
reset_password_token_secret = PASSWORD_SECRET
verification_token_secret = PASSWORD_SECRET
@ -98,6 +113,14 @@ class UserManager(BaseUserManager[UserCreate, UserDB]):
""" set archive ops """
self.archive_ops = ops
async def get_user_names_by_ids(self, user_ids):
""" return list of user names for given ids """
user_ids = [UUID4(id_) for id_ in user_ids]
cursor = self.user_db.collection.find(
{"id": {"$in": user_ids}}, projection=["id", "name"]
)
return await cursor.to_list(length=1000)
# pylint: disable=no-self-use, unused-argument
async def on_after_register(self, user: UserDB, request: Optional[Request] = None):
"""callback after registeration"""
@ -109,7 +132,7 @@ class UserManager(BaseUserManager[UserCreate, UserDB]):
if req_data.get("newArchive"):
print(f"Creating new archive for {user.id}")
archive_name = req_data.get("name") or f"{user.email} Archive"
archive_name = req_data.get("newArchiveName") or f"{user.name}'s Archive"
await self.archive_ops.create_new_archive_for_user(
archive_name=archive_name,
@ -161,7 +184,9 @@ def init_user_manager(mdb, emailsender):
def init_users_api(app, user_manager):
""" init fastapi_users """
jwt_authentication = JWTAuthentication(
secret=PASSWORD_SECRET, lifetime_seconds=3600, tokenUrl="/auth/jwt/login"
secret=PASSWORD_SECRET,
lifetime_seconds=JWT_LIFETIME,
tokenUrl="/auth/jwt/login",
)
fastapi_users = FastAPIUsers(
@ -173,11 +198,20 @@ def init_users_api(app, user_manager):
UserDB,
)
auth_router = fastapi_users.get_auth_router(jwt_authentication)
@auth_router.post("/refresh")
async def refresh_jwt(
response: Response, user=Depends(fastapi_users.current_user(active=True))
):
return await jwt_authentication.get_login_response(user, response, user_manager)
app.include_router(
fastapi_users.get_auth_router(jwt_authentication),
auth_router,
prefix="/auth/jwt",
tags=["auth"],
)
app.include_router(
fastapi_users.get_register_router(),
prefix="/auth",