diff --git a/hypercorn.toml b/hypercorn.toml deleted file mode 100644 index 19c48c6..0000000 --- a/hypercorn.toml +++ /dev/null @@ -1,2 +0,0 @@ -errorlog="-" -workers=4 diff --git a/poetry.lock b/poetry.lock index 7a836e0..826de18 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "asgiref" version = "3.3.4" @@ -66,6 +74,38 @@ six = "*" [package.extras] visualize = ["graphviz (>0.5.1)", "Twisted (>=16.1.1)"] +[[package]] +name = "black" +version = "21.6b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.8.1,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] +python2 = ["typed-ast (>=1.4.2)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "cffi" version = "1.14.5" @@ -93,6 +133,14 @@ Django = ">=2.2" [package.extras] tests = ["pytest", "pytest-django", "pytest-asyncio", "async-generator", "async-timeout", "coverage (>=4.5,<5.0)"] +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "click" version = "8.0.1" @@ -308,11 +356,11 @@ idna = ">=2.5" [[package]] name = "idna" -version = "3.2" +version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" @@ -349,6 +397,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "mysqlclient" version = "2.0.3" @@ -370,6 +426,14 @@ rsa = ["cryptography (>=3.0.0,<4)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "promise" version = "2.3" @@ -500,15 +564,29 @@ optional = false python-versions = "*" [[package]] -name = "requests" -version = "2.15.1" -description = "Python HTTP for Humans." -category = "main" +name = "regex" +version = "2021.7.6" +description = "Alternative regular expression module, to replace re." +category = "dev" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + [package.extras] -security = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)"] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] @@ -534,6 +612,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "service-identity" version = "21.1.0" @@ -607,6 +693,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "twisted" version = "20.3.0" @@ -652,6 +746,14 @@ all = ["zope.interface (>=5.2.0)", "twisted (>=20.3.0)"] dev = ["wheel", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "pep8 (>=1.6.2)", "sphinx (>=1.2.3)", "pyenchant (>=1.6.6)", "sphinxcontrib-spelling (>=2.1.2)", "sphinx-rtd-theme (>=0.1.9)", "tox (>=2.1.1)", "mock (==1.3.0)", "twine (>=1.6.5)", "tox-gh-actions (>=2.2.0)"] twisted = ["zope.interface (>=5.2.0)", "twisted (>=20.3.0)"] +[[package]] +name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "3.10.0.0" @@ -660,6 +762,19 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "urllib3" +version = "1.26.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "uvicorn" version = "0.14.0" @@ -726,9 +841,13 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "894aba6b24869dde338f2fdcb2b4ed6f62e6cfbc9a2f934c4ee73cf61b88c36c" +content-hash = "e15a4f9c7fdbf146c22cdd14ac7e67c7fa975a1db04f10c34a6b4c8aa8c9c91e" [metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] asgiref = [ {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, @@ -745,6 +864,14 @@ automat = [ {file = "Automat-20.2.0-py2.py3-none-any.whl", hash = "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111"}, {file = "Automat-20.2.0.tar.gz", hash = "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33"}, ] +black = [ + {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, + {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, +] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] cffi = [ {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, @@ -800,6 +927,10 @@ channels = [ {file = "channels-3.0.3-py3-none-any.whl", hash = "sha256:3f15bdd2138bb4796e76ea588a0a344b12a7964ea9b2e456f992fddb988a4317"}, {file = "channels-3.0.3.tar.gz", hash = "sha256:056b72e51080a517a0f33a0a30003e03833b551d75394d6636c885d4edb8188f"}, ] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, @@ -878,8 +1009,8 @@ hyperlink = [ {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, ] idna = [ - {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, - {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] importlib-metadata = [ {file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"}, @@ -893,6 +1024,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"}, ] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] mysqlclient = [ {file = "mysqlclient-2.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:3381ca1a4f37ff1155fcfde20836b46416d66531add8843f6aa6d968982731c3"}, {file = "mysqlclient-2.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0ac0dd759c4ca02c35a9fedc24bc982cf75171651e8187c2495ec957a87dfff7"}, @@ -904,6 +1039,10 @@ oauthlib = [ {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"}, {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, ] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] promise = [ {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, ] @@ -973,9 +1112,52 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +regex = [ + {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, + {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, + {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, + {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, + {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, + {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, + {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, + {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, + {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, + {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, + {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, + {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, + {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, +] requests = [ - {file = "requests-2.15.1-py2.py3-none-any.whl", hash = "sha256:ff753b2196cd18b1bbeddc9dcd5c864056599f7a7d9a4fb5677e723efa2b7fb9"}, - {file = "requests-2.15.1.tar.gz", hash = "sha256:e5659b9315a0610505e050bb7190bf6fa2ccee1ac295f2b760ef9d8a03ebbb2e"}, + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] requests-oauthlib = [ {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, @@ -986,6 +1168,10 @@ rx = [ {file = "Rx-1.6.1-py2.py3-none-any.whl", hash = "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"}, {file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"}, ] +semver = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] 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"}, @@ -1007,6 +1193,10 @@ sqlparse = [ {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, ] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] twisted = [ {file = "Twisted-20.3.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7"}, {file = "Twisted-20.3.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a"}, @@ -1036,11 +1226,47 @@ txaio = [ {file = "txaio-21.2.1-py2.py3-none-any.whl", hash = "sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"}, {file = "txaio-21.2.1.tar.gz", hash = "sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8"}, ] +typed-ast = [ + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] +urllib3 = [ + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, +] uvicorn = [ {file = "uvicorn-0.14.0-py3-none-any.whl", hash = "sha256:2a76bb359171a504b3d1c853409af3adbfa5cef374a4a59e5881945a97a93eae"}, {file = "uvicorn-0.14.0.tar.gz", hash = "sha256:45ad7dfaaa7d55cab4cd1e85e03f27e9d60bc067ddc59db52a2b0aeca8870292"}, diff --git a/pyproject.toml b/pyproject.toml index 0abe61d..f45e678 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,12 @@ mysqlclient = "^2.0.3" uvicorn = "^0.14.0" gunicorn = "^20.1.0" Twisted = "20.3.0" +semver = "^2.13.0" +requests = "^2.25.1" [tool.poetry.dev-dependencies] flake8 = "^3.9.2" +black = {version = "^21.6b0", allow-prereleases = true} [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/src/api.ts b/src/api.ts index a26c1da..fadf204 100644 --- a/src/api.ts +++ b/src/api.ts @@ -27,6 +27,12 @@ export interface InstanceInfo { login_enabled: boolean } +export interface Gateway { + host: string + port: number + url: string +} + @Injectable({ providedIn: 'root' }) export class InstanceInfoResolver implements Resolve> { constructor (private http: HttpClient) { } diff --git a/src/components/configModal.component.pug b/src/components/configModal.component.pug index 57889b8..a37b0fe 100644 --- a/src/components/configModal.component.pug +++ b/src/components/configModal.component.pug @@ -3,9 +3,11 @@ .d-flex.align-items-center.py-2.px-4 .me-auto label Active config - .title {{configService.activeConfig.modified_at}} + .title + fa-icon([icon]='_configIcon') + span.ms-2 {{configService.activeConfig.created_at|date:"medium"}} - button.btn.btn-semi.me-2((click)='configService.duplicateConfig()') + button.btn.btn-semi.me-2((click)='configService.duplicateActiveConfig()') fa-icon([icon]='_copyIcon', [fixedWidth]='true') button.btn.btn-semi((click)='deleteConfig()') @@ -16,25 +18,26 @@ div(ngbDropdown) button.btn.btn-semi(ngbDropdownToggle) {{configService.activeVersion.version}} div(ngbDropdownMenu) - a( - *ngFor='let version of versions', + button( + *ngFor='let version of configService.versions', ngbDropdownItem, [class.active]='version == configService.activeVersion', (click)='selectVersion(version)' ) {{version.version}} - div(*ngIf='configService.configs.length > 1') - .dropdown-header All configs + .px-4.pt-3(*ngIf='configService.configs.length > 1') + h5 Other configs - ng-container(*ngFor='let config of configService.configs') - a( - *ngIf='config !== configService.activeConfig', - ngbDropdownItem, - (click)='selectConfig(config)', - href='#' - ) Config modified at {{config.modified_at}} + .list-group.list-group-light + ng-container(*ngFor='let config of configService.configs') + button.list-group-item.list-group-item-action( + *ngIf='config !== configService.activeConfig', + (click)='selectConfig(config)' + ) + fa-icon([icon]='_configIcon') + span Config created at {{config.created_at|date:"medium"}} - .p-3 - button.btn.btn-semi.w-100((click)='configService.createNewConfig()') + .py-3.px-4 + button.btn.btn-semi.w-100((click)='createNewConfig()') fa-icon([icon]='_addIcon', [fixedWidth]='true') span New config diff --git a/src/components/configModal.component.ts b/src/components/configModal.component.ts index 072a41c..f0031ae 100644 --- a/src/components/configModal.component.ts +++ b/src/components/configModal.component.ts @@ -2,7 +2,8 @@ import { Component } from '@angular/core' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { AppConnectorService } from '../services/appConnector.service' import { ConfigService } from '../services/config.service' -import { faPlus } from '@fortawesome/free-solid-svg-icons' +import { faCopy, faFile, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons' +import { Config, Version } from '../api' @Component({ selector: 'config-modal', @@ -11,6 +12,9 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons' }) export class ConfigModalComponent { _addIcon = faPlus + _copyIcon = faCopy + _deleteIcon = faTrash + _configIcon = faFile constructor ( private modalInstance: NgbActiveModal, @@ -26,4 +30,20 @@ export class ConfigModalComponent { this.modalInstance.dismiss() } + async createNewConfig () { + const config = await this.configService.createNewConfig() + await this.configService.selectConfig(config) + this.modalInstance.dismiss() + } + + async selectConfig (config: Config) { + await this.configService.selectConfig(config) + this.modalInstance.dismiss() + } + + async selectVersion (version: Version) { + await this.configService.selectVersion(version) + this.modalInstance.dismiss() + } + } diff --git a/src/components/home.component.pug b/src/components/home.component.pug index 623cc48..5976138 100644 --- a/src/components/home.component.pug +++ b/src/components/home.component.pug @@ -16,8 +16,8 @@ .intro h1 Hey. div My name is Eugene and I've built a nice terminal app, #[em.ms-1 just for you]. - div Crossplatform, local, SSH, serial - it's all there. - div Go on, try it out 👇 + div Crossplatform, local, SSH, serial, Telnet - it's all there. + div Here's a demo 👇 iframe(#iframe) diff --git a/src/components/home.component.ts b/src/components/home.component.ts index 3c21567..f7e2340 100644 --- a/src/components/home.component.ts +++ b/src/components/home.component.ts @@ -103,7 +103,7 @@ export class HomeComponent { async ngAfterViewInit () { const versions = await this.http.get('/api/1/versions').toPromise() - versions.sort((a, b) => semverGT(a, b)) + versions.sort((a, b) => semverGT(a.version, b.version)) this.connector = new DemoConnector(this.iframe.nativeElement.contentWindow, versions[0]) this.iframe.nativeElement.src = '/terminal' } diff --git a/src/components/main.component.pug b/src/components/main.component.pug index aac01a1..cd38f91 100644 --- a/src/components/main.component.pug +++ b/src/components/main.component.pug @@ -1,13 +1,13 @@ .sidebar img.logo(src='{{_logo}}') - button.btn((click)='openConfig()') - fa-icon([icon]='_cogIcon', [fixedWidth]='true') + button.btn.mt-auto((click)='openConfig()') + fa-icon([icon]='_configIcon', [fixedWidth]='true') button.btn((click)='openSettings()') fa-icon([icon]='_settingsIcon', [fixedWidth]='true') - button.btn.mt-auto((click)='logout()') + button.btn.mt-3((click)='logout()') fa-icon([icon]='_logoutIcon', [fixedWidth]='true') .terminal([hidden]='!showApp') diff --git a/src/components/main.component.ts b/src/components/main.component.ts index 94afe02..58bdbd8 100644 --- a/src/components/main.component.ts +++ b/src/components/main.component.ts @@ -2,7 +2,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core' import { HttpClient } from '@angular/common/http' import { AppConnectorService } from '../services/appConnector.service' -import { faCog, faCopy, faTrash, faPlus, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' +import { faCog, faFile, faPlus, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' import { LoginService } from '../services/login.service' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { SettingsModalComponent } from './settingsModal.component' @@ -19,12 +19,10 @@ import { Router } from '@angular/router' }) export class MainComponent { _logo = require('../assets/logo.svg') - _cogIcon = faCog _settingsIcon = faCog _logoutIcon = faSignOutAlt - _copyIcon = faCopy _addIcon = faPlus - _deleteIcon = faTrash + _configIcon = faFile showApp = false diff --git a/src/components/settingsModal.component.pug b/src/components/settingsModal.component.pug index 0bc395b..13217bd 100644 --- a/src/components/settingsModal.component.pug +++ b/src/components/settingsModal.component.pug @@ -1,5 +1,6 @@ .modal-header h5.modal-title Settings + .modal-body .mb-3 .form-check.form-switch @@ -27,6 +28,13 @@ ) label Gateway authentication token + div(*ngIf='appConnector.sockets.length') + h5 Active connections + .list-group.list-group-flush + .list-group-item(*ngFor='let socket of appConnector.sockets') + div {{socket.options.host}}:{{socket.options.port}} + .text-muted via {{socket.url}} + .modal-footer button.btn.btn-primary((click)='apply()') Apply button.btn.btn-secondary((click)='cancel()') Cancel diff --git a/src/components/settingsModal.component.ts b/src/components/settingsModal.component.ts index 0f4051d..318a573 100644 --- a/src/components/settingsModal.component.ts +++ b/src/components/settingsModal.component.ts @@ -3,6 +3,7 @@ import { LoginService } from '../services/login.service' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { User } from '../api' +import { AppConnectorService } from '../services/appConnector.service' @Component({ selector: 'settings-modal', @@ -14,6 +15,7 @@ export class SettingsModalComponent { customGatewayEnabled = false constructor ( + public appConnector: AppConnectorService, private modalInstance: NgbActiveModal, private loginService: LoginService, ) { diff --git a/src/services/appConnector.service.ts b/src/services/appConnector.service.ts index f41fdf5..d9138ab 100644 --- a/src/services/appConnector.service.ts +++ b/src/services/appConnector.service.ts @@ -4,13 +4,15 @@ import { debounceTime } from 'rxjs/operators' import { HttpClient } from '@angular/common/http' import { Injectable } from '@angular/core' import { LoginService } from '../services/login.service' -import { Config, Version } from '../api' +import { Config, Gateway, Version } from '../api' export class SocketProxy { connect$ = new Subject() data$ = new Subject() error$ = new Subject() + close$ = new Subject() + url: string webSocket: WebSocket initialBuffer: Buffer options: { @@ -22,12 +24,18 @@ export class SocketProxy { this.initialBuffer = Buffer.from('') } - connect (options) { + async connect (options) { this.options = options - this.webSocket = new WebSocket( - this.appConnector.loginService.user.custom_connection_gateway || - `ws://${location.host}/api/1/gateway/tcp` - ) + this.url = this.appConnector.loginService.user.custom_connection_gateway + if (!this.url) { + try { + this.url = (await this.appConnector.chooseConnectionGateway()).url + } catch (err) { + this.error$.next(err) + return + } + } + this.webSocket = new WebSocket(this.url) this.webSocket.onmessage = async event => { if (typeof(event.data) === 'string') { this.handleServiceMessage(JSON.parse(event.data)) @@ -35,6 +43,9 @@ export class SocketProxy { this.data$.next(Buffer.from(await event.data.arrayBuffer())) } } + this.webSocket.onclose = () => { + this.close() + } } handleServiceMessage (msg) { @@ -68,20 +79,23 @@ export class SocketProxy { } write (chunk: Buffer): void { - if (!this.webSocket.readyState) { + if (!this.webSocket?.readyState) { this.initialBuffer = Buffer.concat([this.initialBuffer, chunk]) } else { this.webSocket.send(chunk) } } - close (error: Error): void { + close (error?: Error): void { + this.webSocket.close() if (error) { this.error$.next(error) } this.connect$.complete() this.data$.complete() this.error$.complete() + this.close$.next() + this.close$.complete() } } @@ -90,6 +104,7 @@ export class AppConnectorService { private configUpdate = new Subject() private config: Config private version: Version + sockets: SocketProxy[] = [] constructor ( private http: HttpClient, @@ -135,6 +150,15 @@ export class AppConnectorService { } createSocket () { - return new SocketProxy(this) + const socket = new SocketProxy(this) + this.sockets.push(socket) + socket.close$.subscribe(() => { + this.sockets = this.sockets.filter(x => x !== socket) + }) + return socket + } + + async chooseConnectionGateway (): Promise { + return await this.http.post('/api/1/gateways/choose', {}).toPromise() } } diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 4f05b49..9c896cd 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -33,11 +33,17 @@ export class ConfigService { await this.http.put('/api/1/user', this.user).toPromise() } - async createNewConfig () { - this.configs.push(await this.http.post('/api/1/configs', { + async createNewConfig (): Promise { + const config = await this.http.post('/api/1/configs', { content: '{}', - last_used_with_version: this._activeVersion.version, - }).toPromise()) + last_used_with_version: this._activeVersion?.version ?? this.getLatestStableVersion().version, + }).toPromise() + this.configs.push(config) + return config + } + + getLatestStableVersion () { + return this.versions[0] } async duplicateActiveConfig () { @@ -60,6 +66,8 @@ export class ConfigService { this._activeConfig = config this.activeConfig$.next(config) this.selectVersion(matchingVersion) + this.loginService.user.active_config = config.id + await this.loginService.updateUser() } async selectDefaultConfig () { @@ -71,7 +79,7 @@ export class ConfigService { private async init () { 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)) + this.versions.sort((a, b) => semverGT(a.version, b.version)) if (!this.configs.length) { await this.createNewConfig() diff --git a/src/terminal.ts b/src/terminal.ts index daa0abb..56564a6 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -27,16 +27,26 @@ async function start () { return window['module'].exports } + async function prefetchURL (url) { + await (await fetch(url)).text() + } + const baseUrl = `${connector.getDistURL()}/${appVersion}` - await webRequire(`${baseUrl}/tabby-web-container/dist/preload.js`) - await webRequire(`${baseUrl}/tabby-web-container/dist/bundle.js`) + const coreURLs = [ + `${baseUrl}/tabby-web-container/dist/preload.js`, + `${baseUrl}/tabby-web-container/dist/bundle.js`, + ] + + await Promise.all(coreURLs.map(prefetchURL)) + + for (const url of coreURLs) { + await webRequire(url) + } const tabby = window['Tabby'] - const pluginModules = [] - for (const plugin of connector.getPluginsToLoad()) { - pluginModules.push(await tabby.loadPlugin(`${baseUrl}/${plugin}`)) - } + const pluginURLs = connector.getPluginsToLoad().map(x => `${baseUrl}/${x}`) + const pluginModules = await tabby.loadPlugins(pluginURLs) document.querySelector('app-root')['style'].display = 'flex' diff --git a/src/theme/vars.scss b/src/theme/vars.scss index c306aba..cc8c887 100644 --- a/src/theme/vars.scss +++ b/src/theme/vars.scss @@ -52,8 +52,8 @@ $font-size-sm: .85rem; $line-height-base: 1.6; -$border-radius: .4rem; -$border-radius-lg: .6rem; +$border-radius: .35rem; +$border-radius-lg: .35rem; $border-radius-sm: .2rem; $box-shadow: 0 .5rem 1rem rgba($black, .5) !default; @@ -79,6 +79,7 @@ $link-hover-decoration: none; $component-active-color: $white; $component-active-bg: $blue; +$list-group-color: $body-color; $list-group-bg: $table-bg; $list-group-border-color: $table-border-color; @@ -145,6 +146,7 @@ $navbar-padding-x: 0; $dropdown-bg: $body-bg; $dropdown-color: $body-color; $dropdown-border-width: 1px; +$dropdown-border-color: #ffffff24; $dropdown-header-color: $gray-500; $dropdown-link-color: $body-color; @@ -182,7 +184,10 @@ $modal-content-bg: $body-bg; $modal-content-border-color: $body-bg; $modal-header-border-width: 0; $modal-footer-border-width: 0; -$modal-content-border-width: 0; + +$modal-content-border-color: #ffffff24; +$modal-content-border-width: 1px; + $progress-bg: $table-bg; $progress-height: 3px; diff --git a/tabby/app/admin.py b/tabby/app/admin.py index 5f89b0d..e1118dd 100644 --- a/tabby/app/admin.py +++ b/tabby/app/admin.py @@ -1,11 +1,14 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from .models import User, Config +from .models import Gateway, User, Config + class CustomUserAdmin(UserAdmin): fieldsets = UserAdmin.fieldsets + ( (None, {'fields': ('custom_connection_gateway', 'custom_connection_gateway_token')}), ) + admin.site.register(User, CustomUserAdmin) admin.site.register(Config) +admin.site.register(Gateway) diff --git a/tabby/app/api.py b/tabby/app/api.py index b6c64a2..220011c 100644 --- a/tabby/app/api.py +++ b/tabby/app/api.py @@ -1,9 +1,10 @@ import os +import random from dataclasses import dataclass from django.conf import settings from django.contrib.auth import logout from rest_framework import fields -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import PermissionDenied, NotFound from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin @@ -12,7 +13,7 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.serializers import ModelSerializer, Serializer from rest_framework_dataclasses.serializers import DataclassSerializer -from .models import Config, User +from .models import Config, Gateway, User @dataclass @@ -25,6 +26,17 @@ class AppVersionSerializer(DataclassSerializer): dataclass = AppVersion +class GatewaySerializer(ModelSerializer): + url = fields.SerializerMethodField() + + class Meta: + fields = '__all__' + model = Gateway + + def get_url(self, gw): + return f'{"wss" if gw.secure else "ws"}://{gw.host}:{gw.port}/' + + class ConfigSerializer(ModelSerializer): class Meta: model = Config @@ -103,3 +115,14 @@ class InstanceInfoViewSet(RetrieveModelMixin, GenericViewSet): return { 'login_enabled': settings.ENABLE_LOGIN, } + + +class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet): + queryset = Gateway.objects.filter(enabled=True) + serializer_class = GatewaySerializer + + def get_object(self): + gateways = list(self.queryset) + if not len(gateways): + raise NotFound() + return random.choice(gateways) diff --git a/tabby/app/management/__init__.py b/tabby/app/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tabby/app/management/commands/__init__.py b/tabby/app/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tabby/app/management/commands/add_version.py b/tabby/app/management/commands/add_version.py new file mode 100644 index 0000000..7a96164 --- /dev/null +++ b/tabby/app/management/commands/add_version.py @@ -0,0 +1,56 @@ +import logging +import requests +import shutil +import subprocess +import tempfile +from django.core.management.base import BaseCommand +from django.conf import settings +from pathlib import Path + + +class Command(BaseCommand): + help = 'Downloads a new app version' + + def add_arguments(self, parser): + parser.add_argument('version', type=str) + + def handle(self, *args, **options): + version = options['version'] + target: Path = settings.APP_DIST_PATH / version + + plugin_list = [ + 'tabby-web-container', + 'tabby-core', + 'tabby-settings', + 'tabby-terminal', + 'tabby-ssh', + 'tabby-community-color-schemes', + '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}') + response.raise_for_status() + info = response.json() + url = info['dist']['tarball'] + + logging.info(f'Downloading {plugin}@{version} from {url}') + response = requests.get(url) + + 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)] + ) + shutil.move(Path(extraction_tmp) / 'package', plugin_final_target) + + if target.exists(): + shutil.rmtree(target) + shutil.copytree(tempdir, target) diff --git a/tabby/app/migrations/0001_initial.py b/tabby/app/migrations/0001_initial.py index 4dcd921..e81aa92 100644 --- a/tabby/app/migrations/0001_initial.py +++ b/tabby/app/migrations/0001_initial.py @@ -1,8 +1,10 @@ -# Generated by Django 3.2.3 on 2021-06-05 21:23 +# Generated by Django 3.2.3 on 2021-07-08 17:43 +from django.conf import settings import django.contrib.auth.models import django.contrib.auth.validators from django.db import migrations, models +import django.db.models.deletion import django.utils.timezone @@ -29,8 +31,11 @@ class Migration(migrations.Migration): ('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')), - ('groups', 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')), - ('user_permissions', 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')), + ('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)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), ], options={ 'verbose_name': 'user', @@ -41,4 +46,30 @@ class Migration(migrations.Migration): ('objects', django.contrib.auth.models.UserManager()), ], ), + migrations.CreateModel( + 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)), + ], + ), + 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'), + ), + 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'), + ), + 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'), + ), ] diff --git a/tabby/app/migrations/0002_auto_20210605_2137.py b/tabby/app/migrations/0002_auto_20210605_2137.py deleted file mode 100644 index 3d9df5c..0000000 --- a/tabby/app/migrations/0002_auto_20210605_2137.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 3.2.3 on 2021-06-05 21:37 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='active_version', - field=models.CharField(max_length=32, null=True), - ), - migrations.AddField( - model_name='user', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='user', - name='modified_at', - field=models.DateTimeField(auto_now=True), - ), - migrations.CreateModel( - 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)), - ], - ), - 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'), - ), - ] diff --git a/tabby/app/migrations/0002_gateway.py b/tabby/app/migrations/0002_gateway.py new file mode 100644 index 0000000..691a887 --- /dev/null +++ b/tabby/app/migrations/0002_gateway.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.3 on 2021-07-08 20:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + ), + ] diff --git a/tabby/app/migrations/0003_user_custom_connection_gateway.py b/tabby/app/migrations/0003_user_custom_connection_gateway.py deleted file mode 100644 index ac0b451..0000000 --- a/tabby/app/migrations/0003_user_custom_connection_gateway.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.3 on 2021-06-20 20:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0002_auto_20210605_2137'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='custom_connection_gateway', - field=models.CharField(max_length=255, null=True), - ), - ] diff --git a/tabby/app/migrations/0004_user_custom_connection_gateway_token.py b/tabby/app/migrations/0004_user_custom_connection_gateway_token.py deleted file mode 100644 index d62fe60..0000000 --- a/tabby/app/migrations/0004_user_custom_connection_gateway_token.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.3 on 2021-06-20 20:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0003_user_custom_connection_gateway'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='custom_connection_gateway_token', - field=models.CharField(max_length=255, null=True), - ), - ] diff --git a/tabby/app/models.py b/tabby/app/models.py index 751f878..cddbbf9 100644 --- a/tabby/app/models.py +++ b/tabby/app/models.py @@ -22,7 +22,11 @@ class User(AbstractUser): modified_at = models.DateTimeField(auto_now=True) -# @receiver(user_logged_in) -# def post_login(sender, user, request, **kwargs): -# if not user.active_config: -# user.active_config = Config.objects.filter() +class Gateway(models.Model): + host = models.CharField(max_length=255) + port = models.IntegerField(default=1234) + enabled = models.BooleanField(default=True) + secure = models.BooleanField(default=True) + + def __str__(self): + return f'{self.host}:{self.port}' diff --git a/tabby/app/urls.py b/tabby/app/urls.py index 03332f1..79169e7 100644 --- a/tabby/app/urls.py +++ b/tabby/app/urls.py @@ -14,6 +14,7 @@ urlpatterns = [ path('api/1/auth/logout', api.LogoutView.as_view()), path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})), path('api/1/instance-info', api.InstanceInfoViewSet.as_view({'get': 'retrieve'})), + path('api/1/gateways/choose', api.ChooseGatewayViewSet.as_view({'post': 'retrieve'})), re_path('^(|login|app)$', views.IndexView.as_view()), diff --git a/tabby/app/views.py b/tabby/app/views.py index fd3cf82..75dc01b 100644 --- a/tabby/app/views.py +++ b/tabby/app/views.py @@ -21,6 +21,6 @@ class AppDistView(APIView): return static.serve(request, os.path.join(version, path), document_root=str(settings.APP_DIST_PATH)) -class BuildView(APIView): - def get(self, request, path=None, format=None): - return static.serve(request, path, document_root=str(settings.BASE_DIR / 'build')) +# class BuildView(APIView): +# def get(self, request, path=None, format=None): +# return static.serve(request, path, document_root=str(settings.BASE_DIR / 'build')) diff --git a/tabby/settings.py b/tabby/settings.py index 06a194f..76e535b 100644 --- a/tabby/settings.py +++ b/tabby/settings.py @@ -109,6 +109,29 @@ USE_L10N = True USE_TZ = True +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + }, + }, + 'loggers': { + '': { + 'handlers': ['console'], + 'propagate': False, + 'level': 'INFO', + }, + }, +} # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ @@ -140,7 +163,8 @@ SOCIAL_AUTH_GITHUB_SCOPE = ['read:user', 'user:email'] LOGIN_REDIRECT_URL = '/app' -APP_DIST_PATH = os.getenv('APP_DIST_PATH', BASE_DIR / 'app-dist') +APP_DIST_PATH = Path(os.getenv('APP_DIST_PATH', BASE_DIR / 'app-dist')) +NPM_REGISTRY = os.getenv('NPM_REGISTRY', 'https://registry.npmjs.org').rstrip('/') for key in [ 'SOCIAL_AUTH_GITHUB_KEY', @@ -168,6 +192,12 @@ for key in [ globals()[key] = int(globals()[key]) if globals()[key] else None +for key in [ + 'ENABLE_LOGIN', +]: + globals()[key] = int(globals()[key]) if globals()[key] else None + + for key in [ 'CONNECTION_GATEWAY_AUTH_CA', 'CONNECTION_GATEWAY_AUTH_CERTIFICATE',