Skip to content

Commit

Permalink
Merge pull request #54 from phasehq/multi-user
Browse files Browse the repository at this point in the history
feat: multi user support
  • Loading branch information
rohan-chaturvedi authored Sep 30, 2023
2 parents e82d182 + af60b92 commit f8fae33
Show file tree
Hide file tree
Showing 94 changed files with 5,452 additions and 1,044 deletions.
2 changes: 1 addition & 1 deletion .env.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ HTTP_PROTOCOL=https://
NEXTAUTH_URL=https://localhost
OAUTH_REDIRECT_URI=https://localhost
BACKEND_API_BASE=http://backend:8000
NEXT_PUBLIC_BACKEND_API_BASE=https://localhost/ph-backend
NEXT_PUBLIC_BACKEND_API_BASE=https://localhost/service
NEXT_PUBLIC_NEXTAUTH_PROVIDERS=google,github,gitlab

# WARNING: Replace this with a cryptographically strong random value. You can use `openssl rand -hex 32` to generate this.
Expand Down
48 changes: 48 additions & 0 deletions backend/api/emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.conf import settings
from django.core.mail import send_mail
from django.template.loader import render_to_string
from datetime import datetime

from api.utils import get_client_ip


def send_email(subject, recipient_list, template_name, context):
"""
Send email via SMTP gateway through Django's email backend.
"""
# Load the template
email_html_message = render_to_string(template_name, context)

# Get the DEFAULT_FROM_EMAIL from settings
default_from_email = getattr(settings, "DEFAULT_FROM_EMAIL")

# Send the email
send_mail(
subject,
'', # plain text content can be empty as we're sending HTML
default_from_email,
recipient_list,
html_message=email_html_message
)


def send_login_email(request, email):
user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
ip_address = get_client_ip(request)
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

# Creating context dictionary
context = {
'auth': 'GitHub',
'email': email,
'ip': ip_address,
'user_agent': user_agent,
'timestamp': timestamp
}

send_email(
'New Login Alert - Phase Console',
[email],
'backend/api/email_templates/login.html',
context
)
28 changes: 28 additions & 0 deletions backend/api/migrations/0031_organisationmemberinvite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.3 on 2023-09-12 08:18

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


class Migration(migrations.Migration):

dependencies = [
('api', '0030_usertoken_expires_at'),
]

operations = [
migrations.CreateModel(
name='OrganisationMemberInvite',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('invitee_email', models.EmailField(max_length=254)),
('valid', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('expires_at', models.DateTimeField()),
('invited_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='api.organisation')),
],
),
]
18 changes: 18 additions & 0 deletions backend/api/migrations/0032_organisationmemberinvite_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.3 on 2023-09-12 13:04

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0031_organisationmemberinvite'),
]

operations = [
migrations.AddField(
model_name='organisationmemberinvite',
name='apps',
field=models.ManyToManyField(to='api.app'),
),
]
18 changes: 18 additions & 0 deletions backend/api/migrations/0033_organisationmemberinvite_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.3 on 2023-09-14 08:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0032_organisationmemberinvite_apps'),
]

operations = [
migrations.AddField(
model_name='organisationmemberinvite',
name='role',
field=models.CharField(choices=[('owner', 'Owner'), ('admin', 'Admin'), ('dev', 'Developer')], default='dev', max_length=5),
),
]
29 changes: 29 additions & 0 deletions backend/api/migrations/0034_organisationmember_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.3 on 2023-09-15 13:19

from django.db import migrations, models


def set_org_member_apps(apps, schema_editor):
OrgMemberModel = apps.get_model('api', 'OrganisationMember')
AppModel = apps.get_model('api', 'App')

for org_member in OrgMemberModel.objects.all():
org_apps = AppModel.objects.filter(
organisation=org_member.organisation, is_deleted=False)
org_member.apps.set(org_apps)


class Migration(migrations.Migration):

dependencies = [
('api', '0033_organisationmemberinvite_role'),
]

operations = [
migrations.AddField(
model_name='organisationmember',
name='apps',
field=models.ManyToManyField(to='api.app'),
),
migrations.RunPython(set_org_member_apps)
]
27 changes: 27 additions & 0 deletions backend/api/migrations/0035_alter_organisationmember_deleted_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.3 on 2023-09-16 08:50

from django.db import migrations, models


def reset_deleted_field(apps, schema_editor):
OrgMemberModel = apps.get_model('api', 'OrganisationMember')

