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 user support #54

Merged
merged 139 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
139 commits
Select commit Hold shift + click to select a range
7c88797
feat: add invite model, schema, types and operations
rohan-chaturvedi Sep 12, 2023
7cc25e1
chore: regenerate schema and types
rohan-chaturvedi Sep 12, 2023
9d9ee77
feat: add invites query and mutation
rohan-chaturvedi Sep 12, 2023
61b6983
feat: add organisation context
rohan-chaturvedi Sep 12, 2023
69a4fa1
feat: increase free app limit to 5
rohan-chaturvedi Sep 13, 2023
7bc5426
feat: add role to invite model
rohan-chaturvedi Sep 14, 2023
e466a8c
feat: add invite validation query
rohan-chaturvedi Sep 14, 2023
d74c9a4
chore: regenerate schema and types
rohan-chaturvedi Sep 14, 2023
2537295
feat: members page
rohan-chaturvedi Sep 14, 2023
7ce8616
feat: invite page
rohan-chaturvedi Sep 14, 2023
45c06a6
fix: misc tweaks to organisation context
rohan-chaturvedi Sep 14, 2023
b8645be
fix: redirect to callback url on signin
rohan-chaturvedi Sep 14, 2023
ef9eb71
fix: button titles
rohan-chaturvedi Sep 14, 2023
3b6fd5d
feat: add apps, custom delete method to org member model
rohan-chaturvedi Sep 16, 2023
b4b67d2
feat: add create and delete org member mutations
rohan-chaturvedi Sep 16, 2023
52de662
chore: regenerate graphql schema
rohan-chaturvedi Sep 16, 2023
c84f2b2
feat: create and delete org member mutations
rohan-chaturvedi Sep 16, 2023
c77ba95
fix: input ring style
rohan-chaturvedi Sep 16, 2023
4e5c7b2
fix: account for repeated words in list index
rohan-chaturvedi Sep 16, 2023
9de9629
fix: remove redundant border
rohan-chaturvedi Sep 16, 2023
e79226b
fix: restyle password strength bar
rohan-chaturvedi Sep 16, 2023
ac4361d
feat: restyle dark mode toggle
rohan-chaturvedi Sep 16, 2023
b3cd68c
feat: new user onboarding flow
rohan-chaturvedi Sep 16, 2023
351e348
feat: add delete handler and ui to members page
rohan-chaturvedi Sep 16, 2023
5f6e1ab
fix: org member queries to account for soft deletes
rohan-chaturvedi Sep 16, 2023
c147bf1
fix: org member queries
rohan-chaturvedi Sep 16, 2023
04a2ab2
feat: improve mode toggle styling
rohan-chaturvedi Sep 18, 2023
e484f78
fix: misc styling fixes to account password input
rohan-chaturvedi Sep 18, 2023
839da58
fix: restyle sidebar
rohan-chaturvedi Sep 18, 2023
37880be
fix: add icons to mode toggle in onboarding navbar
rohan-chaturvedi Sep 18, 2023
b901573
feat: allow copying invite link from history, light theme fixes
rohan-chaturvedi Sep 18, 2023
babbfcf
feat: add success pane
rohan-chaturvedi Sep 18, 2023
41a09e1
fix: only return apps that a user has access too
rohan-chaturvedi Sep 18, 2023
a617450
fix: icon button color
rohan-chaturvedi Sep 18, 2023
5a2fe7a
fix: update role mutation
rohan-chaturvedi Sep 19, 2023
8a3b274
feat: update org member resolvers, add full name and avatar url
rohan-chaturvedi Sep 19, 2023
d00ffe0
chore: regenerate types
rohan-chaturvedi Sep 19, 2023
4171aa0
feat: update role mutation
rohan-chaturvedi Sep 19, 2023
340a63d
feat: query user full name and avatar
rohan-chaturvedi Sep 19, 2023
febff3f
feat: add referrer to head to fix google avatar 403s
rohan-chaturvedi Sep 19, 2023
a4e77ac
feat: allow updating user roles inline, restyle user row
rohan-chaturvedi Sep 19, 2023
b6520d8
feat: avatar component
rohan-chaturvedi Sep 19, 2023
2e4fb0e
feat: show user fullname and avatar in history
rohan-chaturvedi Sep 19, 2023
d4c04c6
feat: user app management
rohan-chaturvedi Sep 21, 2023
270ebc7
chore: remove commented code
rohan-chaturvedi Sep 21, 2023
375a7fd
feat: simplified nginx config
nimish-ks Sep 21, 2023
19d8249
Merge branch 'multi-user' into nginx-config
nimish-ks Sep 21, 2023
b6c1805
feat: only allow admins to update member access
rohan-chaturvedi Sep 21, 2023
dbb4cc2
fix: only allow admins to delete users
rohan-chaturvedi Sep 21, 2023
ffd769f
Merge branch 'environments' into multi-user
rohan-chaturvedi Sep 21, 2023
db27de0
fix: set identity key for new env keys
rohan-chaturvedi Sep 22, 2023
a6911b1
fix: hide app member form when all org memebrs are added
rohan-chaturvedi Sep 22, 2023
72ecf99
fix: only show service tokens to admins
rohan-chaturvedi Sep 22, 2023
8026905
fix: create keys for all admins when creating envs
rohan-chaturvedi Sep 22, 2023
6703497
fix: disable action buttons for org owner
rohan-chaturvedi Sep 22, 2023
238db0f
refactor: move invites to members screen
rohan-chaturvedi Sep 22, 2023
c7d7ee4
fix: vertical overflow
rohan-chaturvedi Sep 22, 2023
8c33310
fix: disable word wrapping for key names
rohan-chaturvedi Sep 22, 2023
127b143
fix: misc tweaks to invites
rohan-chaturvedi Sep 22, 2023
829e8da
fix: remove member table bg
rohan-chaturvedi Sep 22, 2023
3000c04
fix: conditional logic to show app user action buttons
rohan-chaturvedi Sep 23, 2023
5e3d91c
fix: clean up console.log
rohan-chaturvedi Sep 23, 2023
0566c82
feat: grant user access to all envs when given admin role
rohan-chaturvedi Sep 23, 2023
ce9aab6
feat: better colors for alert
rohan-chaturvedi Sep 23, 2023
90541ef
feat: disallow changing env scope for admins
rohan-chaturvedi Sep 23, 2023
2129583
feat: misc improvements to alert style
rohan-chaturvedi Sep 23, 2023
eddea0c
fix: disable save button when user is admin
rohan-chaturvedi Sep 23, 2023
58edfa4
fix: dialog layout
rohan-chaturvedi Sep 23, 2023
7ee47e9
fix: misc ui improvements
rohan-chaturvedi Sep 24, 2023
07c830e
fix: ui fixes to tokens dialog
rohan-chaturvedi Sep 24, 2023
db13a44
fix: misc ui fixes and cleanup
rohan-chaturvedi Sep 24, 2023
d72ca55
feat: user tokens page, misc styling improvements
rohan-chaturvedi Sep 24, 2023
6c74768
fix: wire app new app dialog with keyring context
rohan-chaturvedi Sep 24, 2023
a6c6835
feat: added SMTP config
nimish-ks Sep 24, 2023
a049888
feat: added login alert email template
nimish-ks Sep 24, 2023
33ab56a
feat: dispatch emails on sso based logins
nimish-ks Sep 24, 2023
8050a65
fix: graphql mutation operation name
rohan-chaturvedi Sep 24, 2023
12b385c
fix: operation name
rohan-chaturvedi Sep 24, 2023
27c69fc
fix: veryify invite in useEffect hook
rohan-chaturvedi Sep 24, 2023
a5f5a70
Multi user (#57)
nimish-ks Sep 25, 2023
417c116
refactor: emails
rohan-chaturvedi Sep 25, 2023
4521be4
feat: grab client user agent and ip during login
rohan-chaturvedi Sep 25, 2023
82f713a
Merge pull request #53 from phasehq/nginx-config
rohan-chaturvedi Sep 25, 2023
1f9c9ce
Merge branch 'multi-user' into multi-user-email
rohan-chaturvedi Sep 25, 2023
2e2ae18
Merge pull request #58 from phasehq/multi-user-email
rohan-chaturvedi Sep 25, 2023
dc86a4b
fix: replace ph-backend with service
rohan-chaturvedi Sep 25, 2023
88e572a
fix: typos
rohan-chaturvedi Sep 25, 2023
2ccb9b1
chore: removed applications from the license
nimish-ks Sep 26, 2023
4851916
fix: get encrypted keyring from backend
rohan-chaturvedi Sep 26, 2023
b0da89e
refactor: use org context to route user post login, correctly check l…
rohan-chaturvedi Sep 26, 2023
7d45b85
fix: re-arrange providers
rohan-chaturvedi Sep 26, 2023
eb2d301
fix: validate org route param
rohan-chaturvedi Sep 26, 2023
53d292a
feat: add organisation list and loading state to org context
rohan-chaturvedi Sep 26, 2023
be5f1d0
feat: allow users with keyring on backend to login on new device with…
rohan-chaturvedi Sep 26, 2023
0719616
Merge branch 'multi-user' into license-updates
rohan-chaturvedi Sep 26, 2023
3d77636
Merge pull request #59 from phasehq/license-updates
rohan-chaturvedi Sep 26, 2023
6b84458
fix: allow all user roles to invite new members
rohan-chaturvedi Sep 26, 2023
0e1e627
fix: check local orgs by id and email
rohan-chaturvedi Sep 26, 2023
8a74b83
fix: misc bugfixes
rohan-chaturvedi Sep 26, 2023
9850cd0
feat: allow switching orgs
rohan-chaturvedi Sep 26, 2023
8b3472b
fix: styling fixes to user menu
rohan-chaturvedi Sep 26, 2023
3c8d881
fix: allow all users to revoke invites
rohan-chaturvedi Sep 26, 2023
f53761f
fix: invite dialog max width
rohan-chaturvedi Sep 26, 2023
1e0049d
feat: encrypted recovery for new users
rohan-chaturvedi Sep 27, 2023
2e9aefb
feat: restyle org selection screen
rohan-chaturvedi Sep 27, 2023
a870453
fix: invite dialog max width
rohan-chaturvedi Sep 27, 2023
f994490
feat: allow skipping recovery step for invited users
rohan-chaturvedi Sep 27, 2023
129432f
refactor: misc fixes, prevent duplicate local keyrings
rohan-chaturvedi Sep 27, 2023
440ba95
fix: refactor settings page, allow viewing account recovery
rohan-chaturvedi Sep 27, 2023
eb9ba95
fix: don't render account panel till activeOrg is truthy
rohan-chaturvedi Sep 27, 2023
41715c1
fix: move service tokens above user tokens
rohan-chaturvedi Sep 27, 2023
0d8a05b
feat: handle updating wrapped keyring and recovery
rohan-chaturvedi Sep 27, 2023
5dbffe4
fix: validate localkeyring before mutation
rohan-chaturvedi Sep 28, 2023
482ed5b
fix: replace ad hoc queries with organisation context
rohan-chaturvedi Sep 28, 2023
7e05ed4
fix: invite link
rohan-chaturvedi Sep 28, 2023
cdd955a
fix: check that apps is truthy before render
rohan-chaturvedi Sep 28, 2023
687e5e4
feat: increased nginx proxy buffer sizes
nimish-ks Sep 28, 2023
7e99c7f
fix: not routing to onboard when logging in with no orgs
rohan-chaturvedi Sep 28, 2023
487b502
fix: misc bug fixes and UI for account recovery
rohan-chaturvedi Sep 28, 2023
959a963
fix: rename onboarding page to /signup
rohan-chaturvedi Sep 28, 2023
22e2989
fix: misc ui fixes to sidebar
rohan-chaturvedi Sep 28, 2023
228c8bc
updated: signup and invite link recovery phase copy
nimish-ks Sep 29, 2023
8c2c62b
Merge pull request #60 from phasehq/mult-user-onboarding-copy
rohan-chaturvedi Sep 29, 2023
9b2692c
fix: user token sorting
rohan-chaturvedi Sep 29, 2023
a9dd7dc
fix: user email for fallback when fullName is not available
rohan-chaturvedi Sep 29, 2023
005526b
fix: add app to creator app list
rohan-chaturvedi Sep 29, 2023
234b0c4
fix: wipe keyring context when switching between orgs
rohan-chaturvedi Sep 29, 2023
3994763
fix: remove user tokens from app tokens tab
rohan-chaturvedi Sep 29, 2023
590533c
feat: init app envs when creating app, correctly catch bad sudo pw exc
rohan-chaturvedi Sep 29, 2023
b70837b
feat: add self property to org member type
rohan-chaturvedi Sep 29, 2023
851d80c
fix: don't allow changing your own role when admin
rohan-chaturvedi Sep 29, 2023
dde62cb
fix: don't allow non admins to delete apps
rohan-chaturvedi Sep 29, 2023
90b6653
fix: handle null selection in combobox
rohan-chaturvedi Sep 29, 2023
ef9e1dd
fix: hide settings tab for devs
rohan-chaturvedi Sep 29, 2023
1a13834
fix: allow devs to view and create service tokens
rohan-chaturvedi Sep 29, 2023
9871d34
fix: rename tokens in sidebar to user tokens
rohan-chaturvedi Sep 29, 2023
9944b83
fix: render env listbox options in row
rohan-chaturvedi Sep 29, 2023
94538da
fix: workspace selection light theme
rohan-chaturvedi Sep 29, 2023
af60b92
fix: horizontal attr for listbox
rohan-chaturvedi Sep 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading