Skip to main content

Local DB user authentication for Reflex apps

Project description

local-auth

Easy access to local authentication in your Reflex app.

Installation

pip install reflex-local-auth

Usage

import reflex_local_auth

Add the canned login and registration pages

If you don't want to create your own login and registration forms, add the canned pages to your app:

app = rx.App()
...
app.add_page(
    reflex_local_auth.pages.login_page,
    route=reflex_local_auth.routes.LOGIN_ROUTE,
    title="Login",
)
app.add_page(
    reflex_local_auth.pages.register_page,
    route=reflex_local_auth.routes.REGISTER_ROUTE,
    title="Register",
)

Create Database Tables

reflex db init  # if needed
reflex db makemigrations
reflex db migrate

Redirect Pages to Login

Use the @reflex_local_auth.require_login decorator to redirect unauthenticated users to the LOGIN_ROUTE.

@rx.page()
@reflex_local_auth.require_login
def need2login(request):
    return rx.heading("Accessing this page will redirect to the login page if not authenticated.")

Although this seems to protect the content, it is still publicly accessible when viewing the source code for the page! This should be considered a mechanism to redirect users to the login page, NOT a way to protect data.

Protect State

It is extremely important to protect private data returned by State via Event Handlers! All static page data should be considered public, the only data that can truly be considered private at runtime must be fetched by an event handler that checks the authenticated user and assigns the data to a State Var. After the user logs out, the private data should be cleared and the user's tab should be closed to destroy the session identifier.

import reflex_local_auth


class ProtectedState(reflex_local_auth.LocalAuthState):
    data: str

    def on_load(self):
        if not self.is_authenticated:
            return reflex_local_auth.LoginState.redir
        self.data = f"This is truly private data for {self.authenticated_user.username}"

    def do_logout(self):
        self.data = ""
        return reflex_local_auth.LocalAuthState.do_logout


@rx.page(on_load=ProtectedState.on_load)
@reflex_local_auth.require_login
def protected_page():
    return rx.heading(ProtectedState.data)

Customization

The basic reflex_local_auth.LocalUser model provides password hashing and verification, and an enabled flag. Additional functionality can be added by creating a new UserInfo model and creating a foreign key relationship to the user.id field.

import sqlmodel
import reflex as rx
import reflex_local_auth


class UserInfo(rx.Model, table=True):
    email: str
    is_admin: bool = False
    created_from_ip: str

    user_id: int = sqlmodel.Field(foreign_key="user.id")

To populate the extra fields, you can create a custom registration page and state that asks for the extra info, or it can be added via other event handlers.

A custom registration state and form might look like:

import reflex as rx
import reflex_local_auth
from reflex_local_auth.pages.components import input_100w, MIN_WIDTH, PADDING_TOP


class MyRegisterState(reflex_local_auth.RegistrationState):
    # This event handler must be named something besides `handle_registration`!!!
    def handle_registration_email(self, form_data):
        registration_result = self.handle_registration(form_data)
        if self.new_user_id >= 0:
            with rx.session() as session:
                session.add(
                    UserInfo(
                        email=form_data["email"],
                        created_from_ip=self.router.headers.get(
                            "x_forwarded_for",
                            self.router.session.client_ip,
                        ),
                        user_id=self.new_user_id,
                    )
                )
                session.commit()
        return registration_result


def register_error() -> rx.Component:
    """Render the registration error message."""
    return rx.cond(
        reflex_local_auth.RegistrationState.error_message != "",
        rx.callout(
            reflex_local_auth.RegistrationState.error_message,
            icon="triangle_alert",
            color_scheme="red",
            role="alert",
            width="100%",
        ),
    )


