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:
parent
8bcdc8877f
commit
081d6f8519
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user