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
+'''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
 from typing import Dict
 
+from fastapi import Request
+from fastapi.responses import RedirectResponse
 from starlette.middleware.sessions import SessionMiddleware
-from starlette.requests import Request
 
 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('/')
 def main_page(request: Request) -> None:
     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:
-        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'):
         username = ui.input('Username')
         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:
-    if (username, password) in [('user1', 'pass1'), ('user2', 'pass2')]:
+    if (username, password) in users:
         session_info[session_id] = {'username': username, 'authenticated': True}
     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()

+ 46 - 19
main.py

@@ -39,14 +39,39 @@ def add_head_html() -> None:
             font-style: normal;
             color: {ACCENT_COLOR};
         }}
+        a:hover {{
+            opacity: 0.9;
+        }}
         </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:
     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')
         with ui.link(target=index_page):
             ui.html((STATIC / 'nicegui_word.svg').read_text()).classes('w-24')
@@ -66,29 +91,31 @@ async def index_page(client: Client):
     add_header()
 
     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')
         with ui.column().classes('gap-8'):
             ui.html('Meet the <em>NiceGUI</em>.') \
                 .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')
 
     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'):
-            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')
             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')
             with ui.row().style('font-size: 150%; color: white').classes('leading-tight gap-2'):
                 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://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>.
@@ -99,7 +126,7 @@ async def index_page(client: Client):
     ui.link_target('features').style(f'position: relative; top: -{HEADER_HEIGHT}')
     with ui.column().classes('w-full q-pa-xl'):
         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')
         with ui.row().classes('w-full no-wrap text-lg leading-tight justify-between'):
             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}')
     with ui.column().classes('w-full q-pa-xl'):
         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')
         with ui.row().classes('w-full no-wrap text-lg leading-tight'):
             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')
 
     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'):
             ui.markdown('Why?') \
                 .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
                 <strong><a href="https://justpy.io/">JustPy</a></strong>.
                 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>
-                and 
+                and
                 <strong><a href="https://quasar.dev/">Quasar</a></strong>
                 for the frontend.<br/>
 
@@ -219,7 +246,7 @@ ui.run()
                 which itself is based on the ASGI framework
                 <strong><a href="https://www.starlette.io/">Starlette</a></strong>,
                 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')
 
         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
 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]]
 name = "kiwisolver"
 version = "1.4.4"
@@ -693,7 +701,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "selenium"
-version = "4.7.0"
+version = "4.7.2"
 description = ""
 category = "dev"
 optional = false
@@ -941,7 +949,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "1fd5c2160bd84817dbeadd7bf67102db075caa635897963771c4cca954682bd1"
+content-hash = "f4f7d8667e1d71c4bd364cabdc219d855492b8b24f69a7fda197c4f194e13e9b"
 
 [metadata.files]
 anyio = [
@@ -1192,6 +1200,10 @@ iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {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 = [
     {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"},
@@ -1592,7 +1604,7 @@ requests = [
     {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
 ]
 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 = [
     {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"
 watchfiles = "^0.18.1"
 
-[tool.poetry.dev-dependencies]
+[tool.poetry.group.dev.dependencies]
 icecream = "^2.1.0"
 autopep8 = "^1.5.7"
 debugpy = "^1.3.0"
@@ -32,6 +32,7 @@ pytest-selenium = "^4.0.0"
 pytest-asyncio = "^0.19.0"
 pytest = "6.2.5"
 replicate = "^0.4.0"
+itsdangerous = "^2.1.2" # required by SessionMiddleware (see https://fastapi.tiangolo.com/?h=itsdangerous#optional-dependencies)
 
 [build-system]
 requires = [

+ 1 - 1
website/constants.py

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