mirror of
https://github.com/Eugeny/tabby-web.git
synced 2025-06-11 23:19:54 +00:00
.
This commit is contained in:
parent
b3bb9fdd40
commit
0484b4c8d7
@ -131,7 +131,7 @@ class UserSerializer(ModelSerializer):
|
|||||||
read_only_fields = ('id', 'username')
|
read_only_fields = ('id', 'username')
|
||||||
|
|
||||||
def get_is_pro(self, obj):
|
def get_is_pro(self, obj):
|
||||||
return check_is_sponsor_cached(obj) or obj.force_pro
|
return obj.force_pro or not settings.GITHUB_ELIGIBLE_SPONSORSHIPS or check_is_sponsor_cached(obj)
|
||||||
|
|
||||||
def get_is_sponsor(self, obj):
|
def get_is_sponsor(self, obj):
|
||||||
return check_is_sponsor_cached(obj)
|
return check_is_sponsor_cached(obj)
|
||||||
|
@ -8,6 +8,8 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
class IndexView(APIView):
|
class IndexView(APIView):
|
||||||
def get(self, request, format=None):
|
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.FRONTEND_BUILD_DIR))
|
return static.serve(request, 'index.html', document_root=str(settings.FRONTEND_BUILD_DIR))
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,6 +100,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
|
|
||||||
AUTH_USER_MODEL = 'app.User'
|
AUTH_USER_MODEL = 'app.User'
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
@ -174,10 +180,12 @@ APP_DIST_STORAGE = os.getenv('APP_DIST_STORAGE', 'file://' + str(BASE_DIR / 'app
|
|||||||
NPM_REGISTRY = os.getenv('NPM_REGISTRY', 'https://registry.npmjs.org').rstrip('/')
|
NPM_REGISTRY = os.getenv('NPM_REGISTRY', 'https://registry.npmjs.org').rstrip('/')
|
||||||
|
|
||||||
FRONTEND_URL = None
|
FRONTEND_URL = None
|
||||||
|
BACKEND_URL = None
|
||||||
GITHUB_ELIGIBLE_SPONSORSHIPS = None
|
GITHUB_ELIGIBLE_SPONSORSHIPS = None
|
||||||
|
|
||||||
for key in [
|
for key in [
|
||||||
'FRONTEND_URL',
|
'FRONTEND_URL',
|
||||||
|
'BACKEND_URL',
|
||||||
'SOCIAL_AUTH_GITHUB_KEY',
|
'SOCIAL_AUTH_GITHUB_KEY',
|
||||||
'SOCIAL_AUTH_GITHUB_SECRET',
|
'SOCIAL_AUTH_GITHUB_SECRET',
|
||||||
'SOCIAL_AUTH_GITLAB_KEY',
|
'SOCIAL_AUTH_GITLAB_KEY',
|
||||||
@ -243,6 +251,8 @@ if FRONTEND_URL:
|
|||||||
]
|
]
|
||||||
frontend_domain = urlparse(FRONTEND_URL).hostname
|
frontend_domain = urlparse(FRONTEND_URL).hostname
|
||||||
CSRF_TRUSTED_ORIGINS = [frontend_domain]
|
CSRF_TRUSTED_ORIGINS = [frontend_domain]
|
||||||
|
if BACKEND_URL:
|
||||||
|
CSRF_TRUSTED_ORIGINS.append(urlparse(BACKEND_URL).hostname)
|
||||||
SESSION_COOKIE_DOMAIN = frontend_domain
|
SESSION_COOKIE_DOMAIN = frontend_domain
|
||||||
CSRF_COOKIE_DOMAIN = frontend_domain
|
CSRF_COOKIE_DOMAIN = frontend_domain
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { Resolve } from '@angular/router'
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
|
id: number
|
||||||
active_config: number
|
active_config: number
|
||||||
active_version: string
|
active_version: string
|
||||||
custom_connection_gateway: string|null
|
custom_connection_gateway: string|null
|
||||||
@ -11,6 +12,7 @@ export interface User {
|
|||||||
config_sync_token: string
|
config_sync_token: string
|
||||||
github_username: string
|
github_username: string
|
||||||
is_pro: boolean
|
is_pro: boolean
|
||||||
|
is_sponsor: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
|
@ -18,6 +18,8 @@ import { ConfigModalComponent } from './components/configModal.component'
|
|||||||
import { SettingsModalComponent } from './components/settingsModal.component'
|
import { SettingsModalComponent } from './components/settingsModal.component'
|
||||||
import { HomeComponent } from './components/home.component'
|
import { HomeComponent } from './components/home.component'
|
||||||
import { LoginComponent } from './components/login.component'
|
import { LoginComponent } from './components/login.component'
|
||||||
|
import { ConnectionListComponent } from './components/connectionList.component'
|
||||||
|
import { UpgradeModalComponent } from './components/upgradeModal.component'
|
||||||
import { InstanceInfoResolver } from './api'
|
import { InstanceInfoResolver } from './api'
|
||||||
|
|
||||||
import '@fortawesome/fontawesome-svg-core/styles.css'
|
import '@fortawesome/fontawesome-svg-core/styles.css'
|
||||||
@ -75,6 +77,8 @@ const ROUTES = [
|
|||||||
LoginComponent,
|
LoginComponent,
|
||||||
ConfigModalComponent,
|
ConfigModalComponent,
|
||||||
SettingsModalComponent,
|
SettingsModalComponent,
|
||||||
|
ConnectionListComponent,
|
||||||
|
UpgradeModalComponent,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
8
frontend/src/components/connectionList.component.pug
Normal file
8
frontend/src/components/connectionList.component.pug
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.list-group.list-group-light
|
||||||
|
.list-group-item.d-flex(*ngFor='let socket of appConnector.sockets')
|
||||||
|
fa-icon.text-success.me-2([icon]='_circleIcon', [fixedWidth]='true')
|
||||||
|
.me-auto
|
||||||
|
div {{socket.options.host}}:{{socket.options.port}}
|
||||||
|
.text-muted via {{socket.url}}
|
||||||
|
button.btn.btn-link((click)='closeSocket(socket)')
|
||||||
|
fa-icon([icon]='_closeIcon', [fixedWidth]='true')
|
20
frontend/src/components/connectionList.component.ts
Normal file
20
frontend/src/components/connectionList.component.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { AppConnectorService, SocketProxy } from '../services/appConnector.service'
|
||||||
|
import { faCircle, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'connection-list',
|
||||||
|
templateUrl: './connectionList.component.pug',
|
||||||
|
})
|
||||||
|
export class ConnectionListComponent {
|
||||||
|
_circleIcon = faCircle
|
||||||
|
_closeIcon = faTimes
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public appConnector: AppConnectorService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
closeSocket (socket: SocketProxy) {
|
||||||
|
socket.close(new Error('Connection closed by user'))
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
main(*ngIf='ready && loggedIn')
|
.login-view(*ngIf='ready')
|
||||||
.login-view(*ngIf='ready && !loggedIn')
|
|
||||||
.buttons
|
.buttons
|
||||||
a.btn(
|
a.btn(
|
||||||
*ngFor='let provider of providers',
|
*ngFor='let provider of providers',
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
.modal-body
|
.modal-body
|
||||||
.mb-3
|
.mb-3
|
||||||
h5 GitHub account
|
h5 GitHub account
|
||||||
a.btn.btn-info(href='/api/1/auth/social/login/github', *ngIf='!user.github_username')
|
a.btn.btn-info(href='{{commonService.backendURL}}/api/1/auth/social/login/github', *ngIf='!user.github_username')
|
||||||
fa-icon([icon]='_githubIcon', [fixedWidth]='true')
|
fa-icon([icon]='_githubIcon', [fixedWidth]='true')
|
||||||
span Connect a GitHub account
|
span Connect a GitHub account
|
||||||
.alert.alert-success.d-flex(*ngIf='user.github_username')
|
.alert.alert-success.d-flex(*ngIf='user.github_username')
|
||||||
@ -61,13 +61,12 @@
|
|||||||
)
|
)
|
||||||
label Gateway authentication token
|
label Gateway authentication token
|
||||||
|
|
||||||
div(*ngIf='appConnector.sockets.length')
|
.mb-3.mt-4(*ngIf='appConnector.sockets.length')
|
||||||
h5 Active connections
|
h5 Active connections
|
||||||
.list-group.list-group-flush
|
connection-list
|
||||||
.list-group-item(*ngFor='let socket of appConnector.sockets')
|
|
||||||
div {{socket.options.host}}:{{socket.options.port}}
|
|
||||||
.text-muted via {{socket.url}}
|
|
||||||
|
|
||||||
.modal-footer
|
.modal-footer
|
||||||
|
.text-muted Account ID: {{user.id}}
|
||||||
|
.ms-auto
|
||||||
button.btn.btn-primary((click)='apply()') Apply
|
button.btn.btn-primary((click)='apply()') Apply
|
||||||
button.btn.btn-secondary((click)='cancel()') Cancel
|
button.btn.btn-secondary((click)='cancel()') Cancel
|
||||||
|
@ -4,6 +4,7 @@ import { LoginService } from '../services/login.service'
|
|||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { User } from '../api'
|
import { User } from '../api'
|
||||||
import { AppConnectorService } from '../services/appConnector.service'
|
import { AppConnectorService } from '../services/appConnector.service'
|
||||||
|
import { CommonService } from '../services/common.service'
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export class SettingsModalComponent {
|
|||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public appConnector: AppConnectorService,
|
public appConnector: AppConnectorService,
|
||||||
|
public commonService: CommonService,
|
||||||
private modalInstance: NgbActiveModal,
|
private modalInstance: NgbActiveModal,
|
||||||
private loginService: LoginService,
|
private loginService: LoginService,
|
||||||
) {
|
) {
|
||||||
|
28
frontend/src/components/upgradeModal.component.pug
Normal file
28
frontend/src/components/upgradeModal.component.pug
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.modal-header
|
||||||
|
h1.modal-title Hey!
|
||||||
|
|
||||||
|
.modal-body
|
||||||
|
h4 It looks like you're enjoying Tabby a lot!
|
||||||
|
|
||||||
|
p Tabby Web has a limit of {{appConnector.connectionLimit}} simultaneous connections due to the fact that I have to pay for hosting and traffic out of my own pocket.
|
||||||
|
|
||||||
|
p #[strong You can have unlimited parallel connections] if you support Tabby on GitHub with #[code $3]/month or more. It's cancellable anytime, there are no hidden costs and it helps me pay my bills.
|
||||||
|
|
||||||
|
a.btn.btn-primary.btn-lg.d-block.mb-3(href='https://github.com/sponsors/Eugeny', target='_blank')
|
||||||
|
fa-icon.me-2([icon]='_loveIcon')
|
||||||
|
span Support Tabby on GitHub
|
||||||
|
|
||||||
|
button.btn.btn-warning.d-block.w-100((click)='skipOnce()', *ngIf='canSkip')
|
||||||
|
fa-icon.me-2([icon]='_giftIcon')
|
||||||
|
span Skip - just this one time
|
||||||
|
|
||||||
|
p.mt-3 If you work in education, have already supported me on Ko-fi before, or your country isn't supported on GitHub Sponsors, just #[a(href='mailto:e@ajenti.org?subject=Help with Tabby Pro') let me know] and I'll hook you up.
|
||||||
|
|
||||||
|
.mb-3(*ngIf='!loginService.user.github_username')
|
||||||
|
a.btn.btn-info(href='{{commonService.backendURL}}/api/1/auth/social/login/github')
|
||||||
|
fa-icon([icon]='_githubIcon', [fixedWidth]='true')
|
||||||
|
span Connect your GitHub account to link your sponsorship
|
||||||
|
|
||||||
|
.mt-4
|
||||||
|
p You can also kill any active connection from the list below to free up a slot.
|
||||||
|
connection-list
|
36
frontend/src/components/upgradeModal.component.ts
Normal file
36
frontend/src/components/upgradeModal.component.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||||
|
import { faGift, faHeart } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
import { LoginService } from '../services/login.service'
|
||||||
|
import { AppConnectorService } from '../services/appConnector.service'
|
||||||
|
import { CommonService } from '../services/common.service'
|
||||||
|
import { User } from '../api'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'upgrade-modal',
|
||||||
|
templateUrl: './upgradeModal.component.pug',
|
||||||
|
})
|
||||||
|
export class UpgradeModalComponent {
|
||||||
|
user: User
|
||||||
|
_githubIcon = faGithub
|
||||||
|
_loveIcon = faHeart
|
||||||
|
_giftIcon = faGift
|
||||||
|
canSkip = false
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public appConnector: AppConnectorService,
|
||||||
|
public commonService: CommonService,
|
||||||
|
public loginService: LoginService,
|
||||||
|
private modalInstance: NgbActiveModal,
|
||||||
|
) {
|
||||||
|
this.canSkip = !window.localStorage['upgrade-modal-skipped']
|
||||||
|
}
|
||||||
|
|
||||||
|
skipOnce () {
|
||||||
|
window.localStorage['upgrade-modal-skipped'] = true
|
||||||
|
window.sessionStorage['upgrade-skip-active'] = true
|
||||||
|
this.modalInstance.close(true)
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,9 @@ import { Buffer } from 'buffer'
|
|||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { debounceTime } from 'rxjs/operators'
|
import { debounceTime } from 'rxjs/operators'
|
||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable, Injector } from '@angular/core'
|
import { Injectable, Injector, NgZone } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { UpgradeModalComponent } from '../components/upgradeModal.component'
|
||||||
import { Config, Gateway, Version } from '../api'
|
import { Config, Gateway, Version } from '../api'
|
||||||
import { LoginService } from './login.service'
|
import { LoginService } from './login.service'
|
||||||
import { CommonService } from './common.service'
|
import { CommonService } from './common.service'
|
||||||
@ -24,16 +26,31 @@ export class SocketProxy {
|
|||||||
|
|
||||||
private appConnector: AppConnectorService
|
private appConnector: AppConnectorService
|
||||||
private loginService: LoginService
|
private loginService: LoginService
|
||||||
|
private ngbModal: NgbModal
|
||||||
|
private zone: NgZone
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
injector: Injector,
|
injector: Injector,
|
||||||
) {
|
) {
|
||||||
this.appConnector = injector.get(AppConnectorService)
|
this.appConnector = injector.get(AppConnectorService)
|
||||||
this.loginService = injector.get(LoginService)
|
this.loginService = injector.get(LoginService)
|
||||||
|
this.ngbModal = injector.get(NgbModal)
|
||||||
|
this.zone = injector.get(NgZone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async connect (options: any): Promise<void> {
|
async connect (options: any): Promise<void> {
|
||||||
|
if (!this.loginService.user.is_pro && this.appConnector.sockets.length > this.appConnector.connectionLimit && !window.sessionStorage['upgrade-skip-active']) {
|
||||||
|
let skipped = false
|
||||||
|
try {
|
||||||
|
skipped = await this.zone.run(() => this.ngbModal.open(UpgradeModalComponent)).result
|
||||||
|
} catch { }
|
||||||
|
if (!skipped) {
|
||||||
|
this.close(new Error('Connection limit reached'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.options = options
|
this.options = options
|
||||||
this.url = this.loginService.user.custom_connection_gateway
|
this.url = this.loginService.user.custom_connection_gateway
|
||||||
this.authToken = this.loginService.user.custom_connection_gateway_token
|
this.authToken = this.loginService.user.custom_connection_gateway_token
|
||||||
@ -127,12 +144,14 @@ export class AppConnectorService {
|
|||||||
private configUpdate = new Subject<string>()
|
private configUpdate = new Subject<string>()
|
||||||
private config: Config
|
private config: Config
|
||||||
private version: Version
|
private version: Version
|
||||||
|
connectionLimit = 3
|
||||||
sockets: SocketProxy[] = []
|
sockets: SocketProxy[] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private injector: Injector,
|
private injector: Injector,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private commonService: CommonService,
|
private commonService: CommonService,
|
||||||
|
private zone: NgZone,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => {
|
this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => {
|
||||||
@ -180,12 +199,14 @@ export class AppConnectorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createSocket () {
|
createSocket () {
|
||||||
const socket = new SocketProxy(this.injector)
|
return this.zone.run(() => {
|
||||||
this.sockets.push(socket)
|
const socket = new SocketProxy(this.injector)
|
||||||
socket.close$.subscribe(() => {
|
this.sockets.push(socket)
|
||||||
this.sockets = this.sockets.filter(x => x !== socket)
|
socket.close$.subscribe(() => {
|
||||||
|
this.sockets = this.sockets.filter(x => x !== socket)
|
||||||
|
})
|
||||||
|
return socket
|
||||||
})
|
})
|
||||||
return socket
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async chooseConnectionGateway (): Promise<Gateway> {
|
async chooseConnectionGateway (): Promise<Gateway> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user