Elastic D&D - Update 2 - Streamlit & the Login Page
Tags: python, elasticsearch
Published: 2023-09-01 19:49:10

Last week I mentioned that I would begin breaking down the code of the Streamlit application. However, the post would be massive, so I will be digging into smaller parts of it over the next few weeks! Enjoy!

## Streamlit
[Streamlit](https://streamlit.io/) is a Python module that turns code into shareable web applications. I was quite surprised at how easy it was to pick this up and create an entire front-end to my data. Some Python experience is required for sure, but the learning curve on this specific module is not bad. They have fantastic documentation, and the [cheat sheet](https://docs.streamlit.io/library/cheatsheet) will be your best friend. I promise.

## Streamlit-Authenticator
[Streamlit-Authenticator](https://github.com/mkhorasani/Streamlit-Authenticator) is a secure authentication module to validate user credentials in a Streamlit application. There is a fantastic 2-part blog series written by Mohammad Khorasani, developer of this package, that illustrates how to use this module. You can find them [here](https://blog.streamlit.io/streamlit-authenticator-part-1-adding-an-authentication-component-to-your-app/) and [here](https://blog.streamlit.io/streamlit-authenticator-part-2-adding-advanced-features-to-your-authentication-component/). I believe I ended up using pieces from both parts -- in fact, the entirety of the Login page would not have been possible without his code. Thank you Mohammad!

## Building a Login Page
### Creating the Authentication YAML File
Place _streamlit-auth.yaml_ in the same directory that you are running your Streamlit application from:
```
cookie:
  expiry_days: 0
  key: random_signature_key
  name: random_cookie_name
credentials:
  usernames:
    player1:
      email: player1@dnd.com
      name: Player 1
      password: # paste hash from generate_password.py
preauthorized:
  emails:
  - player2@dnd.com
  - player3@dnd.com
  - player4@dnd.com
  - player5@dnd.com
```
Use [this](https://github.com/thtmexicnkid/elastic-dnd/blob/main/generate_password.py) to generate a hashed password to paste into your YAML. The list of email addressed under "preauthorized" allows you to control who is allowed to register via the registration page.

_**NOTE**_:
> Streamlit-Authenticator throws errors if there is not at least 1 username in the YAML configuration. I hard-coded myself into the YAML and let everyone else use the registration page.

### Coding the Login Page
**_NOTE:_**
> Because I used Streamlit's built-in session state, there is a need to "initialize" variables, which allows session information to persist across pages. I built the "initialize_session_state" function to implement this in a cleaner fashion.

> You can set the value of a variable in the session state to that of an existing function in your code. I used this throughout to display pages and note input forms.

> `st.experimental_rerun()` is used to control the flow of your application; running your script from the beginning to reflect changes in session state, variables, etc.

Before we reach the login page, we need to initialize the username variable and load the authentication YAML file into the environment. From there, I display a banner and go right into the page display logic.
```
### PROGRAM ###

# initializes session state and loads login authentication configuration
initialize_session_state(["username"])
config, authenticator = load_yml()

# displays application title and sets page accordingly
display_image("banner.png")
if not st.session_state.username:
    st.session_state.runpage = app_page1
    st.session_state.runpage()
else:
    st.session_state.runpage = app_page2
    st.session_state.runpage()
```
Because a username isn't set via the login page yet, the following function will run, displaying the login page:
```
def app_page1():
    # displays login and registration widgets
    tab1, tab2 = st.tabs(["Login", "Register"])
    # login tab
    with tab1:
        try:
            name,authentication_status,username = authenticator.login("Login","main")
            if authentication_status:
                st.session_state.runpage = "app_page2"
                st.experimental_rerun()
            elif authentication_status == False:
                st.error('Username/password is incorrect')
            elif authentication_status == None:
                st.warning('Please enter your username and password')
        except:
            pass
    # registration tab
    with tab2:
        try:
            if authenticator.register_user('Register', preauthorization=True):
                success('User registered successfully')
                update_yml()
        except Exception as e:
            error_message(e)
```
This page consist of 2 tabs, Login and Register, used for authenticating into the rest of the application and registering a new user for authentication respectively.

#### Login Tab
This tab displays a login widget, consisting of a username input, password input, and a submit button, which set values for variables in the session state. One of three things happens in this tab:
1. If "authentication_status" is empty, you will be prompted to enter your information.
2. After hitting submit, if your information matches something in the YAML, the "app_page2" function is run, taking you to the rest of the application.
3. After hitting submit, if your information _doesn't_ match something in the YAML, an error message will appear.

```
# login tab
    with tab1:
        try:
            name,authentication_status,username = authenticator.login("Login","main")
            if authentication_status:
                st.session_state.runpage = "app_page2"
                st.experimental_rerun()
            elif authentication_status == False:
                error_message('Username/password is incorrect')
            elif authentication_status == None:
                st.warning('Please enter your username and password')
        except:
            pass
```

#### Register Tab
This tab displays a registration widget, consisting of the fields required to be set in the YAML and a submit button. Once submitted, if there is something wrong with your input, an error message will appear. If successful, the YAML file will update.

```
# registration tab
    with tab2:
        try:
            if authenticator.register_user('Register', preauthorization=True):
                success('User registered successfully')
                update_yml()
        except Exception as e:
            error_message(e)
```

### Related Functions
```
def error_message(text):
    # displays error message
    import time
    
    error = st.error(text)
    time.sleep(1)
    error.empty()
```
```
def initialize_session_state(variable_list):
    # creates empty variables in streamlit session state
    for variable in variable_list:
        if variable not in st.session_state:
            st.session_state[variable] = None
```
```
def load_yml():
    # loads login authentication configuration
    import yaml
    from yaml.loader import SafeLoader
    
    with open(streamlit_path + "streamlit-auth.yaml") as file:
        config = yaml.load(file, Loader=SafeLoader)

    authenticator = stauth.Authenticate(
        config['credentials'],
        config['cookie']['name'],
        config['cookie']['key'],
        config['cookie']['expiry_days'],
        config['preauthorized']
    )
    
    return config, authenticator
```
```
def update_yml():
    # updates login authentication configuration file
    import yaml

    with open(streamlit_path + "streamlit-auth.yaml", 'w') as file:
        yaml.dump(config, file, default_flow_style=False)
```

## Closing Remarks
Next week, I will start digging into app_page2 and what it can accomplish! 

Check out the GitHub repo below. You can also find my Twitch account in the socials link, where I will be actively working on this during the week while interacting with whoever is hanging out!

[GitHub Repo](https://github.com/thtmexicnkid/elastic-dnd)
[Socials](https://allmylinks.com/thtmexicnkid)

Happy Coding,
Joe