Browse Source

Merge remote-tracking branch 'origin/v1' into v1_reverse_proxy

Rodja Trappe 2 years ago
parent
commit
a72574fc05
5 changed files with 93 additions and 42 deletions
  1. 29 18
      examples/authentication/main.py
  2. 46 19
      main.py
  3. 15 3
      poetry.lock
  4. 2 1
      pyproject.toml
  5. 1 1
      website/constants.py

+ 29 - 18
examples/authentication/main.py

@@ -1,46 +1,57 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
+'''This is only a very simple authentication example which stores session ids in memory and does not do any password hashing.
+
+Please see the `OAuth2 example at FastAPI <https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/>`_  or
+use the great `Authlib package <https://docs.authlib.org/en/v0.13/client/starlette.html#using-fastapi>`_ to implement a real authentication system.
+
+Here we just demonstrate the NiceGUI integration.
+'''
+
 import uuid
 import uuid
 from typing import Dict
 from typing import Dict
 
 
+from fastapi import Request
+from fastapi.responses import RedirectResponse
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.middleware.sessions import SessionMiddleware
-from starlette.requests import Request
 
 
 from nicegui import app, ui
 from nicegui import app, ui
 
 
-app.add_middleware(SessionMiddleware, secret_key='some_random_string')
+app.add_middleware(SessionMiddleware, secret_key='some_random_string')  # use your own secret key here
 
 
-session_info: Dict[str, Dict] = {}  # in reality in a database
+# in reality users and session_info would be persistent (e.g. database, file, ...) and passwords obviously hashed
+users = [('user1', 'pass1'), ('user2', 'pass2')]
+session_info: Dict[str, Dict] = {}
 
 
 
 
 @ui.page('/')
 @ui.page('/')
 def main_page(request: Request) -> None:
 def main_page(request: Request) -> None:
     if is_authenticated(request):
     if is_authenticated(request):
-        create_welcome_message(session_info[request.session['id']]['username'])
+        session = session_info[request.session['id']]
+        with ui.row().classes('absolute-center'):
+            ui.label(f'Hello {session["username"]}!').classes('text-2xl')
     else:
     else:
-        request.session['id'] = str(uuid.uuid4())
-        create_login_form(request.session['id'])
-
-
-def is_authenticated(request: Request) -> bool:
-    return session_info.get(request.session.get('id'), {}).get('authenticated', False)
+        return RedirectResponse('/login')
 
 
 
 
-def create_login_form(session_id: str) -> None:
+@ui.page('/login')
+def login(request: Request) -> None:
+    if is_authenticated(request):
+        return RedirectResponse('/')
+    request.session['id'] = str(uuid.uuid4())  # NOTE this stores a new session id in the cookie of the client
     with ui.card().classes('absolute-center'):
     with ui.card().classes('absolute-center'):
         username = ui.input('Username')
         username = ui.input('Username')
         password = ui.input('Password').classes('w-full').props('type=password')
         password = ui.input('Password').classes('w-full').props('type=password')
-        ui.button('Log in', on_click=lambda: try_login(session_id, username.value, password.value))
+        ui.button('Log in', on_click=lambda: try_login(request.session['id'], username.value, password.value))
+
+
+def is_authenticated(request: Request) -> bool:
+    return session_info.get(request.session.get('id'), {}).get('authenticated', False)
 
 
 
 
 def try_login(session_id: str, username: str, password: str) -> None:
 def try_login(session_id: str, username: str, password: str) -> None:
-    if (username, password) in [('user1', 'pass1'), ('user2', 'pass2')]:
+    if (username, password) in users:
         session_info[session_id] = {'username': username, 'authenticated': True}
         session_info[session_id] = {'username': username, 'authenticated': True}
     ui.open('/')
     ui.open('/')
 
 
 
 
-def create_welcome_message(username: str) -> None:
-    with ui.row().classes('absolute-center'):
-        ui.label(f'Hello {username}!').classes('text-2xl')
-
-
 ui.run()
 ui.run()

+ 46 - 19
main.py

@@ -39,14 +39,39 @@ def add_head_html() -> None:
             font-style: normal;
             font-style: normal;
             color: {ACCENT_COLOR};
             color: {ACCENT_COLOR};
         }}
         }}
+        a:hover {{
+            opacity: 0.9;
+        }}
         </style>
         </style>
     ''')
     ''')
