Login to an app via sms - how to link login to the underlying app

Login to an app via password is kind of old school these days. We wrote some Panel code using Twilio that sends a code to a user and when verified logs them in. This standalone Panel app also does a registration request and may eventually link to Stripe, who knows.

The next step is for the user once logged in to see the underlying Panel app let us say it is called “wind_forecast.py”, a reasonably complex app with several tabs. I am looking for a suggestion as to be the best way to do this.

(1) have a single app and when login is complete a different “forecast_view” is presented
(2) the login app opens the forecast app on the same port (firewall issues) and closes itself
(3) the login app is the template for the panel serve command
(4) some other solution that I don’t appreciate.

We, like any amateurs, are keen to launch our “product” and so time presses to sort this out.
We don’t expect many users 30 would way exceed expectations.

Good question! Can you post the code of the 1st app you mention (the stand alone app) here?

Have you seen the post below and looked around in the Panel Discord server?

Also

Here is the code. It’s pretty rough and I will improve it. Realised I posted a much earlier version. Still getting used to Github, so new I didn’t even use it here. On successful login it goes to a dummy tab which could be the main app I guess although it would require keeping a number of tabs hidden to start with. I’d prefer to have this app run as part of panel serve for the main app. I’ll study the example again.

The main app written by my colleague does a good job of demonstrating Holoviz and Panel and demonstrates to me why I will stick to finance and marketing and leaving coding to someone that can.

import panel as pn
import param
import pandas as pd
import random
import os
from twilio.rest import Client
import logging


# Initialize Panel
pn.extension('tabulator')

# Twilio configuration
TWILIO_ACCOUNT_SID = t_account
TWILIO_AUTH_TOKEN = t_token
TWILIO_PHONE_NUMBER = t_phone



client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

CREDENTIALS_FILE = credentials.csv"
# Global variables
global current_view, current_user, current_code, admin_logged_in, user_logged_in, debug_message, user_df

user_logged_in = False
debug_message = "no time for coffee"
def load_user_df():
    try:
        df = pd.read_csv(CREDENTIALS_FILE, dtype={
            'name': str,
            'email': str,
            'phone': str,  # Ensure phone is read as string
            'organization': str,
            'approved': bool,
            'is_admin': bool
        })
        return df
    except FileNotFoundError:
        return pd.DataFrame(columns=['name', 'email', 'phone', 'organization', 'approved', 'is_admin'])

def save_user_df(df):
    # Ensure the phone column is saved as text
    df['phone'] = df['phone'].astype(str)
    df.to_csv(CREDENTIALS_FILE, index=False)
# Load user data
user_df = load_user_df()

# Create widgets
main_login_button = pn.widgets.Button(name="Login", button_type="primary")
main_register_button = pn.widgets.Button(name="Register", button_type="success")


login_email_input = pn.widgets.TextInput(name="Email", placeholder="Enter your email")
login_submit_button = pn.widgets.Button(name="Send Verification Code", button_type="primary")
login_code_input = pn.widgets.TextInput(name="Verification Code", placeholder="Enter 3-digit code")
login_code_submit_button = pn.widgets.Button(name="Verify Code", button_type="success")

reg_name_input = pn.widgets.TextInput(name="Name", placeholder="Enter your name")
reg_email_input = pn.widgets.TextInput(name="Email", placeholder="Enter your email")
reg_phone_input = pn.widgets.TextInput(name="Mobile Phone", placeholder="Enter your mobile phone number", value="+61")
reg_org_input = pn.widgets.TextInput(name="Organization", placeholder="Enter your organization")
reg_submit_button = pn.widgets.Button(name="Submit Registration", button_type="success")




message_pane = pn.pane.Markdown("")
debug_pane = pn.pane.Markdown("Debug Info: ")

def main_view():
    return pn.Column(
        pn.pane.Markdown("# User System"),
        main_login_button,
        main_register_button,
        message_pane
    )

def login_view():
    return pn.Column(
        pn.pane.Markdown("# User Login"),
        login_email_input,
        login_submit_button,
        login_code_input,
        login_code_submit_button,
        message_pane,
        # debug_pane
    )

def register_view():
    return pn.Column(
        pn.pane.Markdown("# User Registration"),
        reg_name_input,
        reg_email_input,
        reg_phone_input,
        reg_org_input,
        reg_submit_button,
        message_pane
    )

def pricing_model_view():
    return pn.Column(
        pn.pane.Markdown("# Pricing Model", styles={'color': 'white', 'background-color': 'teal', 'font-size': '24px', 'padding': '10px'}),
        pn.pane.Markdown("Logged In", styles={'color': 'white', 'background-color': 'teal', 'font-size': '24px', 'padding': '10px'})
    )



