diff --git a/backend/manage.py b/backend/manage.py index e367650..874a35f 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabby.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tabby.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/backend/tabby/app/admin.py b/backend/tabby/app/admin.py index e1118dd..94b45de 100644 --- a/backend/tabby/app/admin.py +++ b/backend/tabby/app/admin.py @@ -5,7 +5,15 @@ from .models import Gateway, User, Config class CustomUserAdmin(UserAdmin): fieldsets = UserAdmin.fieldsets + ( - (None, {'fields': ('custom_connection_gateway', 'custom_connection_gateway_token')}), + ( + None, + { + "fields": ( + "custom_connection_gateway", + "custom_connection_gateway_token", + ) + }, + ), ) diff --git a/backend/tabby/app/api/__init__.py b/backend/tabby/app/api/__init__.py index a78f95e..819cf18 100644 --- a/backend/tabby/app/api/__init__.py +++ b/backend/tabby/app/api/__init__.py @@ -4,14 +4,17 @@ from . import app_version, auth, config, gateway, user router = routers.DefaultRouter(trailing_slash=False) -router.register('api/1/configs', config.ConfigViewSet) -router.register('api/1/versions', app_version.AppVersionViewSet, basename='app-versions') +router.register("api/1/configs", config.ConfigViewSet) +router.register( + "api/1/versions", app_version.AppVersionViewSet, basename="app-versions" +) urlpatterns = [ - path('api/1/auth/logout', auth.LogoutView.as_view()), - path('api/1/user', user.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})), - path('api/1/gateways/choose', gateway.ChooseGatewayViewSet.as_view({'post': 'retrieve'})), - - - path('', include(router.urls)), + path("api/1/auth/logout", auth.LogoutView.as_view()), + path("api/1/user", user.UserViewSet.as_view({"get": "retrieve", "put": "update"})), + path( + "api/1/gateways/choose", + gateway.ChooseGatewayViewSet.as_view({"post": "retrieve"}), + ), + path("", include(router.urls)), ] diff --git a/backend/tabby/app/api/app_version.py b/backend/tabby/app/api/app_version.py index eee339a..19e9002 100644 --- a/backend/tabby/app/api/app_version.py +++ b/backend/tabby/app/api/app_version.py @@ -25,27 +25,28 @@ class AppVersionSerializer(DataclassSerializer): class AppVersionViewSet(ListModelMixin, GenericViewSet): serializer_class = AppVersionSerializer - lookup_field = 'id' - lookup_value_regex = r'[\w\d.-]+' - queryset = '' + lookup_field = "id" + lookup_value_regex = r"[\w\d.-]+" + queryset = "" def _get_versions(self): fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) return [ - self._get_version(x['name']) + self._get_version(x["name"]) for x in fs.listdir(settings.APP_DIST_STORAGE) - if x['type'] == 'directory' + if x["type"] == "directory" ] def _get_version(self, dir): fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) plugins = [ - os.path.basename(x['name']) + os.path.basename(x["name"]) for x in fs.listdir(dir) - if x['type'] == 'directory' and os.path.basename(x['name']) + if x["type"] == "directory" + and os.path.basename(x["name"]) not in [ - 'tabby-web-container', - 'tabby-web-demo', + "tabby-web-container", + "tabby-web-demo", ] ] diff --git a/backend/tabby/app/api/config.py b/backend/tabby/app/api/config.py index 6ba134b..ec6b886 100644 --- a/backend/tabby/app/api/config.py +++ b/backend/tabby/app/api/config.py @@ -10,8 +10,8 @@ class ConfigSerializer(ModelSerializer): class Meta: model = Config - read_only_fields = ('user', 'created_at', 'modified_at') - fields = '__all__' + read_only_fields = ("user", "created_at", "modified_at") + fields = "__all__" class ConfigViewSet(ModelViewSet): diff --git a/backend/tabby/app/api/gateway.py b/backend/tabby/app/api/gateway.py index 98bf911..6e7ff8b 100644 --- a/backend/tabby/app/api/gateway.py +++ b/backend/tabby/app/api/gateway.py @@ -14,7 +14,7 @@ class GatewaySerializer(ModelSerializer): auth_token = fields.CharField() class Meta: - fields = '__all__' + fields = "__all__" model = Gateway def get_url(self, gw): @@ -23,8 +23,8 @@ class GatewaySerializer(ModelSerializer): class NoGatewaysError(APIException): status_code = status.HTTP_503_SERVICE_UNAVAILABLE - default_detail = 'No connection gateways available.' - default_code = 'no_gateways' + default_detail = "No connection gateways available." + default_code = "no_gateways" class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet): diff --git a/backend/tabby/app/api/user.py b/backend/tabby/app/api/user.py index f7856e7..76fae06 100644 --- a/backend/tabby/app/api/user.py +++ b/backend/tabby/app/api/user.py @@ -19,30 +19,34 @@ class UserSerializer(ModelSerializer): class Meta: model = User fields = ( - 'id', - 'username', - 'active_config', - 'custom_connection_gateway', - 'custom_connection_gateway_token', - 'config_sync_token', - 'is_pro', - 'is_sponsor', - 'github_username', + "id", + "username", + "active_config", + "custom_connection_gateway", + "custom_connection_gateway_token", + "config_sync_token", + "is_pro", + "is_sponsor", + "github_username", ) - read_only_fields = ('id', 'username') + read_only_fields = ("id", "username") def get_is_pro(self, obj): - return obj.force_pro or not settings.GITHUB_ELIGIBLE_SPONSORSHIPS or check_is_sponsor_cached(obj) + return ( + obj.force_pro + or not settings.GITHUB_ELIGIBLE_SPONSORSHIPS + or check_is_sponsor_cached(obj) + ) def get_is_sponsor(self, obj): return check_is_sponsor_cached(obj) def get_github_username(self, obj): - social_auth = UserSocialAuth.objects.filter(user=obj, provider='github').first() + social_auth = UserSocialAuth.objects.filter(user=obj, provider="github").first() if not social_auth: return None - return social_auth.extra_data.get('login') + return social_auth.extra_data.get("login") class UserViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet): diff --git a/backend/tabby/app/apps.py b/backend/tabby/app/apps.py index 296cbf2..e0c1032 100644 --- a/backend/tabby/app/apps.py +++ b/backend/tabby/app/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class AppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'tabby.app' + default_auto_field = "django.db.models.BigAutoField" + name = "tabby.app" diff --git a/backend/tabby/app/gateway.py b/backend/tabby/app/gateway.py index 60e680f..f612915 100644 --- a/backend/tabby/app/gateway.py +++ b/backend/tabby/app/gateway.py @@ -27,8 +27,8 @@ class GatewayConnection: ctx.verify_mode = ssl.CERT_REQUIRED GatewayConnection._ssl_context = ctx - proto = 'wss' if GatewayConnection._ssl_context else 'ws' - self.url = f'{proto}://localhost:9000/connect/{quote(host)}:{quote(str(port))}' + proto = "wss" if GatewayConnection._ssl_context else "ws" + self.url = f"{proto}://localhost:9000/connect/{quote(host)}:{quote(str(port))}" async def connect(self): self.context = websockets.connect(self.url, ssl=GatewayConnection._ssl_context) @@ -53,7 +53,9 @@ class GatewayAdminConnection: def __init__(self, gateway: Gateway): if not settings.CONNECTION_GATEWAY_AUTH_KEY: - raise RuntimeError('CONNECTION_GATEWAY_AUTH_KEY is required to manage connection gateways') + raise RuntimeError( + "CONNECTION_GATEWAY_AUTH_KEY is required to manage connection gateways" + ) if not GatewayAdminConnection._ssl_context: ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) ctx.load_cert_chain( @@ -67,10 +69,12 @@ class GatewayAdminConnection: ctx.verify_mode = ssl.CERT_REQUIRED GatewayAdminConnection._ssl_context = ctx - self.url = f'wss://{gateway.host}:{gateway.admin_port}' + self.url = f"wss://{gateway.host}:{gateway.admin_port}" async def connect(self): - self.context = websockets.connect(self.url, ssl=GatewayAdminConnection._ssl_context) + self.context = websockets.connect( + self.url, ssl=GatewayAdminConnection._ssl_context + ) try: self.socket = await self.context.__aenter__() except OSError: @@ -78,10 +82,14 @@ class GatewayAdminConnection: async def authorize_client(self) -> str: token = secrets.token_hex(32) - await self.send(json.dumps({ - '_': 'authorize-client', - 'token': token, - })) + await self.send( + json.dumps( + { + "_": "authorize-client", + "token": token, + } + ) + ) return token async def send(self, data): diff --git a/backend/tabby/app/management/commands/add_version.py b/backend/tabby/app/management/commands/add_version.py index 4d064bf..e87266c 100644 --- a/backend/tabby/app/management/commands/add_version.py +++ b/backend/tabby/app/management/commands/add_version.py @@ -11,52 +11,52 @@ from urllib.parse import urlparse class Command(BaseCommand): - help = 'Downloads a new app version' + help = "Downloads a new app version" def add_arguments(self, parser): - parser.add_argument('version', type=str) + parser.add_argument("version", type=str) def handle(self, *args, **options): - version = options['version'] - target = f'{settings.APP_DIST_STORAGE}/{version}' + version = options["version"] + target = f"{settings.APP_DIST_STORAGE}/{version}" fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) plugin_list = [ - 'tabby-web-container', - 'tabby-core', - 'tabby-settings', - 'tabby-terminal', - 'tabby-ssh', - 'tabby-community-color-schemes', - 'tabby-serial', - 'tabby-telnet', - 'tabby-web', - 'tabby-web-demo', + "tabby-web-container", + "tabby-core", + "tabby-settings", + "tabby-terminal", + "tabby-ssh", + "tabby-community-color-schemes", + "tabby-serial", + "tabby-telnet", + "tabby-web", + "tabby-web-demo", ] with tempfile.TemporaryDirectory() as tempdir: tempdir = Path(tempdir) for plugin in plugin_list: - logging.info(f'Resolving {plugin}@{version}') - response = requests.get(f'{settings.NPM_REGISTRY}/{plugin}/{version}') + logging.info(f"Resolving {plugin}@{version}") + response = requests.get(f"{settings.NPM_REGISTRY}/{plugin}/{version}") response.raise_for_status() info = response.json() - url = info['dist']['tarball'] + url = info["dist"]["tarball"] - logging.info(f'Downloading {plugin}@{version} from {url}') + logging.info(f"Downloading {plugin}@{version} from {url}") response = requests.get(url) - with tempfile.NamedTemporaryFile('wb') as f: + with tempfile.NamedTemporaryFile("wb") as f: f.write(response.content) plugin_final_target = Path(tempdir) / plugin with tempfile.TemporaryDirectory() as extraction_tmp: subprocess.check_call( - ['tar', '-xzf', f.name, '-C', str(extraction_tmp)] + ["tar", "-xzf", f.name, "-C", str(extraction_tmp)] ) shutil.move( - Path(extraction_tmp) / 'package', plugin_final_target + Path(extraction_tmp) / "package", plugin_final_target ) if fs.exists(target): diff --git a/backend/tabby/app/migrations/0001_initial.py b/backend/tabby/app/migrations/0001_initial.py index d30c377..2352700 100644 --- a/backend/tabby/app/migrations/0001_initial.py +++ b/backend/tabby/app/migrations/0001_initial.py @@ -13,63 +13,171 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('active_version', models.CharField(max_length=32, null=True)), - ('custom_connection_gateway', models.CharField(max_length=255, null=True, blank=True)), - ('custom_connection_gateway_token', models.CharField(max_length=255, null=True, blank=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('modified_at', models.DateTimeField(auto_now=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("active_version", models.CharField(max_length=32, null=True)), + ( + "custom_connection_gateway", + models.CharField(max_length=255, null=True, blank=True), + ), + ( + "custom_connection_gateway_token", + models.CharField(max_length=255, null=True, blank=True), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("modified_at", models.DateTimeField(auto_now=True)), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='Config', + name="Config", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(default='{}')), - ('last_used_with_version', models.CharField(max_length=32, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('modified_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='configs', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(default="{}")), + ("last_used_with_version", models.CharField(max_length=32, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("modified_at", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="configs", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AddField( - model_name='user', - name='active_config', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='app.config'), + model_name="user", + name="active_config", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="app.config", + ), ), migrations.AddField( - model_name='user', - name='groups', - field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), + model_name="user", + name="groups", + field=models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), ), migrations.AddField( - model_name='user', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), + model_name="user", + name="user_permissions", + field=models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), ), ] diff --git a/backend/tabby/app/migrations/0002_gateway.py b/backend/tabby/app/migrations/0002_gateway.py index 691a887..ed6e3b8 100644 --- a/backend/tabby/app/migrations/0002_gateway.py +++ b/backend/tabby/app/migrations/0002_gateway.py @@ -6,18 +6,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('app', '0001_initial'), + ("app", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Gateway', + name="Gateway", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('host', models.CharField(max_length=255)), - ('port', models.IntegerField(default=1234)), - ('enabled', models.BooleanField(default=True)), - ('secure', models.BooleanField(default=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("host", models.CharField(max_length=255)), + ("port", models.IntegerField(default=1234)), + ("enabled", models.BooleanField(default=True)), + ("secure", models.BooleanField(default=True)), ], ), ] diff --git a/backend/tabby/app/migrations/0003_auto_20210711_1855.py b/backend/tabby/app/migrations/0003_auto_20210711_1855.py index f34949d..c23399e 100644 --- a/backend/tabby/app/migrations/0003_auto_20210711_1855.py +++ b/backend/tabby/app/migrations/0003_auto_20210711_1855.py @@ -7,18 +7,23 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('app', '0002_gateway'), + ("app", "0002_gateway"), ] operations = [ migrations.AddField( - model_name='gateway', - name='admin_port', + model_name="gateway", + name="admin_port", field=models.IntegerField(default=1235), ), migrations.AlterField( - model_name='user', - name='active_config', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='app.config'), + model_name="user", + name="active_config", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="app.config", + ), ), ] diff --git a/backend/tabby/app/migrations/0004_sync_token.py b/backend/tabby/app/migrations/0004_sync_token.py index e4096d1..699d52e 100644 --- a/backend/tabby/app/migrations/0004_sync_token.py +++ b/backend/tabby/app/migrations/0004_sync_token.py @@ -3,7 +3,7 @@ from django.db import migrations, models def run_forward(apps, schema_editor): - for user in apps.get_model('app', 'User').objects.all(): + for user in apps.get_model("app", "User").objects.all(): user.config_sync_token = secrets.token_hex(64) user.save() @@ -11,19 +11,19 @@ def run_forward(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('app', '0003_auto_20210711_1855'), + ("app", "0003_auto_20210711_1855"), ] operations = [ migrations.AddField( - model_name='user', - name='config_sync_token', + model_name="user", + name="config_sync_token", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.RunPython(run_forward, lambda _, __: None), migrations.AlterField( - model_name='user', - name='config_sync_token', + model_name="user", + name="config_sync_token", field=models.CharField(max_length=255), ), ] diff --git a/backend/tabby/app/migrations/0005_user_force_pro.py b/backend/tabby/app/migrations/0005_user_force_pro.py index ec276fa..35c2af4 100644 --- a/backend/tabby/app/migrations/0005_user_force_pro.py +++ b/backend/tabby/app/migrations/0005_user_force_pro.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('app', '0004_sync_token'), + ("app", "0004_sync_token"), ] operations = [ migrations.AddField( - model_name='user', - name='force_pro', + model_name="user", + name="force_pro", field=models.BooleanField(default=False), ), ] diff --git a/backend/tabby/app/migrations/0006_config_name.py b/backend/tabby/app/migrations/0006_config_name.py index e60c737..3fe7eb9 100644 --- a/backend/tabby/app/migrations/0006_config_name.py +++ b/backend/tabby/app/migrations/0006_config_name.py @@ -2,27 +2,27 @@ from django.db import migrations, models def run_forward(apps, schema_editor): - for config in apps.get_model('app', 'Config').objects.all(): - config.name = f'Unnamed config ({config.created_at.date()})' + for config in apps.get_model("app", "Config").objects.all(): + config.name = f"Unnamed config ({config.created_at.date()})" config.save() class Migration(migrations.Migration): dependencies = [ - ('app', '0005_user_force_pro'), + ("app", "0005_user_force_pro"), ] operations = [ migrations.AddField( - model_name='config', - name='name', + model_name="config", + name="name", field=models.CharField(max_length=255, null=True), ), migrations.RunPython(run_forward, lambda _, __: None), migrations.AlterField( - model_name='config', - name='name', + model_name="config", + name="name", field=models.CharField(max_length=255), ), ] diff --git a/backend/tabby/app/models.py b/backend/tabby/app/models.py index b4e0c00..46442ae 100644 --- a/backend/tabby/app/models.py +++ b/backend/tabby/app/models.py @@ -5,24 +5,30 @@ from django.contrib.auth.models import AbstractUser class Config(models.Model): - user = models.ForeignKey('app.User', related_name='configs', on_delete=models.CASCADE) + user = models.ForeignKey( + "app.User", related_name="configs", on_delete=models.CASCADE + ) name = models.CharField(max_length=255) - content = models.TextField(default='{}') + content = models.TextField(default="{}") last_used_with_version = models.CharField(max_length=32, null=True) created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): if not self.name: - self.name = f'Unnamed config ({date.today()})' + self.name = f"Unnamed config ({date.today()})" super().save(*args, **kwargs) class User(AbstractUser): - active_config = models.ForeignKey(Config, null=True, on_delete=models.SET_NULL, related_name='+') + active_config = models.ForeignKey( + Config, null=True, on_delete=models.SET_NULL, related_name="+" + ) active_version = models.CharField(max_length=32, null=True) custom_connection_gateway = models.CharField(max_length=255, null=True, blank=True) - custom_connection_gateway_token = models.CharField(max_length=255, null=True, blank=True) + custom_connection_gateway_token = models.CharField( + max_length=255, null=True, blank=True + ) config_sync_token = models.CharField(max_length=255) force_pro = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) @@ -42,4 +48,4 @@ class Gateway(models.Model): secure = models.BooleanField(default=True) def __str__(self): - return f'{self.host}:{self.port}' + return f"{self.host}:{self.port}" diff --git a/backend/tabby/app/sponsors.py b/backend/tabby/app/sponsors.py index e6444e3..9c85ee6 100644 --- a/backend/tabby/app/sponsors.py +++ b/backend/tabby/app/sponsors.py @@ -7,13 +7,13 @@ from social_django.models import UserSocialAuth from .models import User -GQL_ENDPOINT = 'https://api.github.com/graphql' -CACHE_KEY = 'cached-sponsors:%s' +GQL_ENDPOINT = "https://api.github.com/graphql" +CACHE_KEY = "cached-sponsors:%s" def check_is_sponsor(user: User) -> bool: try: - token = user.social_auth.get(provider='github').extra_data.get('access_token') + token = user.social_auth.get(provider="github").extra_data.get("access_token") except UserSocialAuth.DoesNotExist: return False @@ -25,19 +25,19 @@ def check_is_sponsor(user: User) -> bool: url=GQL_ENDPOINT, use_json=True, headers={ - 'Authorization': f'Bearer {token}', - } + "Authorization": f"Bearer {token}", + }, ) ) after = None while True: - params = 'first: 1' + params = "first: 1" if after: params += f', after:"{after}"' - query = ''' + query = """ query { viewer { sponsorshipsAsSponsor(%s) { @@ -56,18 +56,26 @@ def check_is_sponsor(user: User) -> bool: } } } - ''' % (params,) + """ % ( + params, + ) response = client.execute(gql(query)) - info = response['viewer']['sponsorshipsAsSponsor'] - after = info['pageInfo']['endCursor'] - nodes = info['nodes'] + info = response["viewer"]["sponsorshipsAsSponsor"] + after = info["pageInfo"]["endCursor"] + nodes = info["nodes"] if not len(nodes): break for node in nodes: - if node['sponsorable']['login'].lower() not in settings.GITHUB_ELIGIBLE_SPONSORSHIPS: + if ( + node["sponsorable"]["login"].lower() + not in settings.GITHUB_ELIGIBLE_SPONSORSHIPS + ): continue - if info['totalRecurringMonthlyPriceInDollars'] >= settings.GITHUB_SPONSORS_MIN_PAYMENT: + if ( + info["totalRecurringMonthlyPriceInDollars"] + >= settings.GITHUB_SPONSORS_MIN_PAYMENT + ): return True return False diff --git a/backend/tabby/app/urls.py b/backend/tabby/app/urls.py index 847f493..2fff4b9 100644 --- a/backend/tabby/app/urls.py +++ b/backend/tabby/app/urls.py @@ -7,12 +7,10 @@ from . import views urlpatterns = [ *[ path(p, views.IndexView.as_view()) - for p in ['', 'login', 'app', 'about', 'about/features'] + for p in ["", "login", "app", "about", "about/features"] ], - - path('app-dist//', views.AppDistView.as_view()), - path('terminal', views.TerminalView.as_view()), - path('demo', views.DemoView.as_view()), - - path('', include(api.urlpatterns)), + path("app-dist//", views.AppDistView.as_view()), + path("terminal", views.TerminalView.as_view()), + path("demo", views.DemoView.as_view()), + path("", include(api.urlpatterns)), ] diff --git a/backend/tabby/app/views.py b/backend/tabby/app/views.py index 07ae0dd..ef7575c 100644 --- a/backend/tabby/app/views.py +++ b/backend/tabby/app/views.py @@ -2,7 +2,11 @@ import fsspec import os from fsspec.implementations.local import LocalFileSystem from django.conf import settings -from django.http.response import FileResponse, HttpResponseNotFound, HttpResponseRedirect +from django.http.response import ( + FileResponse, + HttpResponseNotFound, + HttpResponseRedirect, +) from django.views import static from rest_framework.views import APIView from urllib.parse import urlparse @@ -12,27 +16,33 @@ class IndexView(APIView): def get(self, request, format=None): if settings.FRONTEND_URL: return HttpResponseRedirect(settings.FRONTEND_URL) - return static.serve(request, 'index.html', document_root=str(settings.STATIC_ROOT)) + return static.serve( + request, "index.html", document_root=str(settings.STATIC_ROOT) + ) class TerminalView(APIView): def get(self, request, format=None): - response = static.serve(request, 'terminal.html', document_root=str(settings.STATIC_ROOT)) - response['X-Frame-Options'] = 'SAMEORIGIN' + response = static.serve( + request, "terminal.html", document_root=str(settings.STATIC_ROOT) + ) + response["X-Frame-Options"] = "SAMEORIGIN" return response class DemoView(APIView): def get(self, request, format=None): - response = static.serve(request, 'demo.html', document_root=str(settings.STATIC_ROOT)) - response['Content-Security-Policy'] = 'frame-ancestors https://tabby.sh' + response = static.serve( + request, "demo.html", document_root=str(settings.STATIC_ROOT) + ) + response["Content-Security-Policy"] = "frame-ancestors https://tabby.sh" return response class AppDistView(APIView): def get(self, request, version=None, path=None, format=None): fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) - url = f'{settings.APP_DIST_STORAGE}/{version}/{path}' + url = f"{settings.APP_DIST_STORAGE}/{version}/{path}" if isinstance(fs, LocalFileSystem): if not fs.exists(url): return HttpResponseNotFound() diff --git a/backend/tabby/middleware.py b/backend/tabby/middleware.py index a7f1a88..5197501 100644 --- a/backend/tabby/middleware.py +++ b/backend/tabby/middleware.py @@ -13,20 +13,20 @@ class BaseMiddleware: class TokenMiddleware(BaseMiddleware): def __call__(self, request): token_value = None - if 'auth_token' in request.GET: - token_value = request.GET['auth_token'] - if request.META.get('HTTP_AUTHORIZATION'): - token_type, *credentials = request.META['HTTP_AUTHORIZATION'].split() - if token_type == 'Bearer' and len(credentials): + if "auth_token" in request.GET: + token_value = request.GET["auth_token"] + if request.META.get("HTTP_AUTHORIZATION"): + token_type, *credentials = request.META["HTTP_AUTHORIZATION"].split() + if token_type == "Bearer" and len(credentials): token_value = credentials[0] user = User.objects.filter(config_sync_token=token_value).first() if user: request.session.save = lambda *args, **kwargs: None - setattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend') + setattr(user, "backend", "django.contrib.auth.backends.ModelBackend") login(request, user) - setattr(request, '_dont_enforce_csrf_checks', True) + setattr(request, "_dont_enforce_csrf_checks", True) response = self.get_response(request) @@ -44,7 +44,7 @@ class GAMiddleware(BaseMiddleware): def __call__(self, request): response = self.get_response(request) - if settings.GA_ID and request.path in ['/', '/app']: + if settings.GA_ID and request.path in ["/", "/app"]: try: self.tracker.track_pageview(Page(request.path), Session(), Visitor()) except Exception: diff --git a/backend/tabby/settings.py b/backend/tabby/settings.py index 597f262..c7c3166 100644 --- a/backend/tabby/settings.py +++ b/backend/tabby/settings.py @@ -9,99 +9,95 @@ load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure') -DEBUG = bool(os.getenv('DEBUG', False)) +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "django-insecure") +DEBUG = bool(os.getenv("DEBUG", False)) -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] USE_X_FORWARDED_HOST = True -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'social_django', - 'corsheaders', - 'tabby.app', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "social_django", + "corsheaders", + "tabby.app", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'tabby.middleware.TokenMiddleware', - 'tabby.middleware.GAMiddleware', + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "corsheaders.middleware.CorsMiddleware", + "tabby.middleware.TokenMiddleware", + "tabby.middleware.GAMiddleware", ] -ROOT_URLCONF = 'tabby.urls' +ROOT_URLCONF = "tabby.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'tabby.wsgi.application' +WSGI_APPLICATION = "tabby.wsgi.application" -DATABASES = { - 'default': dj_database_url.config(conn_max_age=600) -} +DATABASES = {"default": dj_database_url.config(conn_max_age=600)} CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", } } AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -AUTH_USER_MODEL = 'app.User' +AUTH_USER_MODEL = "app.User" REST_FRAMEWORK = { - 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', - ) + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",) } -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -110,144 +106,144 @@ USE_L10N = True USE_TZ = True LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'simple': { - 'format': '%(levelname)s %(message)s' + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": {"format": "%(levelname)s %(message)s"}, + }, + "handlers": { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "simple", }, }, - 'handlers': { - 'console': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'simple' - }, - }, - 'loggers': { - '': { - 'handlers': ['console'], - 'propagate': False, - 'level': 'INFO', + "loggers": { + "": { + "handlers": ["console"], + "propagate": False, + "level": "INFO", }, }, } -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" CSRF_USE_SESSIONS = False CSRF_COOKIE_HTTPONLY = False -CSRF_COOKIE_NAME = 'XSRF-TOKEN' -CSRF_HEADER_NAME = 'HTTP_X_XSRF_TOKEN' +CSRF_COOKIE_NAME = "XSRF-TOKEN" +CSRF_HEADER_NAME = "HTTP_X_XSRF_TOKEN" AUTHENTICATION_BACKENDS = ( - 'social_core.backends.github.GithubOAuth2', - 'social_core.backends.gitlab.GitLabOAuth2', - 'social_core.backends.azuread.AzureADOAuth2', - 'social_core.backends.microsoft.MicrosoftOAuth2', - 'social_core.backends.google.GoogleOAuth2', - 'django.contrib.auth.backends.ModelBackend', + "social_core.backends.github.GithubOAuth2", + "social_core.backends.gitlab.GitLabOAuth2", + "social_core.backends.azuread.AzureADOAuth2", + "social_core.backends.microsoft.MicrosoftOAuth2", + "social_core.backends.google.GoogleOAuth2", + "django.contrib.auth.backends.ModelBackend", ) -SOCIAL_AUTH_GITHUB_SCOPE = ['read:user', 'user:email'] +SOCIAL_AUTH_GITHUB_SCOPE = ["read:user", "user:email"] SOCIAL_AUTH_PIPELINE = ( - 'social_core.pipeline.social_auth.social_details', - 'social_core.pipeline.social_auth.social_uid', - 'social_core.pipeline.social_auth.auth_allowed', - 'social_core.pipeline.social_auth.social_user', - 'social_core.pipeline.user.get_username', - 'social_core.pipeline.social_auth.associate_by_email', - 'social_core.pipeline.user.create_user', - 'social_core.pipeline.social_auth.associate_user', - 'social_core.pipeline.social_auth.load_extra_data', - 'social_core.pipeline.user.user_details', + "social_core.pipeline.social_auth.social_details", + "social_core.pipeline.social_auth.social_uid", + "social_core.pipeline.social_auth.auth_allowed", + "social_core.pipeline.social_auth.social_user", + "social_core.pipeline.user.get_username", + "social_core.pipeline.social_auth.associate_by_email", + "social_core.pipeline.user.create_user", + "social_core.pipeline.social_auth.associate_user", + "social_core.pipeline.social_auth.load_extra_data", + "social_core.pipeline.user.user_details", ) -APP_DIST_STORAGE = os.getenv('APP_DIST_STORAGE', 'file://' + str(BASE_DIR / 'app-dist')) -NPM_REGISTRY = os.getenv('NPM_REGISTRY', 'https://registry.npmjs.org').rstrip('/') -FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / '../frontend/build')) +APP_DIST_STORAGE = os.getenv("APP_DIST_STORAGE", "file://" + str(BASE_DIR / "app-dist")) +NPM_REGISTRY = os.getenv("NPM_REGISTRY", "https://registry.npmjs.org").rstrip("/") +FRONTEND_BUILD_DIR = Path( + os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "../frontend/build") +) FRONTEND_URL = None BACKEND_URL = None GITHUB_ELIGIBLE_SPONSORSHIPS = None for key in [ - 'FRONTEND_URL', - 'BACKEND_URL', - 'SOCIAL_AUTH_GITHUB_KEY', - 'SOCIAL_AUTH_GITHUB_SECRET', - 'SOCIAL_AUTH_GITLAB_KEY', - 'SOCIAL_AUTH_GITLAB_SECRET', - 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', - 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET', - 'SOCIAL_AUTH_MICROSOFT_GRAPH_KEY', - 'SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET', - 'CONNECTION_GATEWAY_AUTH_CA', - 'CONNECTION_GATEWAY_AUTH_CERTIFICATE', - 'CONNECTION_GATEWAY_AUTH_KEY', - 'GITHUB_ELIGIBLE_SPONSORSHIPS', - 'GITHUB_SPONSORS_MIN_PAYMENT', - 'GA_ID', - 'GA_DOMAIN', + "FRONTEND_URL", + "BACKEND_URL", + "SOCIAL_AUTH_GITHUB_KEY", + "SOCIAL_AUTH_GITHUB_SECRET", + "SOCIAL_AUTH_GITLAB_KEY", + "SOCIAL_AUTH_GITLAB_SECRET", + "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", + "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", + "SOCIAL_AUTH_MICROSOFT_GRAPH_KEY", + "SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET", + "CONNECTION_GATEWAY_AUTH_CA", + "CONNECTION_GATEWAY_AUTH_CERTIFICATE", + "CONNECTION_GATEWAY_AUTH_KEY", + "GITHUB_ELIGIBLE_SPONSORSHIPS", + "GITHUB_SPONSORS_MIN_PAYMENT", + "GA_ID", + "GA_DOMAIN", ]: globals()[key] = os.getenv(key) for key in [ - 'GITHUB_SPONSORS_MIN_PAYMENT', + "GITHUB_SPONSORS_MIN_PAYMENT", ]: globals()[key] = int(globals()[key]) if globals()[key] else None for key in [ - 'CONNECTION_GATEWAY_AUTH_CA', - 'CONNECTION_GATEWAY_AUTH_CERTIFICATE', - 'CONNECTION_GATEWAY_AUTH_KEY', + "CONNECTION_GATEWAY_AUTH_CA", + "CONNECTION_GATEWAY_AUTH_CERTIFICATE", + "CONNECTION_GATEWAY_AUTH_KEY", ]: v = globals()[key] if v and not os.path.exists(v): - raise ValueError(f'{v} does not exist') + raise ValueError(f"{v} does not exist") if GITHUB_ELIGIBLE_SPONSORSHIPS: - GITHUB_ELIGIBLE_SPONSORSHIPS = GITHUB_ELIGIBLE_SPONSORSHIPS.split(',') + GITHUB_ELIGIBLE_SPONSORSHIPS = GITHUB_ELIGIBLE_SPONSORSHIPS.split(",") else: GITHUB_ELIGIBLE_SPONSORSHIPS = [] -STATIC_URL = '/static/' +STATIC_URL = "/static/" if FRONTEND_BUILD_DIR.exists(): STATICFILES_DIRS = [FRONTEND_BUILD_DIR] -STATIC_ROOT = BASE_DIR / 'public' +STATIC_ROOT = BASE_DIR / "public" if FRONTEND_URL: - CORS_ALLOWED_ORIGINS = [FRONTEND_URL, 'https://tabby.sh'] + CORS_ALLOWED_ORIGINS = [FRONTEND_URL, "https://tabby.sh"] CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-xsrf-token', - 'x-requested-with', + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-xsrf-token", + "x-requested-with", ] frontend_domain = urlparse(FRONTEND_URL).hostname CSRF_TRUSTED_ORIGINS = [frontend_domain] if BACKEND_URL: CSRF_TRUSTED_ORIGINS.append(urlparse(BACKEND_URL).hostname) - SESSION_COOKIE_DOMAIN = os.getenv('SESSION_COOKIE_DOMAIN', frontend_domain) + SESSION_COOKIE_DOMAIN = os.getenv("SESSION_COOKIE_DOMAIN", frontend_domain) SESSION_COOKIE_SAMESITE = None CSRF_COOKIE_DOMAIN = frontend_domain - FRONTEND_URL = FRONTEND_URL.rstrip('/') + FRONTEND_URL = FRONTEND_URL.rstrip("/") - if FRONTEND_URL.startswith('https://'): + if FRONTEND_URL.startswith("https://"): CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True else: - FRONTEND_URL = '' + FRONTEND_URL = "" -LOGIN_REDIRECT_URL = FRONTEND_URL + '/app' +LOGIN_REDIRECT_URL = FRONTEND_URL + "/app" diff --git a/backend/tabby/urls.py b/backend/tabby/urls.py index 91966bc..134c8d3 100644 --- a/backend/tabby/urls.py +++ b/backend/tabby/urls.py @@ -4,7 +4,7 @@ from django.urls import path, include from .app.urls import urlpatterns as app_urlpatterns urlpatterns = [ - path('', include(app_urlpatterns)), - path('api/1/auth/social/', include('social_django.urls', namespace='social')), - path('admin/', admin.site.urls), + path("", include(app_urlpatterns)), + path("api/1/auth/social/", include("social_django.urls", namespace="social")), + path("admin/", admin.site.urls), ] diff --git a/backend/tabby/wsgi.py b/backend/tabby/wsgi.py index 7c90597..0271c14 100644 --- a/backend/tabby/wsgi.py +++ b/backend/tabby/wsgi.py @@ -2,6 +2,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabby.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tabby.settings") application = get_wsgi_application()