for org_member in OrgMemberModel.objects.all():
org_member.deleted_at = None
org_member.save()


class Migration(migrations.Migration):

dependencies = [
('api', '0034_organisationmember_apps'),
]

operations = [
migrations.AlterField(
model_name='organisationmember',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.RunPython(reset_deleted_field)
]
18 changes: 18 additions & 0 deletions backend/api/migrations/0036_alter_organisationmember_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.3 on 2023-09-20 06:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0035_alter_organisationmember_deleted_at'),
]

operations = [
migrations.AlterField(
model_name='organisationmember',
name='apps',
field=models.ManyToManyField(related_name='members', to='api.app'),
),
]
18 changes: 18 additions & 0 deletions backend/api/migrations/0037_organisationmember_wrapped_recovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.3 on 2023-09-27 08:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0036_alter_organisationmember_apps'),
]

operations = [
migrations.AddField(
model_name='organisationmember',
name='wrapped_recovery',
field=models.TextField(blank=True),
),
]
90 changes: 61 additions & 29 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from uuid import uuid4
from backend.api.kv import write
import json

from django.utils import timezone
from django.conf import settings

CLOUD_HOSTED = settings.APP_HOST == 'cloud'
Expand Down Expand Up @@ -91,34 +91,6 @@ def __str__(self):
return self.name


class OrganisationMember(models.Model):
OWNER = 'owner'
ADMIN = 'admin'
DEVELOPER = 'dev'

USER_ROLES = [
(OWNER, 'Owner'),
(ADMIN, 'Admin'),
(DEVELOPER, 'Developer')
]

id = models.TextField(default=uuid4, primary_key=True, editable=False)
user = models.ForeignKey(
CustomUser, related_name='organisation', on_delete=models.CASCADE)
organisation = models.ForeignKey(
Organisation, related_name='users', on_delete=models.CASCADE)
role = models.CharField(
max_length=5,
choices=USER_ROLES,
default=DEVELOPER,
)
identity_key = models.CharField(max_length=256, null=True, blank=True)
wrapped_keyring = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(auto_now=True)


class App(models.Model):
id = models.TextField(default=uuid4, primary_key=True, editable=False)
organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE)
Expand Down Expand Up @@ -152,6 +124,62 @@ def __str__(self):
return self.name


class OrganisationMember(models.Model):
OWNER = 'owner'
ADMIN = 'admin'
DEVELOPER = 'dev'

USER_ROLES = [
(OWNER, 'Owner'),
(ADMIN, 'Admin'),
(DEVELOPER, 'Developer')
]

id = models.TextField(default=uuid4, primary_key=True, editable=False)
user = models.ForeignKey(
CustomUser, related_name='organisation', on_delete=models.CASCADE)
organisation = models.ForeignKey(
Organisation, related_name='users', on_delete=models.CASCADE)
role = models.CharField(
max_length=5,
choices=USER_ROLES,
default=DEVELOPER,
)
apps = models.ManyToManyField(App, related_name='members')
identity_key = models.CharField(max_length=256, null=True, blank=True)
wrapped_keyring = models.TextField(blank=True)
wrapped_recovery = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(null=True, blank=True)

def delete(self, *args, **kwargs):
"""
Soft delete the object by setting the 'deleted_at' field.
"""
self.deleted_at = timezone.now()
self.save()


class OrganisationMemberInvite(models.Model):
id = models.TextField(default=uuid4, primary_key=True, editable=False)
organisation = models.ForeignKey(
Organisation, related_name='invites', on_delete=models.CASCADE)
apps = models.ManyToManyField(App)
role = models.CharField(
max_length=5,
choices=OrganisationMember.USER_ROLES,
default=OrganisationMember.DEVELOPER,
)
invited_by = models.ForeignKey(
OrganisationMember, on_delete=models.CASCADE)
invitee_email = models.EmailField()
valid = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
expires_at = models.DateTimeField()


class Environment(models.Model):

DEVELOPMENT = "dev"
Expand Down Expand Up @@ -193,6 +221,10 @@ class EnvironmentKey(models.Model):
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(blank=True, null=True)

def delete(self, *args, **kwargs):
self.deleted_at = timezone.now()
self.save()


class EnvironmentToken(models.Model):
id = models.TextField(default=uuid4, primary_key=True, editable=False)
Expand Down
Loading

0 comments on commit f8fae33

Please sign in to comment.