+    ui.add_head_html(f'''
+    <style>
+    .q-header {{
+        height: calc({HEADER_HEIGHT} + 20px);
+        background-color: {ACCENT_COLOR};
+    }}
+    .q-header.fade {{
+        height: {HEADER_HEIGHT};
+        background-color: {ACCENT_COLOR}d0;
+        backdrop-filter: blur(5px);
+    }}
+    </style>
+    <script>
+    window.onscroll = () => {{
+        const header = document.querySelector(".q-header");
+        if (document.documentElement.scrollTop > 50)
+            header.classList.add("fade");
+        else
+            header.classList.remove("fade");
+    }};
+    </script>
+    ''')
 
 
 
 
 def add_header() -> None:
 def add_header() -> None:
     with ui.header() \
     with ui.header() \
-            .classes('items-center') \
-            .style(f'background-color: {ACCENT_COLOR}; height: {HEADER_HEIGHT}; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
+            .classes('items-center duration-200 px-4', remove='q-pa-md') \
+            .style('box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('w-8 stroke-white')
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('w-8 stroke-white')
         with ui.link(target=index_page):
         with ui.link(target=index_page):
             ui.html((STATIC / 'nicegui_word.svg').read_text()).classes('w-24')
             ui.html((STATIC / 'nicegui_word.svg').read_text()).classes('w-24')
@@ -66,29 +91,31 @@ async def index_page(client: Client):
     add_header()
     add_header()
 
 
     with ui.row() \
     with ui.row() \
-            .classes('w-full q-pa-md items-center gap-12 no-wrap') \
-            .style(f'height: calc(100vh - {HEADER_HEIGHT}); transform: translateX(-250px)'):
+            .classes('w-full h-screen q-pa-md items-center gap-12 no-wrap') \
+            .style(f'transform: translateX(-250px)'):
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('stroke-black').style('width: 500px')
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('stroke-black').style('width: 500px')
         with ui.column().classes('gap-8'):
         with ui.column().classes('gap-8'):
             ui.html('Meet the <em>NiceGUI</em>.') \
             ui.html('Meet the <em>NiceGUI</em>.') \
                 .style('font-size: 400%; line-height: 0.9; font-weight: 500')
                 .style('font-size: 400%; line-height: 0.9; font-weight: 500')
-            ui.markdown('Your easy-to-use Python framework to create\n\nuser interfaces which show up in the browser.') \
+            ui.markdown('And let the browser be the frontend\n\nto your Python code.') \
                 .style('font-size: 200%; line-height: 0.9')
                 .style('font-size: 200%; line-height: 0.9')
 
 
     with ui.row() \
     with ui.row() \
-            .classes('w-full q-pa-md items-center gap-28 p-32 no-wrap') \
-            .style(f'height: calc(100vh - {HEADER_HEIGHT}); background: {ACCENT_COLOR}'):
+            .classes('w-full h-screen q-pa-md items-center gap-28 p-32 no-wrap') \
+            .style(f'background: {ACCENT_COLOR}'):
         with ui.column().classes('gap-6'):
         with ui.column().classes('gap-6'):
-            ui.markdown('Create buttons, dialogs, markdown,\n\n3D scenes, plots and much more at ease.') \
+            ui.markdown('Interact with buttons, dialogs, 3D scenes, plots and much more.') \
                 .style('font-size: 300%; color: white; line-height: 0.9; font-weight: 500').classes('mb-4')
                 .style('font-size: 300%; color: white; line-height: 0.9; font-weight: 500').classes('mb-4')
             ui.label('''
             ui.label('''
-                It is great for micro web apps, dashboards, robotics projects, smart home solutions
-                and similar use cases. You can also use it in development, for example when
-                tweaking/configuring a machine learning algorithm or tuning motor controllers.
+                NiceGUI handles the web development details for you.
+                So you can focus the development of your Python code that needs an user interface.
+                Anything from short scripts and dashboards to full robotics projects, IoT solutions, 
+                smart home automations and machine learning projects can benefit from having all code in one place.
+                
             ''').style('font-size: 150%; color: white').classes('leading-tight')
             ''').style('font-size: 150%; color: white').classes('leading-tight')
             with ui.row().style('font-size: 150%; color: white').classes('leading-tight gap-2'):
             with ui.row().style('font-size: 150%; color: white').classes('leading-tight gap-2'):
                 ui.html('''
                 ui.html('''
-                    NiceGUI is available as
+                    Available as
                     <a href="https://pypi.org/project/nicegui/"><strong>PyPI package</strong><span class="material-icons">north_east</span></a>,
                     <a href="https://pypi.org/project/nicegui/"><strong>PyPI package</strong><span class="material-icons">north_east</span></a>,
                     <a href="https://hub.docker.com/r/zauberzeug/nicegui"><strong>Docker image</strong><span class="material-icons">north_east</span></a> and on
                     <a href="https://hub.docker.com/r/zauberzeug/nicegui"><strong>Docker image</strong><span class="material-icons">north_east</span></a> and on
                     <a href="https://github.com/zauberzeug/nicegui"><strong>GitHub</strong><span class="material-icons">north_east</span></a>.
                     <a href="https://github.com/zauberzeug/nicegui"><strong>GitHub</strong><span class="material-icons">north_east</span></a>.
@@ -99,7 +126,7 @@ async def index_page(client: Client):
     ui.link_target('features').style(f'position: relative; top: -{HEADER_HEIGHT}')
     ui.link_target('features').style(f'position: relative; top: -{HEADER_HEIGHT}')
     with ui.column().classes('w-full q-pa-xl'):
     with ui.column().classes('w-full q-pa-xl'):
         ui.label('Features').classes('text-bold text-lg')
         ui.label('Features').classes('text-bold text-lg')
-        ui.html('What has <em>NiceGUI</em> to offer?') \
+        ui.html('Do everything the <em>nice</em> way') \
             .style('font-size: 300%; font-weight: 500; margin-top: -20px')
             .style('font-size: 300%; font-weight: 500; margin-top: -20px')
         with ui.row().classes('w-full no-wrap text-lg leading-tight justify-between'):
         with ui.row().classes('w-full no-wrap text-lg leading-tight justify-between'):
             with ui.column().classes('gap-1'):
             with ui.column().classes('gap-1'):
@@ -126,7 +153,7 @@ async def index_page(client: Client):
     ui.link_target('installation').style(f'position: relative; top: -{HEADER_HEIGHT}')
     ui.link_target('installation').style(f'position: relative; top: -{HEADER_HEIGHT}')
     with ui.column().classes('w-full q-pa-xl'):
     with ui.column().classes('w-full q-pa-xl'):
         ui.label('Installation').classes('text-bold text-lg')
         ui.label('Installation').classes('text-bold text-lg')
-        ui.html('Getting <em>started</em>') \
+        ui.html('Get <em>started</em>') \
             .style('font-size: 300%; font-weight: 500; margin-top: -20px')
             .style('font-size: 300%; font-weight: 500; margin-top: -20px')
         with ui.row().classes('w-full no-wrap text-lg leading-tight'):
         with ui.row().classes('w-full no-wrap text-lg leading-tight'):
             with ui.column().classes('w-1/3 gap-2'):
             with ui.column().classes('w-1/3 gap-2'):
@@ -194,8 +221,8 @@ ui.run()
                 example_link('Infinite Scroll', 'shows an infinitely scrolling image gallery')
                 example_link('Infinite Scroll', 'shows an infinitely scrolling image gallery')
 
 
     with ui.row() \
     with ui.row() \
-            .classes('w-full q-pa-md items-center gap-28 p-32 no-wrap') \
-            .style(f'height: calc(100vh - {HEADER_HEIGHT}); background: {ACCENT_COLOR}'):
+            .classes('w-full h-screen q-pa-md items-center gap-28 p-32 no-wrap') \
+            .style(f'background: {ACCENT_COLOR}'):
         with ui.column().classes('gap-6'):
         with ui.column().classes('gap-6'):
             ui.markdown('Why?') \
             ui.markdown('Why?') \
                 .style('font-size: 300%; color: white; line-height: 0.9; font-weight: 500').classes('mb-4')
                 .style('font-size: 300%; color: white; line-height: 0.9; font-weight: 500').classes('mb-4')
@@ -208,9 +235,9 @@ ui.run()
                 In search for an alternative nice library to write simple graphical user interfaces in Python we discovered
                 In search for an alternative nice library to write simple graphical user interfaces in Python we discovered
                 <strong><a href="https://justpy.io/">JustPy</a></strong>.
                 <strong><a href="https://justpy.io/">JustPy</a></strong>.
                 Although we liked the approach, it is too "low-level HTML" for our daily usage.
                 Although we liked the approach, it is too "low-level HTML" for our daily usage.
-                But it inspired us to use 
+                But it inspired us to use
                 <strong><a href="https://vuejs.org/">Vue</a></strong>
                 <strong><a href="https://vuejs.org/">Vue</a></strong>
-                and 
+                and
                 <strong><a href="https://quasar.dev/">Quasar</a></strong>
                 <strong><a href="https://quasar.dev/">Quasar</a></strong>
                 for the frontend.<br/>
                 for the frontend.<br/>
 
 
@@ -219,7 +246,7 @@ ui.run()
                 which itself is based on the ASGI framework
                 which itself is based on the ASGI framework
                 <strong><a href="https://www.starlette.io/">Starlette</a></strong>,
                 <strong><a href="https://www.starlette.io/">Starlette</a></strong>,
                 and the ASGI webserver
                 and the ASGI webserver
-                <strong><a href="https://www.uvicorn.org/">Uvicorn</a></strong>.                
+                <strong><a href="https://www.uvicorn.org/">Uvicorn</a></strong>.
             ''').style('font-size: 150%; color: white').classes('leading-tight')
             ''').style('font-size: 150%; color: white').classes('leading-tight')
 
 
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('stroke-white').style('width: 1500px')
         ui.html((STATIC / 'happy_face.svg').read_text()).classes('stroke-white').style('width: 1500px')

+ 15 - 3
poetry.lock

@@ -307,6 +307,14 @@ category = "dev"
 optional = false
 optional = false
 python-versions = "*"
 python-versions = "*"
 
 
+[[package]]
+name = "itsdangerous"
+version = "2.1.2"
+description = "Safely pass data to untrusted environments and back."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
 [[package]]
 [[package]]
 name = "kiwisolver"
 name = "kiwisolver"
 version = "1.4.4"
 version = "1.4.4"
@@ -693,7 +701,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 
 [[package]]
 [[package]]
 name = "selenium"
 name = "selenium"
-version = "4.7.0"
+version = "4.7.2"
 description = ""
 description = ""
 category = "dev"
 category = "dev"
 optional = false
 optional = false
@@ -941,7 +949,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 [metadata]
 lock-version = "1.1"
 lock-version = "1.1"
 python-versions = "^3.7"
 python-versions = "^3.7"
-content-hash = "1fd5c2160bd84817dbeadd7bf67102db075caa635897963771c4cca954682bd1"
+content-hash = "f4f7d8667e1d71c4bd364cabdc219d855492b8b24f69a7fda197c4f194e13e9b"
 
 
 [metadata.files]
 [metadata.files]
 anyio = [
 anyio = [
@@ -1192,6 +1200,10 @@ iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
 ]
 ]
+itsdangerous = [
+    {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
+    {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
+]
 kiwisolver = [
 kiwisolver = [
     {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"},
     {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"},
     {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"},
     {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"},
@@ -1592,7 +1604,7 @@ requests = [
     {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
     {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
 ]
 ]
 selenium = [
 selenium = [
-    {file = "selenium-4.7.0-py3-none-any.whl", hash = "sha256:771cb63b821a2aea01b0cdb623caa5b5c35b9a19f44d0d4f32a44b037a324b10"},
+    {file = "selenium-4.7.2-py3-none-any.whl", hash = "sha256:06a1c7d9f313130b21c3218ddd8852070d0e7419afdd31f96160cd576555a5ce"},
 ]
 ]
 setuptools = [
 setuptools = [
     {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
     {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},

+ 2 - 1
pyproject.toml

@@ -24,7 +24,7 @@ fastapi-socketio = "^0.0.9"
 vbuild = "^0.8.1"
 vbuild = "^0.8.1"
 watchfiles = "^0.18.1"
 watchfiles = "^0.18.1"
 
 
-[tool.poetry.dev-dependencies]
+[tool.poetry.group.dev.dependencies]
 icecream = "^2.1.0"
 icecream = "^2.1.0"
 autopep8 = "^1.5.7"
 autopep8 = "^1.5.7"
 debugpy = "^1.3.0"
 debugpy = "^1.3.0"
@@ -32,6 +32,7 @@ pytest-selenium = "^4.0.0"
 pytest-asyncio = "^0.19.0"
 pytest-asyncio = "^0.19.0"
 pytest = "6.2.5"
 pytest = "6.2.5"
 replicate = "^0.4.0"
 replicate = "^0.4.0"
+itsdangerous = "^2.1.2" # required by SessionMiddleware (see https://fastapi.tiangolo.com/?h=itsdangerous#optional-dependencies)
 
 
 [build-system]
 [build-system]
 requires = [
 requires = [

+ 1 - 1
website/constants.py

@@ -1,5 +1,5 @@
 from pathlib import Path
 from pathlib import Path
 
 
 ACCENT_COLOR = '#5A99FF'
 ACCENT_COLOR = '#5A99FF'
-HEADER_HEIGHT = '70px'
+HEADER_HEIGHT = '50px'
 STATIC = Path(__file__).parent / 'static'
 STATIC = Path(__file__).parent / 'static'