def register_form() -> rx.Component:
    """Render the registration form."""
    return rx.form(
        rx.vstack(
            rx.heading("Create an account", size="7"),
            register_error(),
            rx.text("Username"),
            input_100w("username"),
            rx.text("Email"),
            input_100w("email"),
            rx.text("Password"),
            input_100w("password", type="password"),
            rx.text("Confirm Password"),
            input_100w("confirm_password", type="password"),
            rx.button("Sign up", width="100%"),
            rx.center(
                rx.link("Login", on_click=lambda: rx.redirect(reflex_local_auth.routes.LOGIN_ROUTE)),
                width="100%",
            ),
            min_width=MIN_WIDTH,
        ),
        on_submit=MyRegisterState.handle_registration_email,
    )


def register_page() -> rx.Component:
    """Render the registration page.

    Returns:
        A reflex component.
    """

    return rx.center(
        rx.cond(
            reflex_local_auth.RegistrationState.success,
            rx.vstack(
                rx.text("Registration successful!"),
            ),
            rx.card(register_form()),
        ),
        padding_top=PADDING_TOP,
    )

Finally you can create a substate of reflex_local_auth.LocalAuthState which fetches the associated UserInfo record and makes it available to your app.

from typing import Optional

import sqlmodel
import reflex as rx
import reflex_local_auth


class MyLocalAuthState(reflex_local_auth.LocalAuthState):
    @rx.var(cache=True)
    def authenticated_user_info(self) -> Optional[UserInfo]:
        if self.authenticated_user.id < 0:
            return
        with rx.session() as session:
            return session.exec(
                sqlmodel.select(UserInfo).where(
                    UserInfo.user_id == self.authenticated_user.id
                ),
            ).one_or_none()


@rx.page()
@reflex_local_auth.require_login
def user_info():
    return rx.vstack(
        rx.text(f"Username: {MyLocalAuthState.authenticated_user.username}"),
        rx.cond(
            MyLocalAuthState.authenticated_user_info,
            rx.fragment(
                rx.text(f"Email: {MyLocalAuthState.authenticated_user_info.email}"),
                rx.text(f"Account Created From: {MyLocalAuthState.authenticated_user_info.created_from_ip}"),
            ),
        ),
    )

Migrating from 0.0.x to 0.1.x

The User model has been renamed to LocalUser and the AuthSession model has been renamed to LocalAuthSession. If your app was using reflex-local-auth 0.0.x, then you will need to make manual changes to migration script to copy existing user data into the new tables after running reflex db makemigrations.

See local_auth_demo/alembic/version/cb01e050df85_.py for an example migration script.

Importantly, your upgrade function should include the following lines, after creating the new tables and before dropping the old tables:

    op.execute("INSERT INTO localuser SELECT * FROM user;")
    op.execute("INSERT INTO localauthsession SELECT * FROM authsession;")

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reflex_local_auth-0.3.0.tar.gz (19.7 kB view details)

Uploaded Source

Built Distribution

reflex_local_auth-0.3.0-py3-none-any.whl (12.1 kB view details)

Uploaded Python 3

File details

Details for the file reflex_local_auth-0.3.0.tar.gz.

File metadata

  • Download URL: reflex_local_auth-0.3.0.tar.gz
  • Upload date:
  • Size: 19.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.8

File hashes

Hashes for reflex_local_auth-0.3.0.tar.gz
Algorithm Hash digest
SHA256 2e630d8f32c5b2c15ecbb8e3519cdde920d79c720dbe843110437b3a718f8ec0
MD5 0b1592ec0a4d6ed1430dceb4e35be75b
BLAKE2b-256 7e788ef7227a6d7df3e9b80ed1e84c8b682a2b5627575417e77ab939e220b8d7

See more details on using hashes here.

File details

Details for the file reflex_local_auth-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for reflex_local_auth-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 708083afde008d265677c601986f0301af91c399ff41d14a199e050cb3e5939d
MD5 4cb0faa94b246c1a94944764457beacb
BLAKE2b-256 d14a77f50b8610059f31365d25a5f8d3fe7500ea1b35792d935fb01f8bfde585

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page