diff --git a/backend/archives.py b/backend/archives.py index f2919a74..c75fa3b6 100644 --- a/backend/archives.py +++ b/backend/archives.py @@ -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 diff --git a/backend/main.py b/backend/main.py index dd01fc1c..5553820f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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) diff --git a/backend/users.py b/backend/users.py index 39a085c2..c4368014 100644 --- a/backend/users.py +++ b/backend/users.py @@ -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",