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.2.2.tar.gz (10.0 kB view details)

Uploaded Source

Built Distribution

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

Uploaded Python 3

File details

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

File metadata

  • Download URL: reflex_local_auth-0.2.2.tar.gz
  • Upload date:
  • Size: 10.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.12.4

File hashes

Hashes for reflex_local_auth-0.2.2.tar.gz
Algorithm Hash digest
SHA256 56efc191343b0a07e01c92e8a23c5047a22aa269d21cef1bd48d96c48ada1b07
MD5 c94991f1425943cf3b4fa4a935c6db8e
BLAKE2b-256 39c04d8f2f1303a1523d059a3e934153f816d86c3b7eaee699ec4f74ffefa073

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for reflex_local_auth-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9eba898cb9d5e53b2cbe7b56a56790c25dba58d28463411ed4729ce3af6fa729
MD5 dabdec7abfb464e704b2191e78eec4a5
BLAKE2b-256 70028ecaf032e1b5ee351e2a2dd624559377d253c2a5280f7564bca6c1936fef

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