This commit is contained in:
Eugene Pankov 2022-11-07 18:56:10 +01:00
parent 05b476aa23
commit 99264d2bfc
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
24 changed files with 476 additions and 313 deletions

View File

@ -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()

View File

@ -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",
)
},
),
)

View File

@ -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)),
]

View File

@ -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",
]
]

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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"

View File

@ -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):

View File

@ -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):

View File

@ -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",
),
),
]

View File

@ -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)),
],
),
]

View File

@ -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",
),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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}"

View File

@ -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

View File

@ -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/<version>/<path:path>', views.AppDistView.as_view()),
path('terminal', views.TerminalView.as_view()),
path('demo', views.DemoView.as_view()),
path('', include(api.urlpatterns)),
path("app-dist/<version>/<path:path>", views.AppDistView.as_view()),
path("terminal", views.TerminalView.as_view()),
path("demo", views.DemoView.as_view()),
path("", include(api.urlpatterns)),
]

View File

@ -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()

View File

@ -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:

View File

@ -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"

View File

@ -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),
]

View File

@ -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()