# Create all tabs, but set pricing_model to not visible initially
main_tab = pn.Column(main_view())
login_tab = pn.Column(login_view(), visible=False, name="login")
register_tab = pn.Column(register_view(), visible=False, name="register")
pricing_model_tab = pn.Column(pricing_model_view(), visible=False, name="pricing_model")

tabs = pn.Column(main_tab, login_tab, register_tab, pricing_model_tab)

def show_main_view(event):
    global current_view
    current_view = "main"
    update_view()
    
return_to_main_button_login = pn.widgets.Button(name="Return to Main", button_type="default")
return_to_main_button_login.on_click(show_main_view)
login_tab.append(return_to_main_button_login)

return_to_main_button_register = pn.widgets.Button(name="Return to Main", button_type="default")
return_to_main_button_register.on_click(show_main_view)
register_tab.append(return_to_main_button_register)

def update_view():
    global current_view, user_logged_in
    main_tab.visible = current_view == "main"
    login_tab.visible = current_view == "login"
    register_tab.visible = current_view == "register"
    pricing_model_tab.visible = current_view == "pricing_model" and user_logged_in

def generate_code():
    return str(random.randint(100, 999))

def send_code(phone, code):
    try:
        message = client.messages.create(
            body=f"Your verification code is: {code}",
            from_=TWILIO_PHONE_NUMBER,
            to=phone
        )
        print(f"SMS sent successfully. SID: {message.sid}")
        return True
    except Exception as e:
        print(f"Failed to send SMS: {str(e)}")
        return False

def show_login_view(event):
    global current_view
    current_view = "login"
    update_view()

def show_register_view(event):
    global current_view
    current_view = "register"
    update_view()


def handle_login_email_submit(event):
    global current_user, current_code, debug_message
    email = login_email_input.value
    user = user_df[user_df['email'] == email]
    if not user.empty:
        if user['approved'].values[0]:
            current_user = user.to_dict('records')[0]
            current_code = generate_code()
            if send_code(current_user['phone'], current_code):
                message_pane.object = "A verification code has been sent to your phone."
                login_code_input.visible = True
                login_code_submit_button.visible = True
            else:
                message_pane.object = "Failed to send verification code. Please try again later."
        else:
            message_pane.object = "Your account is pending approval. Please try again later."
    else:
        message_pane.object = "User not found. Please check your email or register."
    
    debug_message += f"\nEmail submit: {email}, Code: {current_code}"
    debug_pane.object = f"Debug Info: {debug_message}"

def handle_login_code_submit(event):
    global user_logged_in, debug_message, current_view
    entered_code = login_code_input.value
    debug_message += f"\nCode submit: entered {entered_code}, expected {current_code}"
    
    if entered_code == current_code:
        user_logged_in = True
        success_message = f"Login successful! Welcome, {current_user['name']}."
        message_pane.object = success_message
        debug_message += f"\nLogin successful for {current_user['name']}"
        current_view = "pricing_model"
        update_view()
    else:
        message_pane.object = "Incorrect code. Please try again."
        debug_message += "\nIncorrect code entered"
    
    debug_pane.object = f"Debug Info: {debug_message}"

import re

def validate_phone_number(phone):
    pattern = r'^\+\d{1,4}[.\-\s]?\d+$'
    return re.match(pattern, phone) is not None



def handle_registration(event):
    global user_df, current_view
    name = reg_name_input.value
    email = reg_email_input.value
    phone = reg_phone_input.value
    organization = reg_org_input.value
    
    # Validate phone number
    if not validate_phone_number(phone):
        message_pane.object = "Invalid phone number. Please include a '+' and country code (e.g., +61412345678)."
        return
    
    new_user = pd.DataFrame({
        'name': [name],
        'email': [email],
        'phone': [phone],
        'organization': [organization],
        'approved': [False],
        'is_admin': [False]
    })
    
    user_df = pd.concat([user_df, new_user], ignore_index=True)
    save_user_df(user_df)
    message_pane.object = "Registration submitted. Please wait for admin approval."
    current_view = "register"
    update_view()



# Set up button click handlers
main_login_button.on_click(show_login_view)
main_register_button.on_click(show_register_view)
login_submit_button.on_click(handle_login_email_submit)
login_code_submit_button.on_click(handle_login_code_submit)
reg_submit_button.on_click(handle_registration)

# Create the main layout
main_layout = tabs

# Create the template
template = pn.template.BootstrapTemplate(
    title="ITK pricing registration and login",
    main=[main_layout]
)

template.servable()

So for me the solution was to put the main panel app (forecast.py) as one process and have the twilio based registration/login app on a separate process. Trying to do the whole thing from the one app didn’t go so good for various reasons. On succesful login the user is redirected from the one app to another.

A separate panel app runs user admin. None of the code is worth posting although forecast.py remains an excellent demo of panel and holoviz and our few alpha testers are loving the interface.