Skip to content

All The Coding

Let’s talk about code

Categories

Liz Elliott

About Liz

Liz Elliott is a Senior Software Engineer with 15 years plus experience. Before she was a Software Engineer, she was a Web Developer, and before that (briefly) a Space Engineer. Oh, and she’s a bit of a Star Wars fan. This blog is an attempt to combine her enthusiasm for coding with her love of writing.

Subscribe

Subscribe for FREE to receive notifications when new posts are published
Loading

Recent Posts

  • You Already Know How to Code: Part 6 April 23, 2024
  • You Already Know How to Code: Part 5 April 14, 2024
  • You Already Know How to Code: Part 4 April 6, 2024
  • Creating an API with python: Part 13: Pagination March 22, 2023
  • Experimenting with PyScript January 23, 2023

Recent Comments

  • Creating an API with python: Part 9: Authentication Scopes – All The Coding on Creating an API with python: Part 7: CORS
  • Creating an API with python: Part 10: Integration Tests – All The Coding on Creating an API with python: Part 1: GET Endpoints
  • Creating an API with python: Part 9: Authentication Scopes – All The Coding on Creating an API with python: Part 3: POST Endpoints
  • Creating an API with python: Part 10: Integration Tests – All The Coding on Creating an API with python: Part 9: Authentication Scopes
  • Creating an API with python: Part 9: Authentication Scopes – All The Coding on Creating an API with python: Part 2: MariaDB Database
  • Home
  • Categories
  • Other Projects

Experimenting with PyScript

January 23, 2023 python

I’m not a front-end coder. I prefer the back-end. So when I heard that there was a new front-end scripting language, PyScript, based on python, my back-end programming language of choice, I was curious. Would I now be able to create interactive front-ends with my existing python knowledge? I had to give it a try. Now, every front-end needs a back-end to talk to, and it just so happened that I’d just built a whole API… so for my first experiment, I thought I’d try creating a front-end page with PyScript that logged in to my python API (i.e. retrieved a token from the /token endpoint). What follows is the result…

Prerequisites

I’m using the FastAPI API for my backend, as demonstrated in my Creating an API with python series. If you want to use that too, you’ll need to set it up locally. See https://github.com/liz-allthecoding/taglink-api.

Step 1: Add an html directory

On my local virtual machine, I created a new directory to contain the html page:

$ sudo mkdir -p /usr/share/html/web

Step 2: Update the nginx config

I then updated my existing nginx config for the FastAPI api, adding a /web location:

  1. $ sudo su
    $ cd /etc/nginx/conf.d
  2. I added this snippet to api.conf:
     location ^~/web/ {
                                    root /usr/share/html;
          }
    
  3. The whole file then became:
    server {
            listen       443 ssl http2;
            listen       [::]:443 ssl http2;
            server_name  _;
    
            ssl_certificate    /etc/ssl/certs/server.crt;
            ssl_certificate_key    /etc/ssl/certs/server.key;
            ssl_session_cache shared:SSL:1m;
            ssl_session_timeout  10m;
            ssl_ciphers HIGH:!aNULL:!MD5;
            ssl_prefer_server_ciphers on;
    
            access_log /var/log/nginx/nginx.vhost.access.log;
            error_log /var/log/nginx/nginx.vhost.error.log;
    
                    location ^~/api/ {
                                    rewrite ^/api/(.*)$ /$1 break;
                                    proxy_pass  http://127.0.0.1:8000;
                                    proxy_set_header Host $host;
                                    proxy_set_header X-Real-IP $remote_addr;
                                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                                    proxy_set_header X-Forwarded-Proto $scheme;
                                    proxy_set_header X-Request-Id $http_x_request_id;
                                    proxy_set_header X-Cert-Issuer-DN $ssl_client_i_dn;
                                    proxy_set_header X-Cert-Subject-DN $ssl_client_s_dn;
                    }
    
                    location ^~/web/ {
                                    root /usr/share/html;
                    }
    
        }
    
    server {
            listen       80;
            listen       [::]:80;
            server_name  _;
            return 301 https://$host$request_uri;
        }
    
  4. I then restarted nginx:
    $ systemctl restart nginx

Step 3: Create the HTML Login Page

