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 = super().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.

import sqlmodel
import reflex as rx
import reflex_local_auth


class MyLocalAuthState(reflex_local_auth.LocalAuthState):
    @rx.cached_var
    def authenticated_user_info(self) -> UserInfo | None:
        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.1.0.tar.gz (10.0 kB view details)

Uploaded Source

Built Distribution

reflex_local_auth-0.1.0-py3-none-any.whl (11.8 kB view details)

Uploaded Python 3

File details

Details for the file reflex-local-auth-0.1.0.tar.gz.

File metadata

  • Download URL: reflex-local-auth-0.1.0.tar.gz
  • Upload date:
  • Size: 10.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.0.0 CPython/3.12.2

File hashes

Hashes for reflex-local-auth-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bdd05162121e8cae966042e59ed61ecb0c1a3d17ce12bceeec0c48a7da8c2b80
MD5 4f04276d42210c5040d8f1262d3f8d0c
BLAKE2b-256 5a0574b14cd4f0a5155cef1bd2fa933bdbd6b2a689d9a60ec7f91c156bdffc08

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for reflex_local_auth-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4b358790eec10a635a8b16f609f0e8ea4e214456ff1aa1ccfc4ae1d21c4271d1
MD5 08718f086fc44e8de71ebf068e1c2b49
BLAKE2b-256 3aa9153a18f9bfebd616de6d10477beb94eb1cf5458f52dc74ca9f87813662dc

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 Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page