diff --git a/manage.py b/manage.py index b3c9744..40a3ac4 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """Django's command-line utility for administrative tasks.""" import os import sys diff --git a/poetry.lock b/poetry.lock index 259a2e9..01e05f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -136,6 +136,14 @@ twisted = {version = ">=18.7", extras = ["tls"]} [package.extras] tests = ["hypothesis (==4.23)", "pytest (>=3.10,<4.0)", "pytest-asyncio (>=0.8,<1.0)"] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "django" version = "3.2.3" @@ -256,6 +264,19 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "oauthlib" +version = "3.1.1" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +rsa = ["cryptography (>=3.0.0,<4)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "pyasn1" version = "0.4.8" @@ -299,6 +320,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyjwt" +version = "2.1.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +crypto = ["cryptography (>=3.3.1,<4.0.0)"] +dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] + [[package]] name = "pyopenssl" version = "20.0.1" @@ -315,6 +350,21 @@ six = ">=1.5.2" docs = ["sphinx", "sphinx-rtd-theme"] test = ["flaky", "pretend", "pytest (>=3.0.1)"] +[[package]] +name = "python3-openid" +version = "3.2.0" +description = "OpenID support for modern servers and consumers." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +defusedxml = "*" + +[package.extras] +mysql = ["mysql-connector-python"] +postgresql = ["psycopg2"] + [[package]] name = "pytz" version = "2021.1" @@ -323,6 +373,33 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.15.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = "*" + +[package.extras] +security = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.0" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "service-identity" version = "21.1.0" @@ -352,6 +429,42 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "social-auth-app-django" +version = "4.0.0" +description = "Python Social Authentication, Django integration." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" +social-auth-core = ">=3.3.0" + +[[package]] +name = "social-auth-core" +version = "4.1.0" +description = "Python social authentication made simple." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = ">=1.4" +defusedxml = ">=0.5.0rc1" +oauthlib = ">=1.0.3" +PyJWT = ">=2.0.0" +python3-openid = ">=3.0.10" +requests = ">=2.9.1" +requests-oauthlib = ">=0.6.1" + +[package.extras] +all = ["python-jose (>=3.0.0)", "python3-saml (>=1.2.1)", "cryptography (>=2.1.1)"] +allpy3 = ["python-jose (>=3.0.0)", "python3-saml (>=1.2.1)", "cryptography (>=2.1.1)"] +azuread = ["cryptography (>=2.1.1)"] +openidconnect = ["python-jose (>=3.0.0)"] +saml = ["python3-saml (>=1.2.1)"] + [[package]] name = "sqlparse" version = "0.4.1" @@ -464,7 +577,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "3922460ced69b5e33ce174cf478542b18bf7bc793d32e262fe46a4b7d9a2a047" +content-hash = "ddf1bd292c78d2b6919ec1a9eb9b76b48ff4d97e7a20a3a46783dcdbf5ec1163" [metadata.files] asgiref = [ @@ -560,6 +673,10 @@ daphne = [ {file = "daphne-3.0.2-py3-none-any.whl", hash = "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"}, {file = "daphne-3.0.2.tar.gz", hash = "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f"}, ] +defusedxml = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] django = [ {file = "Django-3.2.3-py3-none-any.whl", hash = "sha256:7e0a1393d18c16b503663752a8b6790880c5084412618990ce8a81cc908b4962"}, {file = "Django-3.2.3.tar.gz", hash = "sha256:13ac78dbfd189532cad8f383a27e58e18b3d33f80009ceb476d7fcbfc5dcebd8"}, @@ -599,6 +716,10 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +oauthlib = [ + {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"}, + {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, +] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, @@ -641,14 +762,31 @@ pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] +pyjwt = [ + {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"}, + {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"}, +] pyopenssl = [ {file = "pyOpenSSL-20.0.1-py2.py3-none-any.whl", hash = "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"}, {file = "pyOpenSSL-20.0.1.tar.gz", hash = "sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51"}, ] +python3-openid = [ + {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, + {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, +] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +requests = [ + {file = "requests-2.15.1-py2.py3-none-any.whl", hash = "sha256:ff753b2196cd18b1bbeddc9dcd5c864056599f7a7d9a4fb5677e723efa2b7fb9"}, + {file = "requests-2.15.1.tar.gz", hash = "sha256:e5659b9315a0610505e050bb7190bf6fa2ccee1ac295f2b760ef9d8a03ebbb2e"}, +] +requests-oauthlib = [ + {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, + {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, + {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, +] service-identity = [ {file = "service-identity-21.1.0.tar.gz", hash = "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34"}, {file = "service_identity-21.1.0-py2.py3-none-any.whl", hash = "sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db"}, @@ -657,6 +795,15 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +social-auth-app-django = [ + {file = "social-auth-app-django-4.0.0.tar.gz", hash = "sha256:2c69e57df0b30c9c1823519c5f1992cbe4f3f98fdc7d95c840e091a752708840"}, + {file = "social_auth_app_django-4.0.0-py2-none-any.whl", hash = "sha256:df5212370bd250108987c4748419a1a1d0cec750878856c2644c36aaa0fd3e58"}, + {file = "social_auth_app_django-4.0.0-py3-none-any.whl", hash = "sha256:567ad0e028311541d7dfed51d3bf2c60440a6fd236d5d4d06c5a618b3d6c57c5"}, +] +social-auth-core = [ + {file = "social-auth-core-4.1.0.tar.gz", hash = "sha256:5ab43b3b15dce5f059db69cc3082c216574739f0edbc98629c8c6e8769c67eb4"}, + {file = "social_auth_core-4.1.0-py3-none-any.whl", hash = "sha256:983b53167ac56e7ba4909db555602a6e7a98c97ca47183bb222eb85ba627bf2b"}, +] sqlparse = [ {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, diff --git a/pyproject.toml b/pyproject.toml index 54ff65c..3eae99c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ django-rest-framework = "^0.1.0" channels = "^3.0.3" uvloop = "^0.15.2" djangorestframework-dataclasses = "^0.9" +social-auth-app-django = "^4.0.0" [tool.poetry.dev-dependencies] flake8 = "^3.9.2" diff --git a/src/app.module.ts b/src/app.module.ts index 5ddfe4e..2abb4c7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http' import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' import { AppComponent } from './components/app.component' +import { MainComponent } from './components/main.component'; @NgModule({ imports: [ @@ -17,6 +18,7 @@ import { AppComponent } from './components/app.component' ], declarations: [ AppComponent, + MainComponent, ], bootstrap: [AppComponent] }) diff --git a/src/components/app.component.pug b/src/components/app.component.pug index 350bbbd..b36a2dc 100644 --- a/src/components/app.component.pug +++ b/src/components/app.component.pug @@ -1,55 +1,2 @@ -.sidebar - img.logo(src='{{_logo}}') - - div(ngbDropdown, placement='bottom-right') - button.btn.btn-secondary(ngbDropdownToggle) - fa-icon([icon]='_cogIcon', [fixedWidth]='true') - - .config-menu(ngbDropdownMenu) - .header(*ngIf='getActiveConfig()') - .dropdown-header Active config - .title {{getActiveConfig().modified_at}} - div(*ngIf='activeVersion') - div App version: - div(ngbDropdown) - button.btn.btn-secondary(ngbDropdownToggle) {{activeVersion.version}} - div(ngbDropdownMenu) - a( - *ngFor='let version of versions', - ngbDropdownItem, - [class.active]='version == activeVersion', - (click)='selectVersion(version)' - ) {{version.version}} - - .btn-toolbar - button.btn.btn-light.w-50((click)='duplicateConfig()') - fa-icon([icon]='_copyIcon', [fixedWidth]='true') - span Duplicate - - button.btn.btn-light.w-50((click)='deleteConfig()') - fa-icon([icon]='_deleteIcon', [fixedWidth]='true') - span Delete - - div(*ngIf='configs.length > 1') - .dropdown-header All configs - - ng-container(*ngFor='let config of configs') - a( - *ngIf='config !== getActiveConfig()', - ngbDropdownItem, - (click)='selectConfig(config)' - ) Config modified at {{config.modified_at}} - - button.btn.btn-light.w-100((click)='createNewConfig()') - fa-icon([icon]='_addIcon', [fixedWidth]='true') - span New config - - div(ngbDropdown, placement='bottom-right') - button.btn.btn-secondary(ngbDropdownToggle) - fa-icon([icon]='_userIcon', [fixedWidth]='true') - - div(ngbDropdownMenu) - a(ngbDropdownItem, (click)='logout()') Logout - -.terminal([hidden]='!activeVersion') - iframe(#iframe) +main(*ngIf='ready && user') +login(*ngIf='ready && !user') diff --git a/src/components/app.component.scss b/src/components/app.component.scss index 27a414d..e69de29 100644 --- a/src/components/app.component.scss +++ b/src/components/app.component.scss @@ -1,41 +0,0 @@ -:host { - position: absolute; - left: 0; - top: 0; - width: 100vw; - height: 100vh; - overflow: hidden; - display: flex; -} - -.sidebar { - width: 64px; - flex: none; - - .logo { - width: 64px; - height: 64px; - } -} - -.terminal { - flex: 1 1 0; - overflow: hidden; - position: relative; -} - -iframe { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - border: none; -} - - -.config-menu { - .header { - border-bottom: 1px solid black; - } -} diff --git a/src/components/app.component.ts b/src/components/app.component.ts index e547de0..34f6da9 100644 --- a/src/components/app.component.ts +++ b/src/components/app.component.ts @@ -1,9 +1,5 @@ -import * as semverGT from 'semver/functions/gt' -import { Component, ElementRef, ViewChild } from '@angular/core' +import { Component } from '@angular/core' import { HttpClient } from '@angular/common/http' -import { AppConnectorService } from '../services/appConnector.service' - -import { faCog, faUser, faCopy, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons' @Component({ selector: 'app', @@ -11,95 +7,18 @@ import { faCog, faUser, faCopy, faTrash, faPlus } from '@fortawesome/free-solid- styleUrls: ['./app.component.scss'], }) export class AppComponent { - _logo = require('../assets/logo.svg') - _cogIcon = faCog - _userIcon = faUser - _copyIcon = faCopy - _addIcon = faPlus - _deleteIcon = faTrash - - configs: any[] = [] - versions: any[] = [] - activeVersion?: any - @ViewChild('iframe') iframe: ElementRef + user: any + ready = false constructor ( - private appConnector: AppConnectorService, private http: HttpClient, - ) { - window.addEventListener('message', event => { - if (event.data === 'request-connector') { - this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector - this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') - } - }) - } + ) { } - async ngAfterViewInit () { - this.configs = await this.http.get('/api/1/configs').toPromise() - this.versions = await this.http.get('/api/1/versions').toPromise() - - this.versions.sort((a, b) => semverGT(a, b)) - - if (!this.configs.length) { - await this.createNewConfig() + async ngOnInit () { + const user = await this.http.get('/api/1/user').toPromise() + if (user.id) { + this.user = user } - - this.selectConfig(this.configs[0]) - } - - async createNewConfig () { - this.configs.push(await this.http.post('/api/1/configs', { - content: '{}', - last_used_with_version: this.versions[0].version, - }).toPromise()) - } - - async duplicateConfig () { - const copy = {...this.appConnector.config, pk: undefined} - this.configs.push(await this.http.post('/api/1/configs', copy).toPromise()) - } - - unloadApp () { - delete this.activeVersion - this.iframe.nativeElement.src = 'about:blank' - } - - loadApp (version) { - this.iframe.nativeElement.src = `/terminal?${version.version}` - this.activeVersion = version - } - - getActiveConfig () { - return this.appConnector.config - } - - selectVersion (version: any) { - // TODO check config incompatibility - this.unloadApp() - setTimeout(() => { - this.loadApp(version) - }) - } - - async selectConfig (config: any) { - let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version) - if (!matchingVersion) { - // TODO ask to upgrade - matchingVersion = this.versions[0] - } - - this.appConnector.config = config - - const result = await this.http.patch(`/api/1/configs/${config.id}`, { - last_used_with_version: matchingVersion.version, - }).toPromise() - Object.assign(config, result) - - this.selectVersion(matchingVersion) - } - - async logout () { - await this.http.post('/api/1/auth/logout', null).toPromise() + this.ready = true } } diff --git a/src/components/main.component.pug b/src/components/main.component.pug new file mode 100644 index 0000000..99d90e9 --- /dev/null +++ b/src/components/main.component.pug @@ -0,0 +1,57 @@ +.sidebar + img.logo(src='{{_logo}}') + + div(ngbDropdown, placement='bottom-right') + button.btn(ngbDropdownToggle) + fa-icon([icon]='_cogIcon', [fixedWidth]='true') + + .config-menu(ngbDropdownMenu) + .header(*ngIf='getActiveConfig()') + .d-flex.align-items-center.py-2.px-4 + .me-auto + label Active config + .title {{getActiveConfig().modified_at}} + + button.btn.btn-semi.me-2((click)='duplicateConfig()') + fa-icon([icon]='_copyIcon', [fixedWidth]='true') + + button.btn.btn-semi((click)='deleteConfig()') + fa-icon([icon]='_deleteIcon', [fixedWidth]='true') + + .d-flex.align-items-center.py-2.px-4(*ngIf='activeVersion') + .me-auto App version: + div(ngbDropdown) + button.btn.btn-semi(ngbDropdownToggle) {{activeVersion.version}} + div(ngbDropdownMenu) + a( + *ngFor='let version of versions', + ngbDropdownItem, + [class.active]='version == activeVersion', + (click)='selectVersion(version)' + ) {{version.version}} + + div(*ngIf='configs.length > 1') + .dropdown-header All configs + + ng-container(*ngFor='let config of configs') + a( + *ngIf='config !== getActiveConfig()', + ngbDropdownItem, + (click)='selectConfig(config)', + href='#' + ) Config modified at {{config.modified_at}} + + .p-3 + button.btn.btn-semi.w-100((click)='createNewConfig()') + fa-icon([icon]='_addIcon', [fixedWidth]='true') + span New config + + div(ngbDropdown, placement='bottom-right') + button.btn(ngbDropdownToggle) + fa-icon([icon]='_userIcon', [fixedWidth]='true') + + div(ngbDropdownMenu) + a(ngbDropdownItem, (click)='logout()', href='#') Logout + +.terminal([hidden]='!activeVersion') + iframe(#iframe) diff --git a/src/components/main.component.scss b/src/components/main.component.scss new file mode 100644 index 0000000..7a9b6f0 --- /dev/null +++ b/src/components/main.component.scss @@ -0,0 +1,61 @@ +@import "../theme/vars"; + +:host { + position: absolute; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + overflow: hidden; + display: flex; +} + +.sidebar { + width: 64px; + flex: none; + display: flex; + flex-direction: column; + align-items: stretch; + + .logo { + width: 32px; + height: 32px; + align-self: center; + margin-top: 15px; + margin-bottom: 20px; + } + + >button, >[ngbdropdown] > button { + width: 64px; + height: 64px; + background: transparent; + box-shadow: none; + + &::after { + display: none; + } + } +} + +.terminal { + flex: 1 1 0; + overflow: hidden; + position: relative; +} + +iframe { + background: $body-bg; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + border: none; +} + + +.config-menu { + .header { + border-bottom: 1px solid black; + } +} diff --git a/src/components/main.component.ts b/src/components/main.component.ts new file mode 100644 index 0000000..bb26604 --- /dev/null +++ b/src/components/main.component.ts @@ -0,0 +1,106 @@ +import * as semverGT from 'semver/functions/gt' +import { Component, ElementRef, ViewChild } from '@angular/core' +import { HttpClient } from '@angular/common/http' +import { AppConnectorService } from '../services/appConnector.service' + +import { faCog, faUser, faCopy, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons' + +@Component({ + selector: 'main', + templateUrl: './main.component.pug', + styleUrls: ['./main.component.scss'], +}) +export class MainComponent { + _logo = require('../assets/logo.svg') + _cogIcon = faCog + _userIcon = faUser + _copyIcon = faCopy + _addIcon = faPlus + _deleteIcon = faTrash + + configs: any[] = [] + versions: any[] = [] + activeVersion?: any + @ViewChild('iframe') iframe: ElementRef + + constructor ( + private appConnector: AppConnectorService, + private http: HttpClient, + ) { + window.addEventListener('message', event => { + if (event.data === 'request-connector') { + this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector + this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') + } + }) + } + + async ngAfterViewInit () { + this.configs = await this.http.get('/api/1/configs').toPromise() + this.versions = await this.http.get('/api/1/versions').toPromise() + + this.versions.sort((a, b) => semverGT(a, b)) + + if (!this.configs.length) { + await this.createNewConfig() + } + + this.selectConfig(this.configs[0]) + } + + async createNewConfig () { + this.configs.push(await this.http.post('/api/1/configs', { + content: '{}', + last_used_with_version: this.versions[0].version, + }).toPromise()) + } + + async duplicateConfig () { + const copy = {...this.appConnector.config, pk: undefined} + this.configs.push(await this.http.post('/api/1/configs', copy).toPromise()) + } + + unloadApp () { + delete this.activeVersion + this.iframe.nativeElement.src = 'about:blank' + } + + loadApp (version) { + this.iframe.nativeElement.src = `/terminal?${version.version}` + this.activeVersion = version + } + + getActiveConfig () { + return this.appConnector.config + } + + selectVersion (version: any) { + // TODO check config incompatibility + this.unloadApp() + setTimeout(() => { + this.loadApp(version) + }) + } + + async selectConfig (config: any) { + let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version) + if (!matchingVersion) { + // TODO ask to upgrade + matchingVersion = this.versions[0] + } + + this.appConnector.config = config + + const result = await this.http.patch(`/api/1/configs/${config.id}`, { + last_used_with_version: matchingVersion.version, + }).toPromise() + Object.assign(config, result) + + this.selectVersion(matchingVersion) + } + + async logout () { + await this.http.post('/api/1/auth/logout', null).toPromise() + location.href = '/' + } +} diff --git a/src/styles.scss b/src/styles.scss index 95a3302..5780c01 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -6,4 +6,4 @@ body { overscroll-behavior: none; } -@import "node_modules/bootstrap/scss/bootstrap"; +@import "./theme/index.scss" diff --git a/src/theme/index.scss b/src/theme/index.scss new file mode 100644 index 0000000..94c4707 --- /dev/null +++ b/src/theme/index.scss @@ -0,0 +1,64 @@ +@import "vars"; + +@import "~bootstrap/scss/functions"; +@import "~bootstrap/scss/variables"; +@import "~bootstrap/scss/mixins"; +@import "~bootstrap/scss/utilities"; + +@import "~bootstrap/scss/root"; +@import "~bootstrap/scss/reboot"; +@import "~bootstrap/scss/type"; +// @import "~bootstrap/scss/images"; +@import "~bootstrap/scss/containers"; +@import "~bootstrap/scss/grid"; +// @import "~bootstrap/scss/tables"; +@import "~bootstrap/scss/forms"; +@import "~bootstrap/scss/buttons"; +@import "~bootstrap/scss/transitions"; +@import "~bootstrap/scss/dropdown"; +@import "~bootstrap/scss/button-group"; +// @import "~bootstrap/scss/nav"; +// @import "~bootstrap/scss/navbar"; +// @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/progress"; +@import "~bootstrap/scss/list-group"; +// @import "~bootstrap/scss/close"; +// @import "~bootstrap/scss/toasts"; +// @import "~bootstrap/scss/modal"; +// @import "~bootstrap/scss/tooltip"; +// @import "~bootstrap/scss/popover"; +// @import "~bootstrap/scss/carousel"; +// @import "~bootstrap/scss/spinners"; +// @import "~bootstrap/scss/offcanvas"; + +// Helpers +@import "~bootstrap/scss/helpers"; +@import "~bootstrap/scss/utilities/api"; + +::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: $gray-900; +} + +::-webkit-scrollbar +{ + height: 6px; + width: 6px; + background-color: #F5F5F5; +} + +::-webkit-scrollbar-thumb +{ + background-color: $gray-700; +} + + +.dropdown-menu { + box-shadow: $dropdown-box-shadow; +} diff --git a/src/theme/vars.scss b/src/theme/vars.scss new file mode 100644 index 0000000..013a08a --- /dev/null +++ b/src/theme/vars.scss @@ -0,0 +1,187 @@ +$white: #fff; +$gray-100: #f8f9fa; +$gray-200: #e9ecef; +$gray-300: #dee2e6; +$gray-400: #ced4da; +$gray-500: #adb5bd; +$gray-600: #6c757d; +$gray-700: #495057; +$gray-800: #343a40; +$gray-900: #212529; +$black: #000; + + +$red: #d9534f !default; +$orange: #f0ad4e !default; +$yellow: #ffd500 !default; +$green: #5cb85c !default; +$blue: #0275d8 !default; +$teal: #5bc0de !default; +$pink: #ff5b77 !default; +$purple: #613d7c !default; +$semi: rgba(0,0,0, .5); + + +@import "~bootstrap/scss/functions"; + +$table-bg: rgba(255,255,255,.05); +$table-bg-hover: rgba(255,255,255,.1); +$table-border-color: rgba(255,255,255,.1); + +$theme-colors: ( + primary: $blue, + secondary: #38434e, + success: $green, + info: $blue, + warning: $orange, + danger: $red, + light: $gray-300, + dark: #0e151d, + rare: $purple, + semi: $semi +); + +$body-color: #ccc; +$body-bg: #0c131b; + +$font-family-sans-serif: "Source Sans Pro"; +$font-family-monospace: "Source Code Pro"; +$font-size-base: 14rem / 16; +$font-size-lg: 1.28rem; +$font-size-sm: .85rem; + +$line-height-base: 1.6; + +$border-radius: .4rem; +$border-radius-lg: .6rem; +$border-radius-sm: .2rem; + +$box-shadow: 0 .5rem 1rem rgba($black, .5) !default; + +// ----- + +$headings-color: #ced9e2; +$headings-font-weight: lighter; + +$input-btn-padding-y: .3rem; +$input-btn-padding-x: .9rem; +$input-btn-line-height: 1.6; +$input-btn-line-height-sm: 1.8; +$input-btn-line-height-lg: 1.8; +$btn-focus-width: 1px; + +$h4-font-size: 18px; + +$link-color: $gray-400; +$link-hover-color: $white; +$link-hover-decoration: none; + +$component-active-color: $white; +$component-active-bg: #2f3a42; + +$list-group-bg: $table-bg; +$list-group-border-color: $table-border-color; + +$list-group-item-padding-y: 0.8rem; +$list-group-item-padding-x: 1rem; + +$list-group-hover-bg: $table-bg-hover; +$list-group-active-bg: rgba(255,255,255,.2); +$list-group-active-color: $component-active-color; +$list-group-active-border-color: translate; + +$list-group-action-color: $body-color; +$list-group-action-hover-color: white; + +$list-group-action-active-color: $component-active-color; +$list-group-action-active-bg: $list-group-active-bg; + +$alert-padding-y: 0.9rem; +$alert-padding-x: 1.25rem; + +$transition-base: all .15s ease-in-out; +$transition-fade: opacity .1s linear; +$transition-collapse: height .35s ease; +$btn-transition: all .15s ease-in-out; + +$popover-bg: $body-bg; +$popover-body-color: $body-color; +$popover-header-bg: $table-bg-hover; +$popover-header-color: $headings-color; +$popover-arrow-color: $popover-bg; +$popover-max-width: 360px; + +$btn-border-width: 2px; + +$input-bg: #181e23; +$input-disabled-bg: #2e3235; + +$input-color: #ddd; +$input-border-color: $input-bg; +$input-border-width: 2px; + +$input-focus-bg: $input-bg; +$input-focus-border-color: rgba(171, 171, 171, 0.61); +$input-focus-color: $input-color; + +$input-group-addon-color: $input-color; +$input-group-addon-bg: $input-bg; +$input-group-addon-border-color: transparent; +$input-group-btn-border-color: $input-bg; + +$nav-tabs-border-radius: 0; +$nav-tabs-border-color: transparent; +$nav-tabs-border-width: 2px; +$nav-tabs-link-hover-border-color: transparent; +$nav-tabs-link-active-color: #eee; +$nav-tabs-link-active-bg: transparent; +$nav-tabs-link-active-border-color: #eee; + +$navbar-padding-y: 0; +$navbar-padding-x: 0; + +$dropdown-bg: $body-bg; +$dropdown-color: $body-color; +$dropdown-border-width: 1px; +$dropdown-header-color: $gray-500; + +$dropdown-link-color: $body-color; +$dropdown-link-hover-color: #eee; +$dropdown-link-hover-bg: rgba(255,255,255,.04); +$dropdown-link-active-color: white; +$dropdown-link-active-bg: rgba(0, 0, 0, .2); +$dropdown-item-padding-y: 0.5rem; +$dropdown-item-padding-x: 1.5rem; + + +$code-color: $orange; +$code-bg: rgba(0, 0, 0, .25); +$code-padding-y: 3px; +$code-padding-x: 5px; +$pre-bg: $dropdown-bg; +$pre-color: $dropdown-link-color; + +$badge-font-size: 0.75rem; +$badge-font-weight: bold; +$badge-padding-y: 4px; +$badge-padding-x: 6px; + + +$custom-control-indicator-size: 1.2rem; +$custom-control-indicator-bg: $body-bg; +$custom-control-indicator-border-color: lighten($body-bg, 25%); +$custom-control-indicator-checked-bg: theme-color("primary"); +$custom-control-indicator-checked-color: $body-bg; +$custom-control-indicator-checked-border-color: transparent; +$custom-control-indicator-active-bg: rgba(255, 255, 0, 0.5); + + +$modal-content-bg: $body-bg; +$modal-content-border-color: $body-bg; +$modal-header-border-width: 0; +$modal-footer-border-color: #222; +$modal-footer-border-width: 1px; +$modal-content-border-width: 0; + +$progress-bg: $table-bg; +$progress-height: 3px; diff --git a/terminus/app/api.py b/terminus/app/api.py index dd18630..fe214a5 100644 --- a/terminus/app/api.py +++ b/terminus/app/api.py @@ -2,15 +2,16 @@ import os from dataclasses import dataclass from django.conf import settings from django.contrib.auth import logout +from rest_framework import fields from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet, ModelViewSet -from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import ModelSerializer, Field from rest_framework_dataclasses.serializers import DataclassSerializer -from .models import Config +from .models import Config, User @dataclass @@ -61,6 +62,26 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet): ).data) +class UserSerializer(ModelSerializer): + id = fields.IntegerField() + + class Meta: + model = User + fields = ('id', 'username', 'active_config') + read_only_fields = ('id', 'username') + + +class UserViewSet(RetrieveModelMixin, GenericViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + + def get_object(self): + if self.request.user.is_authenticated: + return self.request.user + return None + + class LogoutView(APIView): def post(self, request, format=None): logout(request) + return Response(None) diff --git a/terminus/app/urls.py b/terminus/app/urls.py index bc00c1c..4b12470 100644 --- a/terminus/app/urls.py +++ b/terminus/app/urls.py @@ -12,6 +12,7 @@ router.register('api/1/versions', api.AppVersionViewSet, basename='app-versions' urlpatterns = [ path('api/1/auth/logout', api.LogoutView.as_view()), + path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve'})), path('', views.IndexView.as_view()), path('terminal', views.TerminalView.as_view()), diff --git a/terminus/settings.py b/terminus/settings.py index ccd5d35..c3f7f39 100644 --- a/terminus/settings.py +++ b/terminus/settings.py @@ -39,6 +39,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'channels', 'rest_framework', + 'social_django', 'terminus.app', ] diff --git a/terminus/urls.py b/terminus/urls.py index b9613c5..82f16df 100644 --- a/terminus/urls.py +++ b/terminus/urls.py @@ -19,5 +19,6 @@ from .app.urls import urlpatterns urlpatterns = [ path('', include(urlpatterns)), + path('', include('social_django.urls', namespace='social')), path('admin/', admin.site.urls), ]