The login page first consists of a simple html form with minimal styling.

  1. I created a file called index.html and add it in the /usr/share/html/web directory with the following contents:
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>FastAPI Login</title>
      <style>
        .hidden {
            visibility: hidden;
        }
        .visible {
            visibility: visible;
        }
      </style>
    </head>
    <body>
      <div id="login-form" class="visible">
        <p>Please enter your login details</p>
        <p>Email: <input id="email" type="text" placeholder="joe@blogs.com"></p>
        <p>Password: <input id="password" type="password"></p>
        <button>Login</button>
      </div>
      <div id="login-success" class="hidden">
          <p>Login successful!</p>
      </div>
      <div id="login-failed" class="hidden">
          <p>Login failed!</p>
      </div>
    </body>
    </html>
    
    
  2. I then added links to the PyScript styles and code source.
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
      <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    
  3. Next, I added the PyScript functions for an “on click” event and for calling the API (note I replaced <MY_IP> with my server’s IP):
    from pyodide import create_proxy
    
    async def on_add_click(event):
        email = document.querySelector("#email")
        password = document.querySelector("#password")
    
        if email.value and password.value:
            token = await loop.run_until_complete(
                login(email.value, password.value, "admin")
            )
            print(token)
            if token is not None:
                if token != "Error logging in":
                    p = document.getElementById("login-success")
                    p.classList.replace("hidden", "visible")
                    p = document.getElementById("login-failed")
                    p.classList.replace("visible", "hidden")
                    p = document.getElementById("login-form")
                    p.classList.replace("visible", "hidden")
                    return
            p = document.getElementById("login-failed")
            p.classList.replace("hidden", "visible")
            p = document.getElementById("login-success")
            p.classList.replace("visible", "hidden")
    
    
    button = document.querySelector("button")
    button.addEventListener("click", create_proxy(on_add_click))
    
    # Run this using "asyncio"
    
    import json
    import urllib
    
    from pyodide.http import pyfetch
    from pyodide import JsException
    
    async def login(email, password, scope):
        headers = {'Content-Type': 'application/x-www-form-urlencoded', 'accept': 'application/json'}
        body_dict = {"username": email, "password": password, "scope": scope, "grant_type": "", "client_id": "", "client_secret": ""}
        body = urllib.parse.urlencode(body_dict)
        try:
            response = await pyfetch(
                url="https://<MY_IP>/api/token/",
                method="POST",
                headers=headers,
                body=body
            )
            if response.ok:
                data = await response.json()
                return data.get("access_token")
        except JsException:
            return "Error logging in"
    
    Note that this is a strange hybrid mix of python and javascript. Meet PyScript!
  4. Finally, putting it all together:
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>FastAPI Login</title>
      <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
      <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
      <style>
        .hidden {
            visibility: hidden;
        }
        .visible {
            visibility: visible;
        }
      </style>
    </head>
    <body>
      <div id="login-form" class="visible">
        <p>Please enter your login details</p>
        <p>Email: <input id="email" type="text" placeholder="joe@blogs.com"></p>
        <p>Password: <input id="password" type="password"></p>
        <button>Login</button>
      </div>
      <div id="login-success" class="hidden">
          <p>Login successful!</p>
      </div>
      <div id="login-failed" class="hidden">
          <p>Login failed!</p>
      </div>
      <py-script>
    from pyodide import create_proxy
    
    async def on_add_click(event):
        email = document.querySelector("#email")
        password = document.querySelector("#password")
    
        if email.value and password.value:
            token = await loop.run_until_complete(
                login(email.value, password.value, "admin")
            )
            print(token)
            if token is not None:
                if token != "Error logging in":
                    p = document.getElementById("login-success")
                    p.classList.replace("hidden", "visible")
                    p = document.getElementById("login-failed")
                    p.classList.replace("visible", "hidden")
                    p = document.getElementById("login-form")
                    p.classList.replace("visible", "hidden")
                    return
            p = document.getElementById("login-failed")
            p.classList.replace("hidden", "visible")
            p = document.getElementById("login-success")
            p.classList.replace("visible", "hidden")
    
    
    button = document.querySelector("button")
    button.addEventListener("click", create_proxy(on_add_click))
    
    # Run this using "asyncio"
    
    import json
    import urllib
    
    from pyodide.http import pyfetch
    from pyodide import JsException
    
    async def login(email, password, scope):
        headers = {'Content-Type': 'application/x-www-form-urlencoded', 'accept': 'application/json'}
        body_dict = {"username": email, "password": password, "scope": scope, "grant_type": "", "client_id": "", "client_secret": ""}
        body = urllib.parse.urlencode(body_dict)
        try:
            response = await pyfetch(
                url="https://<MY_IP>/api/token/",
                method="POST",
                headers=headers,
                body=body
            )
            if response.ok:
                data = await response.json()
                return data.get("access_token")
        except JsException:
            return "Error logging in"
      </py-script>
    </body>
    </html>
    

Step 4: Start the FastAPI Service

I made sure my FastAPI service was running with the command:

$ sudo systemctl start taglink-api

Step 5: Test the Login Page

I was able to view my new login page my navigating to https://<MY_IP>/web/.

It wasn’t terribly pretty, but if I tried entering the user details for a user previously created for my FastAPI API and clicking ‘Login’, I got the message ‘Login successful!’, and if I inspected the Console, I could see my token printed there.

If I refreshed the page and tried logging in with an invalid password, I got the message ‘Login failed!’.

So all working as expected.

Conclusion

My login page is not very pretty and needs some refinement, but as a first foray into PyScript, I’m quite pleased with it. I will have to experiment some more and see what else I can do with it.

Thanks for reading!

Tags: login pagenginx web serverpyfetchpyscriptpyscript frontendpython api frontend

You may also like...

  • Tux (Linux logo)

    Launching a CentOS7 Virtual Machine on Windows10: Part 3: Shared Folder

  • Tux (Linux logo)

    Launching a CentOS7 Virtual Machine on Windows10: Part 4: API Server

  • Aeroplane (Pexels)

    When Logic goes Wrong

  • Next Creating an API with python: Part 13: Pagination
  • Previous Creating an API with python: Part 12: Database Character Encoding and Connections

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Subscribe

Subscribe for FREE to receive notifications when new posts are published
Loading

All The Coding © 2025. All Rights Reserved.

Powered by WordPress. Theme by Alx.