fastapi-users / fastapi-users-db-beanie

FastAPI Users database adapter for Beanie
MIT License
5 stars 4 forks source link

BeanieBaseUserDocument unique email index not being applied, multiple users with the same email in the database #14

Open mthanded opened 9 months ago

mthanded commented 9 months ago

Describe the bug

BeanieBaseUserDocument defines 2 indexes on the email field , one being a unique index and the other being a collation. However only the collation index is applied to the email field in the database. This means that multiple users with the same emaill can end up in the database.

To Reproduce

To make reproducing this simpler i have lifted the BeanieBaseUserDocument model directly from this repo and put it into a minimal example.

Model from https://github.com/fastapi-users/fastapi-users-db-beanie/blob/eedd1039cd8d3aece0f49fa1a90d9649ca80d1ee/fastapi_users_db_beanie/__init__.py#L16

import asyncio
from typing import Optional

from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel

from beanie import Document, Indexed, init_beanie
from pymongo import IndexModel
from pymongo.collation import Collation

class BeanieBaseUser(BaseModel):
    email: str
    hashed_password: str
    is_active: bool = True
    is_superuser: bool = False
    is_verified: bool = False

    class Settings:
        email_collation = Collation("en", strength=2)
        indexes = [
            IndexModel("email", unique=True),
            IndexModel(
                "email", name="case_insensitive_email_index", collation=email_collation
            ),
        ]

class BeanieBaseUserDocument(BeanieBaseUser, Document):  # type: ignore
    pass

async def example():
    client = AsyncIOMotorClient("mongodb://localhost:27017")
    await init_beanie(database=client.db_name, document_models=[BeanieBaseUserDocument])

    await BeanieBaseUserDocument(email="email@email.email",hashed_password="hashed_password").save()
    await BeanieBaseUserDocument(email="email@email.email",hashed_password="hashed_password1").save()

if __name__ == "__main__":
    asyncio.run(example())

The behavior is not as expected:

Expected behavior

The database should prevent duplicate emails from being added via the indexes.

Additional context

class BeanieBaseUser(BaseModel):
    email: str
    hashed_password: str
    is_active: bool = True
    is_superuser: bool = False
    is_verified: bool = False

    class Settings:
        email_collation = Collation("en", strength=2)
        indexes = [
            IndexModel(
                "email", name="case_insensitive_email_index_unique", collation=email_collation, unique=True
            ),
        ]

Software versions

frankie567 commented 8 months ago

Thank you @mthanded for the very detailed error report. I'm surprised both indices are not created properly 🤔

Anyway, as you suggest, it makes more sense to directly have the collation index unique since it's the behavior we adopt on Python's side.

PR welcome to solve this :)