From 7b4928b32a2ef1903e44ff0437d13c53faa35c2e Mon Sep 17 00:00:00 2001 From: Lina Date: Thu, 6 Nov 2025 15:20:03 +0100 Subject: [PATCH 1/5] Inbox mail table --- .../versions/ebf4435d83f8_inbox_mail_table.py | 53 +++++++++++++++++++ submodules/model | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/ebf4435d83f8_inbox_mail_table.py diff --git a/alembic/versions/ebf4435d83f8_inbox_mail_table.py b/alembic/versions/ebf4435d83f8_inbox_mail_table.py new file mode 100644 index 00000000..c77e1443 --- /dev/null +++ b/alembic/versions/ebf4435d83f8_inbox_mail_table.py @@ -0,0 +1,53 @@ +"""Inbox mail table + +Revision ID: ebf4435d83f8 +Revises: 199a0d8aefbe +Create Date: 2025-11-06 13:44:57.938408 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'ebf4435d83f8' +down_revision = '199a0d8aefbe' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('inbox_mail', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('send_from', sa.String(), nullable=True), + sa.Column('send_to', sa.JSON(), nullable=True), + sa.Column('subject', sa.String(), nullable=True), + sa.Column('content', sa.String(), nullable=True), + sa.Column('mark_as_important', sa.Boolean(), nullable=True), + sa.Column('meta_data', sa.JSON(), nullable=True), + sa.Column('is_seen', sa.Boolean(), nullable=True), + sa.Column('being_worked_on', sa.Boolean(), nullable=True), + sa.Column('parent_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('child_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint(['child_id'], ['global.inbox_mail.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organization.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['parent_id'], ['global.inbox_mail.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + schema='global' + ) + op.create_index(op.f('ix_global_inbox_mail_child_id'), 'inbox_mail', ['child_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_organization_id'), 'inbox_mail', ['organization_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_parent_id'), 'inbox_mail', ['parent_id'], unique=False, schema='global') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_global_inbox_mail_parent_id'), table_name='inbox_mail', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_organization_id'), table_name='inbox_mail', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_child_id'), table_name='inbox_mail', schema='global') + op.drop_table('inbox_mail', schema='global') + # ### end Alembic commands ### diff --git a/submodules/model b/submodules/model index c996e22c..9bf0041a 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit c996e22c1ec38ef43a46b495aa1e450a8383df50 +Subproject commit 9bf0041aa3b8ee3bde2f937848090191dcbebbfe From 313ce435b5a35ce11d319be4e634ce4729a2173f Mon Sep 17 00:00:00 2001 From: LennartSchmidtKern Date: Tue, 11 Nov 2025 15:19:15 +0100 Subject: [PATCH 2/5] model --- submodules/model | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/model b/submodules/model index 9bf0041a..5afff308 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit 9bf0041aa3b8ee3bde2f937848090191dcbebbfe +Subproject commit 5afff308f051cb6e5a52f6632cc5d37b6c3a5d5d From c21b13b91bdd6bd1f0924d5a3906045164096966 Mon Sep 17 00:00:00 2001 From: LennartSchmidtKern Date: Tue, 11 Nov 2025 15:20:43 +0100 Subject: [PATCH 3/5] alembic merge --- .../versions/ebf4435d83f8_inbox_mail_table.py | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/alembic/versions/ebf4435d83f8_inbox_mail_table.py b/alembic/versions/ebf4435d83f8_inbox_mail_table.py index c77e1443..f4f4ad67 100644 --- a/alembic/versions/ebf4435d83f8_inbox_mail_table.py +++ b/alembic/versions/ebf4435d83f8_inbox_mail_table.py @@ -1,53 +1,87 @@ """Inbox mail table Revision ID: ebf4435d83f8 -Revises: 199a0d8aefbe +Revises: 85bb3ebee137 Create Date: 2025-11-06 13:44:57.938408 """ + from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = 'ebf4435d83f8' -down_revision = '199a0d8aefbe' +revision = "ebf4435d83f8" +down_revision = "85bb3ebee137" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('inbox_mail', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.Column('send_from', sa.String(), nullable=True), - sa.Column('send_to', sa.JSON(), nullable=True), - sa.Column('subject', sa.String(), nullable=True), - sa.Column('content', sa.String(), nullable=True), - sa.Column('mark_as_important', sa.Boolean(), nullable=True), - sa.Column('meta_data', sa.JSON(), nullable=True), - sa.Column('is_seen', sa.Boolean(), nullable=True), - sa.Column('being_worked_on', sa.Boolean(), nullable=True), - sa.Column('parent_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('child_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.ForeignKeyConstraint(['child_id'], ['global.inbox_mail.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['organization_id'], ['organization.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['parent_id'], ['global.inbox_mail.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id'), - schema='global' + op.create_table( + "inbox_mail", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("organization_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("send_from", sa.String(), nullable=True), + sa.Column("send_to", sa.JSON(), nullable=True), + sa.Column("subject", sa.String(), nullable=True), + sa.Column("content", sa.String(), nullable=True), + sa.Column("mark_as_important", sa.Boolean(), nullable=True), + sa.Column("meta_data", sa.JSON(), nullable=True), + sa.Column("is_seen", sa.Boolean(), nullable=True), + sa.Column("being_worked_on", sa.Boolean(), nullable=True), + sa.Column("parent_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("child_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint( + ["child_id"], ["global.inbox_mail.id"], ondelete="SET NULL" + ), + sa.ForeignKeyConstraint( + ["organization_id"], ["organization.id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint( + ["parent_id"], ["global.inbox_mail.id"], ondelete="SET NULL" + ), + sa.PrimaryKeyConstraint("id"), + schema="global", + ) + op.create_index( + op.f("ix_global_inbox_mail_child_id"), + "inbox_mail", + ["child_id"], + unique=False, + schema="global", + ) + op.create_index( + op.f("ix_global_inbox_mail_organization_id"), + "inbox_mail", + ["organization_id"], + unique=False, + schema="global", + ) + op.create_index( + op.f("ix_global_inbox_mail_parent_id"), + "inbox_mail", + ["parent_id"], + unique=False, + schema="global", ) - op.create_index(op.f('ix_global_inbox_mail_child_id'), 'inbox_mail', ['child_id'], unique=False, schema='global') - op.create_index(op.f('ix_global_inbox_mail_organization_id'), 'inbox_mail', ['organization_id'], unique=False, schema='global') - op.create_index(op.f('ix_global_inbox_mail_parent_id'), 'inbox_mail', ['parent_id'], unique=False, schema='global') # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_global_inbox_mail_parent_id'), table_name='inbox_mail', schema='global') - op.drop_index(op.f('ix_global_inbox_mail_organization_id'), table_name='inbox_mail', schema='global') - op.drop_index(op.f('ix_global_inbox_mail_child_id'), table_name='inbox_mail', schema='global') - op.drop_table('inbox_mail', schema='global') + op.drop_index( + op.f("ix_global_inbox_mail_parent_id"), table_name="inbox_mail", schema="global" + ) + op.drop_index( + op.f("ix_global_inbox_mail_organization_id"), + table_name="inbox_mail", + schema="global", + ) + op.drop_index( + op.f("ix_global_inbox_mail_child_id"), table_name="inbox_mail", schema="global" + ) + op.drop_table("inbox_mail", schema="global") # ### end Alembic commands ### From f807fcf5d53a9a0ea742b985e3f53afc996d3a12 Mon Sep 17 00:00:00 2001 From: LennartSchmidtKern Date: Wed, 12 Nov 2025 17:52:34 +0100 Subject: [PATCH 4/5] tem alembic files --- .../b59188cf5526_change_inbox_model.py | 62 +++++++++++++++++++ submodules/model | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/b59188cf5526_change_inbox_model.py diff --git a/alembic/versions/b59188cf5526_change_inbox_model.py b/alembic/versions/b59188cf5526_change_inbox_model.py new file mode 100644 index 00000000..83cf240b --- /dev/null +++ b/alembic/versions/b59188cf5526_change_inbox_model.py @@ -0,0 +1,62 @@ +"""Change inbox model + +Revision ID: b59188cf5526 +Revises: ebf4435d83f8 +Create Date: 2025-11-12 10:01:36.389914 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b59188cf5526' +down_revision = 'ebf4435d83f8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail', sa.Column('sender_id', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('recipient_id', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('other_recipient_ids', sa.JSON(), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('thread_id', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('is_important', sa.Boolean(), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('being_working_on', sa.Boolean(), nullable=True), schema='global') + op.drop_index('ix_global_inbox_mail_child_id', table_name='inbox_mail', schema='global') + op.create_index(op.f('ix_global_inbox_mail_recipient_id'), 'inbox_mail', ['recipient_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_sender_id'), 'inbox_mail', ['sender_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_thread_id'), 'inbox_mail', ['thread_id'], unique=False, schema='global') + op.drop_constraint('inbox_mail_child_id_fkey', 'inbox_mail', schema='global', type_='foreignkey') + op.create_foreign_key(None, 'inbox_mail', 'user', ['sender_id'], ['id'], source_schema='global', ondelete='SET NULL') + op.create_foreign_key(None, 'inbox_mail', 'user', ['recipient_id'], ['id'], source_schema='global', ondelete='CASCADE') + op.drop_column('inbox_mail', 'mark_as_important', schema='global') + op.drop_column('inbox_mail', 'being_worked_on', schema='global') + op.drop_column('inbox_mail', 'child_id', schema='global') + op.drop_column('inbox_mail', 'send_from', schema='global') + op.drop_column('inbox_mail', 'send_to', schema='global') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail', sa.Column('send_to', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('send_from', sa.VARCHAR(), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('child_id', postgresql.UUID(), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('being_worked_on', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('mark_as_important', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='global') + op.drop_constraint(None, 'inbox_mail', schema='global', type_='foreignkey') + op.drop_constraint(None, 'inbox_mail', schema='global', type_='foreignkey') + op.create_foreign_key('inbox_mail_child_id_fkey', 'inbox_mail', 'inbox_mail', ['child_id'], ['id'], source_schema='global', referent_schema='global', ondelete='SET NULL') + op.drop_index(op.f('ix_global_inbox_mail_thread_id'), table_name='inbox_mail', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_sender_id'), table_name='inbox_mail', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_recipient_id'), table_name='inbox_mail', schema='global') + op.create_index('ix_global_inbox_mail_child_id', 'inbox_mail', ['child_id'], unique=False, schema='global') + op.drop_column('inbox_mail', 'being_working_on', schema='global') + op.drop_column('inbox_mail', 'is_important', schema='global') + op.drop_column('inbox_mail', 'thread_id', schema='global') + op.drop_column('inbox_mail', 'other_recipient_ids', schema='global') + op.drop_column('inbox_mail', 'recipient_id', schema='global') + op.drop_column('inbox_mail', 'sender_id', schema='global') + # ### end Alembic commands ### diff --git a/submodules/model b/submodules/model index 5afff308..66283591 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit 5afff308f051cb6e5a52f6632cc5d37b6c3a5d5d +Subproject commit 662835912546e75a73be751ab23c62e95935409d From 02436113aa66bb11a01395b8da0a96ac80db6452 Mon Sep 17 00:00:00 2001 From: LennartSchmidtKern Date: Mon, 17 Nov 2025 09:43:54 +0100 Subject: [PATCH 5/5] move kratos --- .../094dd28a1d8d_add_thread_association.py | 50 +++ ...f381e3ed_restructure_inbox_mail_threads.py | 240 ++++++++++++++ controller/auth/kratos.py | 295 ------------------ controller/auth/manager.py | 2 +- controller/comment/manager.py | 2 +- controller/misc/manager.py | 2 +- controller/monitor/manager.py | 2 +- controller/organization/manager.py | 2 +- controller/project/manager.py | 2 +- controller/user/manager.py | 2 +- fast_api/routes/heuristic.py | 2 +- fast_api/routes/labeling.py | 2 +- fast_api/routes/organization.py | 2 +- submodules/model | 2 +- tests/fast_api/routes/test_invite_users.py | 2 +- 15 files changed, 302 insertions(+), 307 deletions(-) create mode 100644 alembic/versions/094dd28a1d8d_add_thread_association.py create mode 100644 alembic/versions/d02bf381e3ed_restructure_inbox_mail_threads.py delete mode 100644 controller/auth/kratos.py diff --git a/alembic/versions/094dd28a1d8d_add_thread_association.py b/alembic/versions/094dd28a1d8d_add_thread_association.py new file mode 100644 index 00000000..437b3272 --- /dev/null +++ b/alembic/versions/094dd28a1d8d_add_thread_association.py @@ -0,0 +1,50 @@ +"""Add Thread association + +Revision ID: 094dd28a1d8d +Revises: d02bf381e3ed +Create Date: 2025-11-15 19:07:22.805825 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '094dd28a1d8d' +down_revision = 'd02bf381e3ed' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('inbox_mail_thread_association', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('thread_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint(['thread_id'], ['global.inbox_mail_thread.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + schema='global' + ) + op.create_index(op.f('ix_global_inbox_mail_thread_association_thread_id'), 'inbox_mail_thread_association', ['thread_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_thread_association_user_id'), 'inbox_mail_thread_association', ['user_id'], unique=False, schema='global') + op.add_column('inbox_mail_thread', sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.drop_index('idx_inbox_thread_participants_gin', table_name='inbox_mail_thread', schema='global') + op.create_index(op.f('ix_global_inbox_mail_thread_created_by'), 'inbox_mail_thread', ['created_by'], unique=False, schema='global') + op.create_foreign_key(None, 'inbox_mail_thread', 'user', ['created_by'], ['id'], source_schema='global', ondelete='CASCADE') + op.drop_column('inbox_mail_thread', 'participant_ids', schema='global') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail_thread', sa.Column('participant_ids', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True), schema='global') + op.drop_constraint(None, 'inbox_mail_thread', schema='global', type_='foreignkey') + op.drop_index(op.f('ix_global_inbox_mail_thread_created_by'), table_name='inbox_mail_thread', schema='global') + op.create_index('idx_inbox_thread_participants_gin', 'inbox_mail_thread', ['participant_ids'], unique=False, schema='global') + op.drop_column('inbox_mail_thread', 'created_by', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_thread_association_user_id'), table_name='inbox_mail_thread_association', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_thread_association_thread_id'), table_name='inbox_mail_thread_association', schema='global') + op.drop_table('inbox_mail_thread_association', schema='global') + # ### end Alembic commands ### diff --git a/alembic/versions/d02bf381e3ed_restructure_inbox_mail_threads.py b/alembic/versions/d02bf381e3ed_restructure_inbox_mail_threads.py new file mode 100644 index 00000000..b3b62c05 --- /dev/null +++ b/alembic/versions/d02bf381e3ed_restructure_inbox_mail_threads.py @@ -0,0 +1,240 @@ +"""Restructure inbox mail threads + +Revision ID: d02bf381e3ed +Revises: b59188cf5526 +Create Date: 2025-11-15 18:44:47.229926 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "d02bf381e3ed" +down_revision = "b59188cf5526" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "inbox_mail_thread", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("organization_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("subject", sa.String(), nullable=True), + sa.Column("participant_ids", sa.ARRAY(sa.String()), nullable=True), + sa.Column("meta_data", sa.JSON(), nullable=True), + sa.Column("is_important", sa.Boolean(), nullable=True), + sa.Column("being_working_on", sa.Boolean(), nullable=True), + sa.Column("is_admin_support_thread", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["organization_id"], ["organization.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("id"), + schema="global", + ) + op.create_index( + "idx_inbox_thread_participants_gin", + "inbox_mail_thread", + ["participant_ids"], + unique=False, + schema="global", + postgresql_using="gin", + postgresql_ops={"participant_ids": "array_ops"}, + ) + op.create_index( + op.f("ix_global_inbox_mail_thread_organization_id"), + "inbox_mail_thread", + ["organization_id"], + unique=False, + schema="global", + ) + op.drop_index( + "ix_global_inbox_mail_reference_inbox_mail_id", + table_name="inbox_mail_reference", + schema="global", + ) + op.drop_index( + "ix_global_inbox_mail_reference_user_id", + table_name="inbox_mail_reference", + schema="global", + ) + op.drop_table("inbox_mail_reference", schema="global") + op.drop_index( + "ix_global_inbox_mail_organization_id", table_name="inbox_mail", schema="global" + ) + op.drop_index( + "ix_global_inbox_mail_parent_id", table_name="inbox_mail", schema="global" + ) + op.drop_constraint( + "inbox_mail_parent_id_fkey", "inbox_mail", schema="global", type_="foreignkey" + ) + op.drop_constraint( + "inbox_mail_organization_id_fkey", + "inbox_mail", + schema="global", + type_="foreignkey", + ) + op.create_foreign_key( + None, + "inbox_mail", + "inbox_mail_thread", + ["thread_id"], + ["id"], + source_schema="global", + referent_schema="global", + ondelete="CASCADE", + ) + op.drop_column("inbox_mail", "being_working_on", schema="global") + op.drop_column("inbox_mail", "original_recipient_ids", schema="global") + op.drop_column("inbox_mail", "organization_id", schema="global") + op.drop_column("inbox_mail", "parent_id", schema="global") + op.drop_column("inbox_mail", "subject", schema="global") + op.drop_column("inbox_mail", "is_important", schema="global") + op.drop_column("inbox_mail", "meta_data", schema="global") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "inbox_mail", + sa.Column( + "meta_data", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("is_important", sa.BOOLEAN(), autoincrement=False, nullable=True), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("subject", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("parent_id", postgresql.UUID(), autoincrement=False, nullable=True), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column( + "organization_id", postgresql.UUID(), autoincrement=False, nullable=True + ), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column( + "original_recipient_ids", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("being_working_on", sa.BOOLEAN(), autoincrement=False, nullable=True), + schema="global", + ) + op.drop_constraint( + "inbox_mail_thread_id_fkey", + "inbox_mail", + schema="global", + type_="foreignkey", + ) + op.create_foreign_key( + "inbox_mail_organization_id_fkey", + "inbox_mail", + "organization", + ["organization_id"], + ["id"], + source_schema="global", + ondelete="CASCADE", + ) + op.create_foreign_key( + "inbox_mail_parent_id_fkey", + "inbox_mail", + "inbox_mail", + ["parent_id"], + ["id"], + source_schema="global", + referent_schema="global", + ondelete="SET NULL", + ) + op.create_index( + "ix_global_inbox_mail_parent_id", + "inbox_mail", + ["parent_id"], + unique=False, + schema="global", + ) + op.create_index( + "ix_global_inbox_mail_organization_id", + "inbox_mail", + ["organization_id"], + unique=False, + schema="global", + ) + op.create_table( + "inbox_mail_reference", + sa.Column("id", postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column( + "inbox_mail_id", postgresql.UUID(), autoincrement=False, nullable=False + ), + sa.Column("scope", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("user_id", postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column("is_seen", sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint( + ["inbox_mail_id"], + ["global.inbox_mail.id"], + name="inbox_mail_reference_inbox_mail_id_fkey", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + name="inbox_mail_reference_user_id_fkey", + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name="inbox_mail_reference_pkey"), + schema="global", + ) + op.create_index( + "ix_global_inbox_mail_reference_user_id", + "inbox_mail_reference", + ["user_id"], + unique=False, + schema="global", + ) + op.create_index( + "ix_global_inbox_mail_reference_inbox_mail_id", + "inbox_mail_reference", + ["inbox_mail_id"], + unique=False, + schema="global", + ) + op.drop_index( + op.f("ix_global_inbox_mail_thread_organization_id"), + table_name="inbox_mail_thread", + schema="global", + ) + op.drop_index( + "idx_inbox_thread_participants_gin", + table_name="inbox_mail_thread", + schema="global", + postgresql_using="gin", + postgresql_ops={"participant_ids": "array_ops"}, + ) + op.drop_table("inbox_mail_thread", schema="global") + # ### end Alembic commands ### diff --git a/controller/auth/kratos.py b/controller/auth/kratos.py deleted file mode 100644 index bd7b7b31..00000000 --- a/controller/auth/kratos.py +++ /dev/null @@ -1,295 +0,0 @@ -from email.mime.text import MIMEText -import smtplib -from typing import Union, Any, List, Dict -import os -import requests -import logging -from datetime import datetime, timedelta -from urllib.parse import quote - -from controller.user import manager - - -logging.basicConfig(level=logging.INFO) -logger: logging.Logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - -KRATOS_ADMIN_URL = os.getenv("KRATOS_ADMIN_URL") -SMTP_HOST = os.getenv("SMTP_HOST") -SMTP_PORT = os.getenv("SMTP_PORT") -SMTP_USER = os.getenv("SMTP_USER") -SMTP_PASSWORD = os.getenv("SMTP_PASSWORD") - -# user_id -> {"identity" -> full identity, "simple" -> {"id": str, "mail": str, "firstName": str, "lastName": str}} -# "collected" -> timestamp -KRATOS_IDENTITY_CACHE: Dict[str, Any] = {} -KRATOS_IDENTITY_CACHE_TIMEOUT = timedelta(minutes=30) - -LANGUAGE_MESSAGES = { - "en": "Hello!\n\nClick the link to complete your account setup:\n\n", - "de": "Hallo!\n\nKlicken Sie auf den Link, um Ihre Kontoeinrichtung abzuschließen:\n\n", -} - -INVITATION_SUBJECT = "Sie sind zu unserer app eingeladen/You are invited to our app" - -LANGUAGE_EXPIRATION_INFO = { - "en": "This link can only be clicked once and is valid for 2 days. Contact your system admin if you have issues.", - "de": "Dieser Link kann nur einmal angeklickt werden und ist 2 Tage lang gültig. Kontaktieren Sie Ihren Systemadministrator, wenn Sie Probleme haben.", -} - - -def get_cached_values(update_db_users: bool = True) -> Dict[str, Dict[str, Any]]: - global KRATOS_IDENTITY_CACHE - if not KRATOS_IDENTITY_CACHE or len(KRATOS_IDENTITY_CACHE) == 0: - __refresh_identity_cache(update_db_users) - elif ( - KRATOS_IDENTITY_CACHE["collected"] + KRATOS_IDENTITY_CACHE_TIMEOUT - < datetime.now() - ): - __refresh_identity_cache(update_db_users) - return KRATOS_IDENTITY_CACHE - - -def __refresh_identity_cache(update_db_users: bool = True) -> None: - global KRATOS_IDENTITY_CACHE - request = requests.get(f"{KRATOS_ADMIN_URL}/identities") - if request.ok: - collected = datetime.now() - identities = request.json() - - # maybe more pages https://www.ory.sh/docs/ecosystem/api-design#pagination - while next_link := __get_link_from_kratos_request(request): - request = requests.get(next_link) - if request.ok: - identities.extend(request.json()) - - KRATOS_IDENTITY_CACHE = { - identity["id"]: { - "identity": identity, - "simple": __parse_identity_to_simple(identity), - } - for identity in identities - } - - KRATOS_IDENTITY_CACHE["collected"] = collected - else: - KRATOS_IDENTITY_CACHE = {} - - if update_db_users: - manager.migrate_kratos_users() - - -def __get_link_from_kratos_request(request: requests.Response) -> str: - # rel=next only if there is more than 1 page - # ; rel="first",; rel="next" - link = request.headers.get("Link") - if link: - if 'rel="next"' in link: - parts = link.split("<") - for part in parts: - if 'rel="next"' in part: - return part.split(">")[0].replace("/admin", KRATOS_ADMIN_URL) - return None - - -def __get_identity(user_id: str, only_simple: bool = True) -> Dict[str, Any]: - if not isinstance(user_id, str): - user_id = str(user_id) - cache = get_cached_values() - if user_id in cache: - if only_simple: - return cache[user_id]["simple"] - return cache[user_id] - - if len(user_id) == 36: - # check not new entry outside cache - request = requests.get(f"{KRATOS_ADMIN_URL}/identities/{user_id}") - if request.ok: - identity = request.json() - if identity["id"] == user_id: - KRATOS_IDENTITY_CACHE[user_id] = { - "identity": identity, - "simple": __parse_identity_to_simple(identity), - } - if only_simple: - return KRATOS_IDENTITY_CACHE[user_id]["simple"] - return KRATOS_IDENTITY_CACHE[user_id] - # e.g. if id "GOLD_STAR" is requested => wont be in cache but expects a dummy dict - if only_simple: - return __parse_identity_to_simple({"id": user_id}) - return { - "identity": { - "id": user_id, - "traits": {"email": None, "name": {"first": None, "last": None}}, - } - } - - -def __parse_identity_to_simple(identity: Dict[str, Any]) -> Dict[str, str]: - r = { - "id": identity["id"], - "mail": None, - "firstName": None, - "lastName": None, - } - if "traits" in identity: - r["mail"] = identity["traits"]["email"] - if "name" in identity["traits"]: - r["firstName"] = identity["traits"]["name"]["first"] - r["lastName"] = identity["traits"]["name"]["last"] - return r - - -def get_userid_from_mail(user_mail: str) -> str: - values = get_cached_values() - for key in values: - if key == "collected": - continue - if values[key]["simple"]["mail"] == user_mail: - return key - # not in cached values, try search kratos - return __search_kratos_for_user_mail(user_mail)["id"] - - -def __search_kratos_for_user_mail(user_mail: str) -> str: - request = requests.get( - f"{KRATOS_ADMIN_URL}/identities?preview_credentials_identifier_similar={quote(user_mail)}" - ) - if request.ok: - identities = request.json() - for i in identities: - if i["traits"]["email"].lower() == user_mail.lower(): - return i - return None - - -def resolve_user_mail_by_id(user_id: str) -> str: - i = __get_identity(user_id) - if i: - return i["mail"] - return None - - -def resolve_user_name_by_id(user_id: str) -> Dict[str, str]: - i = __get_identity(user_id, False) - if i: - i = i["identity"] - return i["traits"]["name"] if "name" in i["traits"] else None - return None - - -def resolve_all_user_ids( - relevant_ids: List[str], as_list: bool = True -) -> Union[Dict[str, Dict[str, str]], List[Dict[str, str]]]: - final = [] if as_list else {} - for id in relevant_ids: - i = __get_identity(id) - if as_list: - final.append(i) - else: - final[id] = i - return final - - -def expand_user_mail_name( - users: List[Dict[str, str]], user_id_key="id" -) -> List[Dict[str, str]]: - final = [] - for user in users: - i = __get_identity(user[user_id_key]) - user = {**user, **i} - final.append(user) - return final - - -def resolve_user_name_and_email_by_id(user_id: str) -> dict: - i = __get_identity(user_id, False) - if i: - i = i["identity"] - if i and "traits" in i and i["traits"]: - return i["traits"]["name"], i["traits"]["email"] - return None - - -def create_user_kratos(email: str, provider: str = None): - payload_registration = { - "schema_id": "default", - "traits": {"email": email}, - } - if provider: - payload_registration["metadata_public"] = { - "registration_scope": { - "provider_id": provider, - "invitation_sso": True, - } - } - response_create = requests.post( - f"{KRATOS_ADMIN_URL}/identities", - json=payload_registration, - ) - return response_create.json() if response_create.ok else None - - -def delete_user_kratos(user_id: str) -> bool: - response_delete = requests.delete(f"{KRATOS_ADMIN_URL}/identities/{user_id}") - if response_delete.ok: - del KRATOS_IDENTITY_CACHE[user_id] - return True - return False - - -def get_recovery_link(user_id: str) -> str: - payload_recovery_link = { - "expires_in": "48h", - "identity_id": user_id, - } - response_link = requests.post( - f"{KRATOS_ADMIN_URL}/recovery/link", json=payload_recovery_link - ) - return response_link.json() if response_link.ok else None - - -def email_with_link(to_email: str, recovery_link: str) -> None: - msg = MIMEText( - f"{LANGUAGE_MESSAGES['de']}{recovery_link}\n\n{LANGUAGE_EXPIRATION_INFO['de']}\n\n\n------\n\n{LANGUAGE_MESSAGES['en']}{recovery_link}\n\n{LANGUAGE_EXPIRATION_INFO['en']}", - ) - msg["Subject"] = INVITATION_SUBJECT - msg["From"] = "signup@kern.ai" - msg["To"] = to_email - - with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: - if SMTP_USER and SMTP_PASSWORD: - server.ehlo() - server.starttls() - server.login(SMTP_USER, SMTP_PASSWORD) - server.send_message(msg) - - -def send_bulk_emails(emails: List[str], recovery_links: List[str]) -> None: - - with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: - if SMTP_USER and SMTP_PASSWORD: - server.ehlo() - server.starttls() - server.login(SMTP_USER, SMTP_PASSWORD) - - for to_email, recovery_link in zip(emails, recovery_links): - msg = MIMEText( - f"{LANGUAGE_MESSAGES['de']}{recovery_link}\n\n{LANGUAGE_EXPIRATION_INFO['de']}\n\n\n------\n\n{LANGUAGE_MESSAGES['en']}{recovery_link}\n\n{LANGUAGE_EXPIRATION_INFO['en']}", - ) - msg["Subject"] = INVITATION_SUBJECT - msg["From"] = "signup@kern.ai" - msg["To"] = to_email - server.send_message(msg) - - -def check_user_exists(email: str) -> bool: - request = requests.get( - f"{KRATOS_ADMIN_URL}/identities?preview_credentials_identifier_similar={quote(email)}" - ) - if request.ok: - identities = request.json() - for i in identities: - if i["traits"]["email"].lower() == email.lower(): - return True - return False diff --git a/controller/auth/manager.py b/controller/auth/manager.py index 2df60d71..7f082583 100644 --- a/controller/auth/manager.py +++ b/controller/auth/manager.py @@ -1,7 +1,7 @@ import re from typing import Any, Dict, List, Optional -from controller.auth import kratos +from submodules.model import kratos from fastapi import Request from exceptions.exceptions import ( AuthManagerError, diff --git a/controller/comment/manager.py b/controller/comment/manager.py index a5a505e0..3b5df18d 100644 --- a/controller/comment/manager.py +++ b/controller/comment/manager.py @@ -1,5 +1,5 @@ from typing import Any, Dict, List, Optional, Union -from controller.auth import kratos +from submodules.model import kratos from controller.auth import manager as user_manager from submodules.model import enums from submodules.model.models import CommentData, User diff --git a/controller/misc/manager.py b/controller/misc/manager.py index 87cdbd08..f649dd5f 100644 --- a/controller/misc/manager.py +++ b/controller/misc/manager.py @@ -6,7 +6,7 @@ ) from datetime import datetime import os -from controller.auth import kratos +from submodules.model import kratos from submodules.model.util import sql_alchemy_to_dict from submodules.model import enums import requests diff --git a/controller/monitor/manager.py b/controller/monitor/manager.py index f4bdd43b..ca196e3b 100644 --- a/controller/monitor/manager.py +++ b/controller/monitor/manager.py @@ -1,6 +1,6 @@ from typing import Any, List, Dict from submodules.model.business_objects import monitor as task_monitor -from controller.auth import kratos +from submodules.model import kratos from submodules.model.util import sql_alchemy_to_dict diff --git a/controller/organization/manager.py b/controller/organization/manager.py index 3a82124a..0bfa2ef7 100644 --- a/controller/organization/manager.py +++ b/controller/organization/manager.py @@ -4,7 +4,7 @@ from submodules.model.business_objects import organization, general, user from submodules.model.exceptions import EntityAlreadyExistsException from submodules.model.models import Organization, User -from controller.auth import kratos +from submodules.model import kratos from submodules.model.util import sql_alchemy_to_dict from submodules.s3 import controller as s3 from submodules.model.cognition_objects import integration diff --git a/controller/project/manager.py b/controller/project/manager.py index 8123e2d2..d958b67c 100644 --- a/controller/project/manager.py +++ b/controller/project/manager.py @@ -30,7 +30,7 @@ ) from submodules.s3 import controller as s3 from service.search import search -from controller.auth import kratos +from submodules.model import kratos from submodules.model.util import sql_alchemy_to_dict from controller.embedding import connector diff --git a/controller/user/manager.py b/controller/user/manager.py index 81df8e35..17701950 100644 --- a/controller/user/manager.py +++ b/controller/user/manager.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional, Any from submodules.model import User, daemon, enums from submodules.model.business_objects import user, general -from controller.auth import kratos +from submodules.model import kratos from submodules.model.exceptions import EntityNotFoundException from controller.organization import manager as organization_manager from datetime import datetime, timedelta diff --git a/fast_api/routes/heuristic.py b/fast_api/routes/heuristic.py index dceb3919..b8dc43b0 100644 --- a/fast_api/routes/heuristic.py +++ b/fast_api/routes/heuristic.py @@ -11,7 +11,7 @@ from util import notification from controller.task_master import manager as task_master_manager from submodules.model import enums -from controller.auth import kratos +from submodules.model import kratos router = APIRouter() diff --git a/fast_api/routes/labeling.py b/fast_api/routes/labeling.py index d38ea84b..c985a22d 100644 --- a/fast_api/routes/labeling.py +++ b/fast_api/routes/labeling.py @@ -32,7 +32,7 @@ ) from submodules.model.util import sql_alchemy_to_dict, to_frontend_obj_raw from util import notification -from controller.auth import kratos +from submodules.model import kratos router = APIRouter() diff --git a/fast_api/routes/organization.py b/fast_api/routes/organization.py index bbd5fe22..4ae12135 100644 --- a/fast_api/routes/organization.py +++ b/fast_api/routes/organization.py @@ -15,7 +15,7 @@ RemoveUserToOrganizationBody, ) from controller.auth import manager as auth_manager -from controller.auth.kratos import ( +from submodules.model.kratos import ( resolve_user_mail_by_id, resolve_user_name_by_id, ) diff --git a/submodules/model b/submodules/model index 66283591..a817046f 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit 662835912546e75a73be751ab23c62e95935409d +Subproject commit a817046f94317ad2d32931782f63e5b6c7df35ab diff --git a/tests/fast_api/routes/test_invite_users.py b/tests/fast_api/routes/test_invite_users.py index 9d6eaf8a..7a96cab6 100644 --- a/tests/fast_api/routes/test_invite_users.py +++ b/tests/fast_api/routes/test_invite_users.py @@ -1,5 +1,5 @@ from fastapi.testclient import TestClient -from controller.auth.kratos import delete_user_kratos +from submodules.model.kratos import delete_user_kratos from submodules.model.business_objects import organization as organization_bo from submodules.model.enums import UserRoles