From 3de04221c27062de2d63c4833b8ae87bb2bd1db9 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sat, 24 Jul 2021 15:48:12 +0200 Subject: [PATCH] wip --- package.json | 1 + src/api.ts | 2 + src/app.module.ts | 2 + src/components/configModal.component.pug | 4 +- src/components/home.component.ts | 5 +- src/components/settingsModal.component.pug | 22 ++++++- src/components/settingsModal.component.ts | 3 + src/theme/index.scss | 4 +- src/theme/vars.scss | 4 ++ tabby/app/api.py | 15 +++-- tabby/app/migrations/0001_initial.py | 4 +- tabby/app/migrations/0004_sync_token.py | 29 +++++++++ tabby/app/migrations/0005_user_force_pro.py | 18 ++++++ tabby/app/migrations/0006_config_name.py | 28 +++++++++ tabby/app/models.py | 18 +++++- tabby/app/sponsors.py | 67 ++++++++++++--------- tabby/middleware.py | 35 ++++++++++- tabby/settings.py | 10 ++- yarn.lock | 16 ++++- 19 files changed, 238 insertions(+), 49 deletions(-) create mode 100644 tabby/app/migrations/0004_sync_token.py create mode 100644 tabby/app/migrations/0005_user_force_pro.py create mode 100644 tabby/app/migrations/0006_config_name.py diff --git a/package.json b/package.json index 7805186..6c318c1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "private": true, "devDependencies": { "@angular/animations": "^11.0.0", + "@angular/cdk": "^12.1.3", "@angular/common": "^11.0.0", "@angular/compiler": "^11.0.0", "@angular/compiler-cli": "^11.0.0", diff --git a/src/api.ts b/src/api.ts index d596e90..6670436 100644 --- a/src/api.ts +++ b/src/api.ts @@ -8,6 +8,8 @@ export interface User { active_version: string custom_connection_gateway: string|null custom_connection_gateway_token: string|null + config_sync_token: string + github_username: string is_pro: boolean } diff --git a/src/app.module.ts b/src/app.module.ts index cbf5bdd..3678811 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { CommonModule } from '@angular/common' import { FormsModule } from '@angular/forms' import { RouterModule } from '@angular/router' import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http' +import { ClipboardModule } from '@angular/cdk/clipboard' import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' import { AppComponent } from './components/app.component' import { MainComponent } from './components/main.component' @@ -50,6 +51,7 @@ const ROUTES = [ NgbDropdownModule, NgbModalModule, FontAwesomeModule, + ClipboardModule, RouterModule.forRoot(ROUTES), ], declarations: [ diff --git a/src/components/configModal.component.pug b/src/components/configModal.component.pug index c6cba10..fb95bdb 100644 --- a/src/components/configModal.component.pug +++ b/src/components/configModal.component.pug @@ -8,7 +8,7 @@ label Active config .title fa-icon([icon]='_configIcon') - span.ms-2 {{configService.activeConfig.created_at|date:"medium"}} + span.ms-2 {{configService.activeConfig.name}} button.btn.btn-semi.me-2((click)='configService.duplicateActiveConfig()') fa-icon([icon]='_copyIcon', [fixedWidth]='true') @@ -38,7 +38,7 @@ (click)='selectConfig(config)' ) fa-icon([icon]='_configIcon') - span Config created at {{config.created_at|date:"medium"}} + span {{config.name}} .py-3 button.btn.btn-semi.w-100((click)='createNewConfig()') diff --git a/src/components/home.component.ts b/src/components/home.component.ts index d017e0e..937dd19 100644 --- a/src/components/home.component.ts +++ b/src/components/home.component.ts @@ -15,7 +15,10 @@ class DemoConnector { async loadConfig (): Promise { return `{ - recoverTabs: false + recoverTabs: false, + web: { + preventAccidentalTabClosure: false, + }, }` } diff --git a/src/components/settingsModal.component.pug b/src/components/settingsModal.component.pug index 3bfa6e8..1ec685c 100644 --- a/src/components/settingsModal.component.pug +++ b/src/components/settingsModal.component.pug @@ -4,11 +4,29 @@ .modal-body .mb-3 h5 GitHub account - a.btn.btn-info(href='/api/1/auth/social/login/github') + a.btn.btn-info(href='/api/1/auth/social/login/github', *ngIf='!user.github_username') fa-icon([icon]='_githubIcon', [fixedWidth]='true') span Connect a GitHub account + .alert.alert-success.d-flex(*ngIf='user.github_username') + fa-icon.me-2([icon]='_okIcon', [fixedWidth]='true') + div + div Connected as #[strong {{user.github_username}}] + div(*ngIf='user.is_sponsor') Thank you for supporting Tabby on GitHub! - .mb-3 + .mb-3.mt-4 + h5 Config sync + .d-flex.aling-items-stretch.mb-3 + .form-floating.w-100 + input.form-control( + type='text', + readonly, + [ngModel]='user.config_sync_token' + ) + label Sync token for the Tabby app + button.btn.btn-dark([cdkCopyToClipboard]='user.config_sync_token') + fa-icon([icon]='_copyIcon', [fixedWidth]='true') + + .mb-3.mt-4 h5 Connection gateway .form-check.form-switch input.form-check-input( diff --git a/src/components/settingsModal.component.ts b/src/components/settingsModal.component.ts index 68db3f4..6f344cc 100644 --- a/src/components/settingsModal.component.ts +++ b/src/components/settingsModal.component.ts @@ -5,6 +5,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { User } from '../api' import { AppConnectorService } from '../services/appConnector.service' import { faGithub } from '@fortawesome/free-brands-svg-icons' +import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons' @Component({ selector: 'settings-modal', @@ -15,6 +16,8 @@ export class SettingsModalComponent { user: User customGatewayEnabled = false _githubIcon = faGithub + _copyIcon = faCopy + _okIcon = faCheck constructor ( public appConnector: AppConnectorService, diff --git a/src/theme/index.scss b/src/theme/index.scss index b52e202..38e382d 100644 --- a/src/theme/index.scss +++ b/src/theme/index.scss @@ -19,12 +19,12 @@ @import "~bootstrap/scss/button-group"; // @import "~bootstrap/scss/nav"; // @import "~bootstrap/scss/navbar"; -// @import "~bootstrap/scss/card"; +@import "~bootstrap/scss/card"; // @import "~bootstrap/scss/accordion"; // @import "~bootstrap/scss/breadcrum/b"; // @import "~bootstrap/scss/pagination"; @import "~bootstrap/scss/badge"; -// @import "~bootstrap/scss/alert"; +@import "~bootstrap/scss/alert"; // @import "~bootstrap/scss/progress"; @import "~bootstrap/scss/list-group"; // @import "~bootstrap/scss/close"; diff --git a/src/theme/vars.scss b/src/theme/vars.scss index cc8c887..fac6560 100644 --- a/src/theme/vars.scss +++ b/src/theme/vars.scss @@ -191,3 +191,7 @@ $modal-content-border-width: 1px; $progress-bg: $table-bg; $progress-height: 3px; + +$alert-bg-scale: 90%; +$alert-border-scale: 50%; +$alert-color-scale: 50%; diff --git a/tabby/app/api.py b/tabby/app/api.py index d42c030..e8b25d2 100644 --- a/tabby/app/api.py +++ b/tabby/app/api.py @@ -17,7 +17,7 @@ from social_django.models import UserSocialAuth from typing import List from .consumers import GatewayAdminConnection -from .sponsors import get_sponsor_usernames +from .sponsors import check_is_sponsor, check_is_sponsor_cached from .models import Config, Gateway, User @@ -45,6 +45,8 @@ class GatewaySerializer(ModelSerializer): class ConfigSerializer(ModelSerializer): + name = fields.CharField(required=False) + class Meta: model = Config read_only_fields = ('user', 'created_at', 'modified_at') @@ -100,6 +102,7 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet): class UserSerializer(ModelSerializer): id = fields.IntegerField() is_pro = fields.SerializerMethodField() + is_sponsor = fields.SerializerMethodField() github_username = fields.SerializerMethodField() class Meta: @@ -110,16 +113,18 @@ class UserSerializer(ModelSerializer): 'active_config', 'custom_connection_gateway', 'custom_connection_gateway_token', + 'config_sync_token', 'is_pro', + 'is_sponsor', 'github_username', ) read_only_fields = ('id', 'username') def get_is_pro(self, obj): - username = self.get_github_username(obj) - if not username: - return False - return username in get_sponsor_usernames() + return check_is_sponsor_cached(obj) or obj.force_pro + + 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() diff --git a/tabby/app/migrations/0001_initial.py b/tabby/app/migrations/0001_initial.py index e81aa92..d30c377 100644 --- a/tabby/app/migrations/0001_initial.py +++ b/tabby/app/migrations/0001_initial.py @@ -32,8 +32,8 @@ class Migration(migrations.Migration): ('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)), - ('custom_connection_gateway_token', models.CharField(max_length=255, 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)), ], diff --git a/tabby/app/migrations/0004_sync_token.py b/tabby/app/migrations/0004_sync_token.py new file mode 100644 index 0000000..e4096d1 --- /dev/null +++ b/tabby/app/migrations/0004_sync_token.py @@ -0,0 +1,29 @@ +import secrets +from django.db import migrations, models + + +def run_forward(apps, schema_editor): + for user in apps.get_model('app', 'User').objects.all(): + user.config_sync_token = secrets.token_hex(64) + user.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0003_auto_20210711_1855'), + ] + + operations = [ + migrations.AddField( + 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', + field=models.CharField(max_length=255), + ), + ] diff --git a/tabby/app/migrations/0005_user_force_pro.py b/tabby/app/migrations/0005_user_force_pro.py new file mode 100644 index 0000000..ec276fa --- /dev/null +++ b/tabby/app/migrations/0005_user_force_pro.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.3 on 2021-07-24 10:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0004_sync_token'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='force_pro', + field=models.BooleanField(default=False), + ), + ] diff --git a/tabby/app/migrations/0006_config_name.py b/tabby/app/migrations/0006_config_name.py new file mode 100644 index 0000000..e60c737 --- /dev/null +++ b/tabby/app/migrations/0006_config_name.py @@ -0,0 +1,28 @@ +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()})' + config.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0005_user_force_pro'), + ] + + operations = [ + migrations.AddField( + 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', + field=models.CharField(max_length=255), + ), + ] diff --git a/tabby/app/models.py b/tabby/app/models.py index bc177b7..b4e0c00 100644 --- a/tabby/app/models.py +++ b/tabby/app/models.py @@ -1,26 +1,38 @@ +import secrets +from datetime import date from django.db import models from django.contrib.auth.models import AbstractUser -from django.contrib.auth.signals import user_logged_in -from django.dispatch import receiver -from django.db.models.signals import post_save class Config(models.Model): user = models.ForeignKey('app.User', related_name='configs', on_delete=models.CASCADE) + name = models.CharField(max_length=255) 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()})' + super().save(*args, **kwargs) + class User(AbstractUser): 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) + config_sync_token = models.CharField(max_length=255) + force_pro = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) + def save(self, *args, **kwargs): + if not self.config_sync_token: + self.config_sync_token = secrets.token_hex(64) + super().save(*args, **kwargs) + class Gateway(models.Model): host = models.CharField(max_length=255) diff --git a/tabby/app/sponsors.py b/tabby/app/sponsors.py index 3330bcd..366495b 100644 --- a/tabby/app/sponsors.py +++ b/tabby/app/sponsors.py @@ -2,25 +2,34 @@ from django.conf import settings from django.core.cache import cache from gql import Client, gql from gql.transport.requests import RequestsHTTPTransport +from social_django.models import UserSocialAuth + +from .models import User GQL_ENDPOINT = 'https://api.github.com/graphql' -CACHE_KEY = 'cached-sponsors' +CACHE_KEY = 'cached-sponsors:%s' -def fetch_sponsor_usernames(): +def check_is_sponsor(user: User) -> bool: + try: + token = user.social_auth.get(provider='github').extra_data.get('access_token') + except UserSocialAuth.DoesNotExist: + return False + + if not token: + return False + client = Client( transport=RequestsHTTPTransport( url=GQL_ENDPOINT, use_json=True, headers={ - 'Authorization': f'Bearer {settings.GITHUB_TOKEN}', + 'Authorization': f'Bearer {token}', } ) ) - result = [] - after = None while True: @@ -31,21 +40,17 @@ def fetch_sponsor_usernames(): query = ''' query { user (login: "eugeny") { - sponsorshipsAsMaintainer(%s, includePrivate: true) { - pageInfo { - startCursor - hasNextPage - endCursor - } - nodes { - createdAt - tier { - monthlyPriceInDollars - } - sponsor{ - ... on User { - login - } + sponsorshipsAsSponsor(%s) { + pageInfo { + startCursor + hasNextPage + endCursor + } + totalRecurringMonthlyPriceInDollars + nodes { + sponsorable { + ... on Organization { login } + ... on User { login } } } } @@ -54,18 +59,22 @@ def fetch_sponsor_usernames(): ''' % (params,) response = client.execute(gql(query)) - after = response['user']['sponsorshipsAsMaintainer']['pageInfo']['endCursor'] - nodes = response['user']['sponsorshipsAsMaintainer']['nodes'] + info = response['user']['sponsorshipsAsSponsor'] + after = info['pageInfo']['endCursor'] + nodes = info['nodes'] if not len(nodes): break for node in nodes: - if node['tier']['monthlyPriceInDollars'] >= settings.GITHUB_SPONSORS_MIN_PAYMENT: - result.append(node['sponsor']['login']) + if node['sponsorable']['login'].lower() not in settings.GITHUB_ELIGIBLE_SPONSORSHIPS: + continue + if info['totalRecurringMonthlyPriceInDollars'] >= settings.GITHUB_SPONSORS_MIN_PAYMENT: + return True - return result + return False -def get_sponsor_usernames(): - if not cache.get(CACHE_KEY): - cache.set(CACHE_KEY, fetch_sponsor_usernames(), timeout=30) - return cache.get(CACHE_KEY) +def check_is_sponsor_cached(user: User) -> bool: + cache_key = CACHE_KEY % user.id + if not cache.get(cache_key): + cache.set(cache_key, check_is_sponsor(user), timeout=30) + return cache.get(cache_key) diff --git a/tabby/middleware.py b/tabby/middleware.py index 8f6948f..8b1ce2e 100644 --- a/tabby/middleware.py +++ b/tabby/middleware.py @@ -1,11 +1,44 @@ import logging +from tabby.app.models import User from django.conf import settings +from django.contrib.auth import hashers, logout, login from pyga.requests import Tracker, Page, Session, Visitor -class GAMiddleware: +class BaseMiddleware: def __init__(self, get_response): self.get_response = get_response + + +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): + 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') + login(request, user) + setattr(request, '_dont_enforce_csrf_checks', True) + + response = self.get_response(request) + + if user: + response.set_cookie = lambda *args, **kwargs: None + + return response + + +class GAMiddleware(BaseMiddleware): + def __init__(self, get_response): + super().__init__(get_response) if settings.GA_ID: self.tracker = Tracker(settings.GA_ID, settings.GA_DOMAIN) diff --git a/tabby/settings.py b/tabby/settings.py index c76a9af..a3a936d 100644 --- a/tabby/settings.py +++ b/tabby/settings.py @@ -46,6 +46,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'tabby.middleware.TokenMiddleware', 'tabby.middleware.GAMiddleware', ] @@ -179,6 +180,8 @@ LOGIN_REDIRECT_URL = '/app' APP_DIST_PATH = Path(os.getenv('APP_DIST_PATH', BASE_DIR / 'app-dist')) NPM_REGISTRY = os.getenv('NPM_REGISTRY', 'https://registry.npmjs.org').rstrip('/') +GITHUB_ELIGIBLE_SPONSORSHIPS = None + for key in [ 'SOCIAL_AUTH_GITHUB_KEY', 'SOCIAL_AUTH_GITHUB_SECRET', @@ -191,7 +194,7 @@ for key in [ 'CONNECTION_GATEWAY_AUTH_CA', 'CONNECTION_GATEWAY_AUTH_CERTIFICATE', 'CONNECTION_GATEWAY_AUTH_KEY', - 'GITHUB_SPONSORS_USER', + 'GITHUB_ELIGIBLE_SPONSORSHIPS', 'GITHUB_SPONSORS_MIN_PAYMENT', 'GITHUB_TOKEN', 'ENABLE_LOGIN', @@ -221,3 +224,8 @@ for key in [ v = globals()[key] if v and not os.path.exists(v): raise ValueError(f'{v} does not exist') + +if GITHUB_ELIGIBLE_SPONSORSHIPS: + GITHUB_ELIGIBLE_SPONSORSHIPS = GITHUB_ELIGIBLE_SPONSORSHIPS.split(',') +else: + GITHUB_ELIGIBLE_SPONSORSHIPS = [] diff --git a/yarn.lock b/yarn.lock index 3698041..a59d8dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,15 @@ dependencies: tslib "^2.0.0" +"@angular/cdk@^12.1.3": + version "12.1.3" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.1.3.tgz#716d1a20040adf1b41335f9f7b8bf961758de5e6" + integrity sha512-uCWHk/PjddNJsdrmexasphWGbf4kYtYyhUCSd4HEBrIDjYz166MTVSr3FHgn/s8/tlVou7uTnaEZM+ILWoe2iQ== + dependencies: + tslib "^2.2.0" + optionalDependencies: + parse5 "^5.0.0" + "@angular/common@^11.0.0": version "11.2.14" resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.14.tgz#52887277b0ae0438e584f9ae97b417ee51a694b5" @@ -3046,6 +3055,11 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse5@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + parse5@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" @@ -4271,7 +4285,7 @@ trim-newlines@^1.0.0: dependencies: glob "^7.1.2" -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==