diff --git a/backend/alembic/versions/f11b408e39d3_force_lowercase_all_users.py b/backend/alembic/versions/f11b408e39d3_force_lowercase_all_users.py new file mode 100644 index 000000000..48684ea88 --- /dev/null +++ b/backend/alembic/versions/f11b408e39d3_force_lowercase_all_users.py @@ -0,0 +1,36 @@ +"""force lowercase all users + +Revision ID: f11b408e39d3 +Revises: 3bd4c84fe72f +Create Date: 2025-02-26 17:04:55.683500 + +""" + + +# revision identifiers, used by Alembic. +revision = "f11b408e39d3" +down_revision = "3bd4c84fe72f" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1) Convert all existing user emails to lowercase + from alembic import op + + op.execute( + """ + UPDATE "user" + SET email = LOWER(email) + """ + ) + + # 2) Add a check constraint to ensure emails are always lowercase + op.create_check_constraint("ensure_lowercase_email", "user", "email = LOWER(email)") + + +def downgrade() -> None: + # Drop the check constraint + from alembic import op + + op.drop_constraint("ensure_lowercase_email", "user", type_="check") diff --git a/backend/alembic_tenants/versions/34e3630c7f32_lowercase_multi_tenant_user_auth.py b/backend/alembic_tenants/versions/34e3630c7f32_lowercase_multi_tenant_user_auth.py new file mode 100644 index 000000000..c98fc2ca2 --- /dev/null +++ b/backend/alembic_tenants/versions/34e3630c7f32_lowercase_multi_tenant_user_auth.py @@ -0,0 +1,42 @@ +"""lowercase multi-tenant user auth + +Revision ID: 34e3630c7f32 +Revises: a4f6ee863c47 +Create Date: 2025-02-26 15:03:01.211894 + +""" +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "34e3630c7f32" +down_revision = "a4f6ee863c47" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1) Convert all existing rows to lowercase + op.execute( + """ + UPDATE user_tenant_mapping + SET email = LOWER(email) + """ + ) + # 2) Add a check constraint so that emails cannot be written in uppercase + op.create_check_constraint( + "ensure_lowercase_email", + "user_tenant_mapping", + "email = LOWER(email)", + schema="public", + ) + + +def downgrade() -> None: + # Drop the check constraint + op.drop_constraint( + "ensure_lowercase_email", + "user_tenant_mapping", + schema="public", + type_="check", + ) diff --git a/backend/onyx/db/models.py b/backend/onyx/db/models.py index 0001ec318..6c6eadcb8 100644 --- a/backend/onyx/db/models.py +++ b/backend/onyx/db/models.py @@ -7,6 +7,7 @@ from typing import Optional from uuid import uuid4 from pydantic import BaseModel +from sqlalchemy.orm import validates from typing_extensions import TypedDict # noreorder from uuid import UUID @@ -206,6 +207,10 @@ class User(SQLAlchemyBaseUserTableUUID, Base): primaryjoin="User.id == foreign(ConnectorCredentialPair.creator_id)", ) + @validates("email") + def validate_email(self, key: str, value: str) -> str: + return value.lower() if value else value + @property def password_configured(self) -> bool: """ @@ -2270,6 +2275,10 @@ class UserTenantMapping(Base): email: Mapped[str] = mapped_column(String, nullable=False, primary_key=True) tenant_id: Mapped[str] = mapped_column(String, nullable=False) + @validates("email") + def validate_email(self, key: str, value: str) -> str: + return value.lower() if value else value + # This is a mapping from tenant IDs to anonymous user paths class TenantAnonymousUserPath(Base): diff --git a/web/src/app/auth/login/EmailPasswordForm.tsx b/web/src/app/auth/login/EmailPasswordForm.tsx index 6f7a48cc9..68e1d2154 100644 --- a/web/src/app/auth/login/EmailPasswordForm.tsx +++ b/web/src/app/auth/login/EmailPasswordForm.tsx @@ -36,11 +36,14 @@ export function EmailPasswordForm({ {popup} value.toLowerCase()), password: Yup.string().required(), })} onSubmit={async (values) => {