mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-08-14 18:58:10 +08:00
* Add CRU superuser routes * Add delete method on DB adapters * Add superuser delete route * Add superuser routes documentation * Pass black formatter
This commit is contained in:
@ -129,3 +129,96 @@ Update the current authenticated active user.
|
||||
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
## Superuser
|
||||
|
||||
### `GET /`
|
||||
|
||||
Return the list of registered users.
|
||||
|
||||
!!! success "`200 OK`"
|
||||
```json
|
||||
[{
|
||||
"id": "57cbb51a-ab71-4009-8802-3f54b4f2e23",
|
||||
"email": "king.arthur@camelot.bt",
|
||||
"is_active": true,
|
||||
"is_superuser": false
|
||||
}]
|
||||
```
|
||||
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
!!! fail "`403 Forbidden`"
|
||||
Not a superuser.
|
||||
|
||||
### `GET /{user_id}`
|
||||
|
||||
Return the user with id `user_id`.
|
||||
|
||||
!!! success "`200 OK`"
|
||||
```json
|
||||
{
|
||||
"id": "57cbb51a-ab71-4009-8802-3f54b4f2e23",
|
||||
"email": "king.arthur@camelot.bt",
|
||||
"is_active": true,
|
||||
"is_superuser": false
|
||||
}
|
||||
```
|
||||
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
!!! fail "`403 Forbidden`"
|
||||
Not a superuser.
|
||||
|
||||
!!! fail "`404 Not found`"
|
||||
The user does not exist.
|
||||
|
||||
### `PATCH /{user_id}`
|
||||
|
||||
Update the user with id `user_id`.
|
||||
|
||||
!!! abstract "Payload"
|
||||
```json
|
||||
{
|
||||
"email": "king.arthur@tintagel.bt",
|
||||
"password": "merlin",
|
||||
"is_active": false,
|
||||
"is_superuser": true
|
||||
}
|
||||
```
|
||||
|
||||
!!! success "`200 OK`"
|
||||
```json
|
||||
{
|
||||
"id": "57cbb51a-ab71-4009-8802-3f54b4f2e23",
|
||||
"email": "king.arthur@camelot.bt",
|
||||
"is_active": false,
|
||||
"is_superuser": true
|
||||
}
|
||||
```
|
||||
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
!!! fail "`403 Forbidden`"
|
||||
Not a superuser.
|
||||
|
||||
!!! fail "`404 Not found`"
|
||||
The user does not exist.
|
||||
|
||||
### `DELETE /{user_id}`
|
||||
|
||||
Delete the user with id `user_id`.
|
||||
|
||||
!!! success "`204 No content`"
|
||||
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
!!! fail "`403 Forbidden`"
|
||||
Not a superuser.
|
||||
|
||||
!!! fail "`404 Not found`"
|
||||
The user does not exist.
|
||||
|
@ -29,6 +29,10 @@ class BaseUserDatabase:
|
||||
"""Update a user."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def delete(self, user: BaseUserDB) -> None:
|
||||
"""Delete a user."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def authenticate(
|
||||
self, credentials: OAuth2PasswordRequestForm
|
||||
) -> Optional[BaseUserDB]:
|
||||
|
@ -38,3 +38,6 @@ class MongoDBUserDatabase(BaseUserDatabase):
|
||||
async def update(self, user: BaseUserDB) -> BaseUserDB:
|
||||
await self.collection.replace_one({"id": user.id}, user.dict())
|
||||
return user
|
||||
|
||||
async def delete(self, user: BaseUserDB) -> None:
|
||||
await self.collection.delete_one({"id": user.id})
|
||||
|
@ -57,3 +57,7 @@ class SQLAlchemyUserDatabase(BaseUserDatabase):
|
||||
)
|
||||
await self.database.execute(query)
|
||||
return user
|
||||
|
||||
async def delete(self, user: BaseUserDB) -> None:
|
||||
query = self.users.delete().where(self.users.c.id == user.id)
|
||||
await self.database.execute(query)
|
||||
|
@ -23,6 +23,9 @@ class BaseUser(BaseModel):
|
||||
skip_defaults=True, exclude={"id", "is_superuser", "is_active"}
|
||||
)
|
||||
|
||||
def create_update_dict_superuser(self):
|
||||
return self.dict(skip_defaults=True, exclude={"id"})
|
||||
|
||||
|
||||
class BaseUserCreate(BaseUser):
|
||||
email: EmailStr
|
||||
|
@ -54,6 +54,24 @@ def get_user_router(
|
||||
reset_password_token_audience = "fastapi-users:reset"
|
||||
|
||||
get_current_active_user = auth.get_current_active_user(user_db)
|
||||
get_current_superuser = auth.get_current_superuser(user_db)
|
||||
|
||||
async def _get_or_404(id: str) -> models.UserDB: # type: ignore
|
||||
user = await user_db.get(id)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
return user
|
||||
|
||||
async def _update_user(
|
||||
user: models.UserDB, update_dict: typing.Dict[str, typing.Any] # type: ignore
|
||||
):
|
||||
for field in update_dict:
|
||||
if field == "password":
|
||||
hashed_password = get_password_hash(update_dict[field])
|
||||
user.hashed_password = hashed_password
|
||||
else:
|
||||
setattr(user, field, update_dict[field])
|
||||
return await user_db.update(user)
|
||||
|
||||
@router.post(
|
||||
"/register", response_model=models.User, status_code=status.HTTP_201_CREATED
|
||||
@ -136,13 +154,38 @@ def get_user_router(
|
||||
user: models.UserDB = Depends(get_current_active_user), # type: ignore
|
||||
):
|
||||
updated_user_data = updated_user.create_update_dict()
|
||||
for field in updated_user_data:
|
||||
if field == "password":
|
||||
hashed_password = get_password_hash(updated_user_data[field])
|
||||
user.hashed_password = hashed_password
|
||||
else:
|
||||
setattr(user, field, updated_user_data[field])
|
||||
return await _update_user(user, updated_user_data)
|
||||
|
||||
return await user_db.update(user)
|
||||
@router.get("/", response_model=typing.List[models.User]) # type: ignore
|
||||
async def list_users(
|
||||
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
|
||||
):
|
||||
return await user_db.list()
|
||||
|
||||
@router.get("/{id}", response_model=models.User)
|
||||
async def get_user(
|
||||
id: str,
|
||||
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
|
||||
):
|
||||
return await _get_or_404(id)
|
||||
|
||||
@router.patch("/{id}", response_model=models.User)
|
||||
async def update_user(
|
||||
id: str,
|
||||
updated_user: models.UserUpdate, # type: ignore
|
||||
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
|
||||
):
|
||||
user = await _get_or_404(id)
|
||||
updated_user_data = updated_user.create_update_dict_superuser()
|
||||
return await _update_user(user, updated_user_data)
|
||||
|
||||
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
id: str,
|
||||
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
|
||||
):
|
||||
user = await _get_or_404(id)
|
||||
await user_db.delete(user)
|
||||
return None
|
||||
|
||||
return router
|
||||
|
@ -75,6 +75,9 @@ def mock_user_db(user, inactive_user, superuser) -> BaseUserDatabase:
|
||||
async def update(self, user: BaseUserDB) -> BaseUserDB:
|
||||
return user
|
||||
|
||||
async def delete(self, user: BaseUserDB) -> None:
|
||||
pass
|
||||
|
||||
return MockUserDatabase()
|
||||
|
||||
|
||||
|
@ -31,6 +31,9 @@ async def test_not_implemented_methods(user):
|
||||
with pytest.raises(NotImplementedError):
|
||||
await base_user_db.update(user)
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
await base_user_db.delete(user)
|
||||
|
||||
|
||||
class TestAuthenticate:
|
||||
@pytest.mark.asyncio
|
||||
|
@ -61,3 +61,8 @@ async def test_queries(mongodb_user_db):
|
||||
# Unknown user
|
||||
unknown_user = await mongodb_user_db.get_by_email("galahad@camelot.bt")
|
||||
assert unknown_user is None
|
||||
|
||||
# Delete user
|
||||
await mongodb_user_db.delete(user)
|
||||
deleted_user = await mongodb_user_db.get(user.id)
|
||||
assert deleted_user is None
|
||||
|
@ -79,3 +79,8 @@ async def test_queries(sqlalchemy_user_db):
|
||||
# Unknown user
|
||||
unknown_user = await sqlalchemy_user_db.get_by_email("galahad@camelot.bt")
|
||||
assert unknown_user is None
|
||||
|
||||
# Delete user
|
||||
await sqlalchemy_user_db.delete(user)
|
||||
deleted_user = await sqlalchemy_user_db.get(user.id)
|
||||
assert deleted_user is None
|
||||
|
@ -37,7 +37,7 @@ def fastapi_users(request, mock_user_db, mock_authentication) -> FastAPIUsers:
|
||||
@pytest.fixture()
|
||||
def test_app_client(fastapi_users) -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(fastapi_users.router)
|
||||
app.include_router(fastapi_users.router, prefix="/users")
|
||||
|
||||
@app.get("/current-user")
|
||||
def current_user(user=Depends(fastapi_users.get_current_user)):
|
||||
@ -63,16 +63,25 @@ class TestFastAPIUsers:
|
||||
|
||||
class TestRouter:
|
||||
def test_routes_exist(self, test_app_client: TestClient):
|
||||
response = test_app_client.post("/register")
|
||||
response = test_app_client.post("/users/register")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
|
||||
response = test_app_client.post("/login")
|
||||
response = test_app_client.post("/users/login")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
|
||||
response = test_app_client.post("/forgot-password")
|
||||
response = test_app_client.post("/users/forgot-password")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
|
||||
response = test_app_client.post("/reset-password")
|
||||
response = test_app_client.post("/users/reset-password")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
|
||||
response = test_app_client.get("/users")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
|
||||
response = test_app_client.get("/users/aaa")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
|
||||
response = test_app_client.patch("/users/aaa")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
|
@ -357,3 +357,195 @@ class TestUpdateMe:
|
||||
|
||||
updated_user = mock_user_db.update.call_args[0][0]
|
||||
assert updated_user.hashed_password != current_hashed_passord
|
||||
|
||||
|
||||
class TestListUsers:
|
||||
def test_missing_token(self, test_app_client: TestClient):
|
||||
response = test_app_client.get("/")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
|
||||
response = test_app_client.get(
|
||||
"/", headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_superuser(self, test_app_client: TestClient, superuser: BaseUserDB):
|
||||
response = test_app_client.get(
|
||||
"/", headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
response_json = response.json()
|
||||
assert len(response_json) == 3
|
||||
for user in response_json:
|
||||
assert "id" in user
|
||||
assert "hashed_password" not in user
|
||||
|
||||
|
||||
class TestGetUser:
|
||||
def test_missing_token(self, test_app_client: TestClient):
|
||||
response = test_app_client.get("/000")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
|
||||
response = test_app_client.get(
|
||||
"/000", headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_not_existing_user(
|
||||
self, test_app_client: TestClient, superuser: BaseUserDB
|
||||
):
|
||||
response = test_app_client.get(
|
||||
"/000", headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_superuser(
|
||||
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
|
||||
):
|
||||
response = test_app_client.get(
|
||||
f"/{user.id}", headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
response_json = response.json()
|
||||
assert response_json["id"] == user.id
|
||||
assert "hashed_password" not in response_json
|
||||
|
||||
|
||||
class TestUpdateUser:
|
||||
def test_missing_token(self, test_app_client: TestClient):
|
||||
response = test_app_client.patch("/000")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
|
||||
response = test_app_client.patch(
|
||||
"/000", headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_not_existing_user(
|
||||
self, test_app_client: TestClient, superuser: BaseUserDB
|
||||
):
|
||||
response = test_app_client.patch(
|
||||
"/000", json={}, headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_empty_body(
|
||||
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
|
||||
):
|
||||
response = test_app_client.patch(
|
||||
f"/{user.id}", json={}, headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
response_json = response.json()
|
||||
assert response_json["email"] == user.email
|
||||
|
||||
def test_valid_body(
|
||||
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
|
||||
):
|
||||
json = {"email": "king.arthur@tintagel.bt"}
|
||||
response = test_app_client.patch(
|
||||
f"/{user.id}",
|
||||
json=json,
|
||||
headers={"Authorization": f"Bearer {superuser.id}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
response_json = response.json()
|
||||
assert response_json["email"] == "king.arthur@tintagel.bt"
|
||||
|
||||
def test_valid_body_is_superuser(
|
||||
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
|
||||
):
|
||||
json = {"is_superuser": True}
|
||||
response = test_app_client.patch(
|
||||
f"/{user.id}",
|
||||
json=json,
|
||||
headers={"Authorization": f"Bearer {superuser.id}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
response_json = response.json()
|
||||
assert response_json["is_superuser"] is True
|
||||
|
||||
def test_valid_body_is_active(
|
||||
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
|
||||
):
|
||||
json = {"is_active": False}
|
||||
response = test_app_client.patch(
|
||||
f"/{user.id}",
|
||||
json=json,
|
||||
headers={"Authorization": f"Bearer {superuser.id}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
response_json = response.json()
|
||||
assert response_json["is_active"] is False
|
||||
|
||||
def test_valid_body_password(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: TestClient,
|
||||
user: BaseUserDB,
|
||||
superuser: BaseUserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
current_hashed_passord = user.hashed_password
|
||||
|
||||
json = {"password": "merlin"}
|
||||
response = test_app_client.patch(
|
||||
f"/{user.id}",
|
||||
json=json,
|
||||
headers={"Authorization": f"Bearer {superuser.id}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert mock_user_db.update.called is True
|
||||
|
||||
updated_user = mock_user_db.update.call_args[0][0]
|
||||
assert updated_user.hashed_password != current_hashed_passord
|
||||
|
||||
|
||||
class TestDeleteUser:
|
||||
def test_missing_token(self, test_app_client: TestClient):
|
||||
response = test_app_client.delete("/000")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
|
||||
response = test_app_client.delete(
|
||||
"/000", headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_not_existing_user(
|
||||
self, test_app_client: TestClient, superuser: BaseUserDB
|
||||
):
|
||||
response = test_app_client.delete(
|
||||
"/000", headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_superuser(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: TestClient,
|
||||
user: BaseUserDB,
|
||||
superuser: BaseUserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "delete")
|
||||
|
||||
response = test_app_client.delete(
|
||||
f"/{user.id}", headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert response.json() is None
|
||||
assert mock_user_db.delete.called is True
|
||||
|
||||
deleted_user = mock_user_db.delete.call_args[0][0]
|
||||
assert deleted_user.id == user.id
|
||||
|
Reference in New Issue
Block a user