Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Multi-tenancy / Add support for multiple tenants #561

Merged
merged 40 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c9030a3
feat: multi tenancy basic configuration (#480)
wojcikmat Feb 22, 2024
94f91da
feat: multi tenancy basic tenants management (#483)
wojcikmat Mar 4, 2024
a032f68
feat: multi tenancy memberships management (#496)
wojcikmat Mar 8, 2024
c4d4653
feat: Add admin panel for multitenancy (#499)
wojcikmat Mar 8, 2024
bcb633f
feat: Add tenant invitation notification (#497)
wojcikmat Mar 11, 2024
433bb89
Make all tenants query public (#503)
wojcikmat Mar 14, 2024
0c2c41e
feat: multi tenancy refactor membership type (#506)
wojcikmat Mar 25, 2024
2a6d85b
feat: multi tenancy subscriptions (#509)
wojcikmat Mar 27, 2024
9433ca2
fix: multi tenancy enum type (#511)
wojcikmat Mar 27, 2024
2aebe4a
feat: Add initial multi-tenancy logic to the webapp (#504)
mkleszcz Apr 4, 2024
746360e
feat: Create tenant form (#505)
mkleszcz Apr 4, 2024
1dcb2d2
feat: Make tenant dependent example CRUD demo item model (#512)
wojcikmat Apr 8, 2024
bf26a59
feat: Organization settings page and invitation to the tenant (#513)
mkleszcz Apr 9, 2024
88ef08c
feat: allow the user to change membership role (#515)
mskwierczynski Apr 9, 2024
6ed8325
feat: User membership delete feature (#517)
mskwierczynski Apr 11, 2024
96bc8a3
feat: #463 Tenant update feature (#518)
mskwierczynski Apr 11, 2024
cb687bb
feat: multi tenancy invitation notifications (#516)
wojcikmat Apr 12, 2024
da0e893
fix: Fix invalid URL in tenant invitation email (#521)
mkleszcz Apr 15, 2024
6f1f589
fix: Fix issue with showing invalid active tenant in selector (#520)
mkleszcz Apr 15, 2024
cc77d19
feat: Add TenantAuthRoute component and tenant role checks (#522)
mkleszcz Apr 15, 2024
76d4641
Fix tenant related queries and tests for finances app (#524)
wojcikmat Apr 15, 2024
f55519c
Fix update and delete for not accepted tenant memberships (#523)
wojcikmat Apr 15, 2024
091183a
Change schema to avoid duplicated types in FE schema (#525)
wojcikmat Apr 17, 2024
676b4c3
feat: multi tenancy subscription plan adjustments (#526)
mskwierczynski Apr 18, 2024
9943c91
feat: Multi-tenancy CRUD adjustments (#535)
mskwierczynski Apr 26, 2024
cb0040f
feat: Adjust subscriptions routing (#539)
mkleszcz May 9, 2024
8faf88e
feat: multi tenancy - add tenant removal form (#542)
sdrejkarz May 9, 2024
0018fe5
feat: multi tenancy delete tenant validation (#543)
wojcikmat May 10, 2024
0ab5c17
Merge branch 'master' into feat/multi-tenancy
mkleszcz May 13, 2024
365d438
chore: Update webapp-tenants package.json to support pnpm 9+
mkleszcz May 13, 2024
c0888d5
docs: Multi tenancy feature description (#547)
wojcikmat May 13, 2024
bfa26b1
fix: Fix type-check errors
mkleszcz May 13, 2024
b43f3f0
feat: Add API reference docs for webapp-tenants (#548)
mkleszcz May 13, 2024
b4bc9a5
feat: add multi tenancy notifications (#550)
sdrejkarz May 16, 2024
361a728
fix: test coverage of webapp-tenants (#556)
sdrejkarz May 17, 2024
48f019b
feat: add additional info in the personal settings' members tab (#558)
sdrejkarz May 17, 2024
6127c1c
Merge branch 'refs/heads/master' into feat/multi-tenancy
mkleszcz May 17, 2024
9065ca3
Merge branch 'master' into feat/multi-tenancy
mkleszcz May 20, 2024
c2ca5b3
fix: Coherent organization naming (#562)
mkleszcz May 20, 2024
302e04a
fix: copies
sdrejkarz May 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"@sb/webapp-documents/**",
"@sb/webapp-finances/**",
"@sb/webapp-generative-ai/**",
"@sb/webapp-notifications/**"
"@sb/webapp-notifications/**",
"@sb/webapp-tenants/**"
],
"depConstraints": [
{
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/webapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ jobs:
- webapp-emails
- webapp-finances
- webapp-generative-ai
- webapp-tenants
steps:
- uses: actions/checkout@v3
with:
Expand Down Expand Up @@ -130,5 +131,6 @@ jobs:
SONAR_WEBAPP_FINANCES_PROJECT_KEY: ${{ vars.SONAR_WEBAPP_FINANCES_PROJECT_KEY }}
SONAR_WEBAPP_GENERATIVE_AI_PROJECT_KEY: ${{ vars.SONAR_WEBAPP_GENERATIVE_AI_PROJECT_KEY }}
SONAR_WEBAPP_NOTIFICATIONS_PROJECT_KEY: ${{ vars.SONAR_WEBAPP_NOTIFICATIONS_PROJECT_KEY }}
SONAR_WEBAPP_TENANTS_PROJECT_KEY: ${{ vars.SONAR_WEBAPP_TENANTS_PROJECT_KEY }}
with:
projectBaseDir: "packages/webapp-libs/${{ matrix.webapp-lib-name }}"
4 changes: 4 additions & 0 deletions .versionrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ module.exports = {
filename: './packages/webapp-libs/webapp-notifications/package.json',
type: 'json',
},
{
filename: './packages/webapp-libs/webapp-tenants/package.json',
type: 'json',
},

{
filename: './packages/workers/package.json',
Expand Down
27 changes: 27 additions & 0 deletions bitbucket-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ definitions:
pnpmwebappemails: $BITBUCKET_CLONE_DIR/.pnpm-store
pnpmwebappfinances: $BITBUCKET_CLONE_DIR/.pnpm-store
pnpmwebappgenerativeai: $BITBUCKET_CLONE_DIR/.pnpm-store
pnpmwebapptenants: $BITBUCKET_CLONE_DIR/.pnpm-store
pnpmworkers: $BITBUCKET_CLONE_DIR/.pnpm-store
pnpminfracore: $BITBUCKET_CLONE_DIR/.pnpm-store
pnpminfrashared: $BITBUCKET_CLONE_DIR/.pnpm-store
Expand Down Expand Up @@ -306,6 +307,30 @@ definitions:
- pnpmwebappgenerativeai
- clis
- sonar
- step: &webappTenantsTest
name: 'webapp-tenants: Lint & test'
image: atlassian/default-image:4
size: 2x
script:
- *initializeStep
- pnpm install
--include-workspace-root
--frozen-lockfile
--filter=webapp-tenants...
- pnpm nx run webapp-tenants:lint
- pnpm nx run webapp-tenants:type-check
- pnpm nx run webapp-tenants:test --watchAll=false --maxWorkers=20% --coverage
- if [ -z "${SONAR_ORGANIZATION}" ]; then exit 0; fi
- pipe: sonarsource/sonarcloud-scan:1.4.0
variables:
SONAR_ORGANIZATION: ${SONAR_ORGANIZATION}
SONAR_WEBAPP_TENANTS_PROJECT_KEY: ${SONAR_WEBAPP_TENANTS_PROJECT_KEY}
SONAR_TOKEN: ${SONAR_TOKEN}
EXTRA_ARGS: '-Dsonar.projectBaseDir=packages/webapp-libs/webapp-tenants'
caches:
- pnpmwebapptenants
- clis
- sonar
- step: &backendBuildAndTest
name: 'backend: Build image & run tests'
image: atlassian/default-image:4
Expand Down Expand Up @@ -501,6 +526,7 @@ pipelines:
- step: *webappEmailsTest
- step: *webappFinancesTest
- step: *webappGenAiTest
- step: *webappTenantsTest

- step: *backendBuildAndTest

Expand Down Expand Up @@ -531,6 +557,7 @@ pipelines:
- step: *webappEmailsTest
- step: *webappFinancesTest
- step: *webappGenAiTest
- step: *webappTenantsTest

- step: *backendBuildAndTest

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"postinstall": "node packages/internal/cli/scripts/build.js"
},
"devDependencies": {
"@apollo/client": "^3.8.8",
"@apollo/client": "^3.9.6",
Copy link
Contributor

@sdrejkarz sdrejkarz May 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this update on purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... Interesting. I think it should go with the newer version

"@apollo/rover": "^0.19.1",
"@babel/preset-react": "^7.24.1",
"@graphql-codegen/cli": "^5.0.0",
Expand Down
40 changes: 40 additions & 0 deletions packages/backend/apps/demo/migrations/0003_cruddemoitem_tenant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 4.2 on 2024-03-26 12:10

from django.db import migrations, models
import django.db.models.deletion

from apps.multitenancy.constants import TenantType


def populate_tenant(apps, schema_editor):
CrudDemoItem = apps.get_model('demo', 'CrudDemoItem')
Tenant = apps.get_model('multitenancy', 'Tenant')

for item in CrudDemoItem.objects.all():
if not item.tenant:
default_tenant = Tenant.objects.filter(type=TenantType.DEFAULT, creator=item.created_by).first()
item.tenant = default_tenant
item.save()


class Migration(migrations.Migration):
dependencies = [
('multitenancy', '0005_tenant_billing_email'),
('demo', '0002_initial')
]

operations = [
migrations.AddField(
model_name='cruddemoitem',
name='tenant',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='%(class)s_set',
to='multitenancy.tenant',
verbose_name='Tenant',
),
),
migrations.RunPython(populate_tenant, reverse_code=migrations.RunPython.noop),
]
8 changes: 3 additions & 5 deletions packages/backend/apps/demo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

from apps.content import models as content_models
from common.storages import UniqueFilePathGenerator
from common.models import TimestampedMixin, TenantDependentModelMixin

User = get_user_model()


class CrudDemoItem(models.Model):
class CrudDemoItem(TenantDependentModelMixin, models.Model):
id = hashid_field.HashidAutoField(primary_key=True)
name = models.CharField(max_length=255)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
Expand All @@ -24,14 +25,11 @@ def __init__(self, *args, **kwargs):
self.edited_by: Optional[User] = None


class ContentfulDemoItemFavorite(models.Model):
class ContentfulDemoItemFavorite(TimestampedMixin, models.Model):
id = hashid_field.HashidAutoField(primary_key=True)
item = models.ForeignKey(content_models.DemoItem, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
unique_together = [['item', 'user']]

Expand Down
11 changes: 5 additions & 6 deletions packages/backend/apps/demo/notifications.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.contrib.auth import get_user_model
from graphql_relay import to_global_id

from apps.notifications import sender
Expand All @@ -7,10 +6,10 @@


def send_new_entry_created_notification(entry: models.CrudDemoItem):
User = get_user_model()
for admin in User.objects.filter_admins():
tenant = entry.tenant
for owner in tenant.owners:
sender.send_notification(
user=admin,
user=owner,
type=constants.Notification.CRUD_ITEM_CREATED.value,
data={
"id": to_global_id('CrudDemoItemType', str(entry.id)),
Expand All @@ -23,8 +22,8 @@ def send_new_entry_created_notification(entry: models.CrudDemoItem):
def send_entry_updated_notification(entry: models.CrudDemoItem):
if not entry.edited_by:
return
User = get_user_model()
users_to_be_notified = set(User.objects.filter_admins()) | {entry.created_by}
tenant = entry.tenant
users_to_be_notified = set(tenant.owners) | {entry.created_by}
for user in users_to_be_notified:
if user and user != entry.edited_by:
sender.send_notification(
Expand Down
33 changes: 24 additions & 9 deletions packages/backend/apps/demo/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from django.shortcuts import get_object_or_404
from graphene import relay
from graphene_django import DjangoObjectType
from graphql_relay import to_global_id
from graphql_relay import to_global_id, from_global_id

from apps.content import models as content_models
from common.graphql import mutations
from common.acl.policies import IsTenantMemberAccess
from common.graphql.acl import permission_classes
from . import models, serializers


Expand Down Expand Up @@ -42,7 +44,7 @@ class Meta:
node = ContentfulDemoItemFavoriteType


class CreateCrudDemoItemMutation(mutations.CreateModelMutation):
class CreateCrudDemoItemMutation(mutations.CreateTenantDependentModelMutation):
class Meta:
serializer_class = serializers.CrudDemoItemSerializer
edge_class = CrudDemoItemConnection.Edge
Expand Down Expand Up @@ -103,26 +105,28 @@ def mutate_and_get_payload(cls, root, info, id):
return cls(deleted_ids=[id])


class UpdateCrudDemoItemMutation(mutations.UpdateModelMutation):
class UpdateCrudDemoItemMutation(mutations.UpdateTenantDependentModelMutation):
class Meta:
serializer_class = serializers.CrudDemoItemSerializer
edge_class = CrudDemoItemConnection.Edge


class DeleteCrudDemoItemMutation(mutations.DeleteModelMutation):
class DeleteCrudDemoItemMutation(mutations.DeleteTenantDependentModelMutation):
class Meta:
model = models.CrudDemoItem


class Query(graphene.ObjectType):
crud_demo_item = graphene.relay.Node.Field(CrudDemoItemType)
all_crud_demo_items = graphene.relay.ConnectionField(CrudDemoItemConnection)
crud_demo_item = graphene.Field(CrudDemoItemType, id=graphene.ID(), tenant_id=graphene.ID())
all_crud_demo_items = graphene.relay.ConnectionField(CrudDemoItemConnection, tenant_id=graphene.ID())
all_contentful_demo_item_favorites = graphene.relay.ConnectionField(ContentfulDemoItemFavoriteConnection)
all_document_demo_items = graphene.relay.ConnectionField(DocumentDemoItemConnection)

@staticmethod
def resolve_all_crud_demo_items(root, info, **kwargs):
return models.CrudDemoItem.objects.all()
@permission_classes(IsTenantMemberAccess)
def resolve_all_crud_demo_items(root, info, tenant_id, **kwargs):
_, pk = from_global_id(tenant_id)
return models.CrudDemoItem.objects.filter(tenant_id=pk).all()

@staticmethod
def resolve_all_contentful_demo_item_favorites(root, info, **kwargs):
Expand All @@ -132,11 +136,22 @@ def resolve_all_contentful_demo_item_favorites(root, info, **kwargs):
def resolve_all_document_demo_items(root, info, **kwargs):
return info.context.user.documents.all()

@staticmethod
@permission_classes(IsTenantMemberAccess)
def resolve_crud_demo_item(root, info, id, tenant_id, **kwargs):
_, pk = from_global_id(id)
_, tenant_pk = from_global_id(tenant_id)
return models.CrudDemoItem.objects.filter(pk=pk, tenant=tenant_pk).first()

class Mutation(graphene.ObjectType):

@permission_classes(IsTenantMemberAccess)
class TenantMemberMutation(graphene.ObjectType):
create_crud_demo_item = CreateCrudDemoItemMutation.Field()
update_crud_demo_item = UpdateCrudDemoItemMutation.Field()
delete_crud_demo_item = DeleteCrudDemoItemMutation.Field()


class Mutation(graphene.ObjectType):
create_document_demo_item = CreateDocumentDemoItemMutation.Field()
delete_document_demo_item = DeleteDocumentDemoItemMutation.Field()
create_favorite_contentful_demo_item = CreateFavoriteContentfulDemoItemMutation.Field()
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/apps/demo/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

class CrudDemoItemSerializer(serializers.ModelSerializer):
id = hidrest.HashidSerializerCharField(source_field="users.User.id", read_only=True)
tenant_id = hidrest.HashidSerializerCharField()
created_by = serializers.HiddenField(default=serializers.CurrentUserDefault())

def update(self, instance, validated_data):
Expand All @@ -17,7 +18,7 @@ def update(self, instance, validated_data):

class Meta:
model = models.CrudDemoItem
fields = ('id', 'name', 'created_by')
fields = ('id', 'tenant_id', 'name', 'created_by')


class DocumentDemoItemSerializer(serializers.ModelSerializer):
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/apps/demo/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

from apps.users.tests import factories as user_factories
from apps.content.tests import factories as content_factories
from apps.multitenancy.tests import factories as multitenancy_factories


class CrudDemoItemFactory(factory.django.DjangoModelFactory):
name = factory.Faker('pystr')
tenant = factory.SubFactory(multitenancy_factories.TenantFactory)

class Meta:
model = models.CrudDemoItem
Expand Down
Loading
Loading