# local-auth
Easy access to local authentication in your [Reflex](https://reflex.dev) app.
## Installation
```bash
pip install reflex-local-auth
```
## Usage
```python
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:
```python
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
```console
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.
```python
@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.
```python
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.
```python
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:
```python
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.
```python
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`](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:
```python
op.execute("INSERT INTO localuser SELECT * FROM user;")
op.execute("INSERT INTO localauthsession SELECT * FROM authsession;")
```
Raw data
{
"_id": null,
"home_page": null,
"name": "reflex-local-auth",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "reflex, reflex-custom-components",
"author": null,
"author_email": "Masen Furer <m_github@0x26.net>",
"download_url": "https://files.pythonhosted.org/packages/39/c0/4d8f2f1303a1523d059a3e934153f816d86c3b7eaee699ec4f74ffefa073/reflex_local_auth-0.2.2.tar.gz",
"platform": null,
"description": "# local-auth\n\nEasy access to local authentication in your [Reflex](https://reflex.dev) app.\n\n## Installation\n\n```bash\npip install reflex-local-auth\n```\n\n## Usage\n\n```python\nimport reflex_local_auth\n```\n\n### Add the canned login and registration pages\n\nIf you don't want to create your own login and registration forms, add the canned pages to your app:\n\n```python\napp = rx.App()\n...\napp.add_page(\n reflex_local_auth.pages.login_page,\n route=reflex_local_auth.routes.LOGIN_ROUTE,\n title=\"Login\",\n)\napp.add_page(\n reflex_local_auth.pages.register_page,\n route=reflex_local_auth.routes.REGISTER_ROUTE,\n title=\"Register\",\n)\n```\n\n### Create Database Tables\n\n```console\nreflex db init # if needed\nreflex db makemigrations\nreflex db migrate\n```\n\n### Redirect Pages to Login\n\nUse the `@reflex_local_auth.require_login` decorator to redirect unauthenticated users to the LOGIN_ROUTE.\n\n```python\n@rx.page()\n@reflex_local_auth.require_login\ndef need2login(request):\n return rx.heading(\"Accessing this page will redirect to the login page if not authenticated.\")\n```\n\nAlthough this _seems_ to protect the content, it is still publicly accessible\nwhen viewing the source code for the page! This should be considered a mechanism\nto redirect users to the login page, NOT a way to protect data.\n\n### Protect State\n\nIt is _extremely_ important to protect private data returned by State via Event\nHandlers! All static page data should be considered public, the only data that\ncan truly be considered private at runtime must be fetched by an event handler\nthat checks the authenticated user and assigns the data to a State Var. After\nthe user logs out, the private data should be cleared and the user's tab should\nbe closed to destroy the session identifier.\n\n```python\nimport reflex_local_auth\n\n\nclass ProtectedState(reflex_local_auth.LocalAuthState):\n data: str\n\n def on_load(self):\n if not self.is_authenticated:\n return reflex_local_auth.LoginState.redir\n self.data = f\"This is truly private data for {self.authenticated_user.username}\"\n\n def do_logout(self):\n self.data = \"\"\n return reflex_local_auth.LocalAuthState.do_logout\n\n\n@rx.page(on_load=ProtectedState.on_load)\n@reflex_local_auth.require_login\ndef protected_page():\n return rx.heading(ProtectedState.data)\n```\n\n## Customization\n\nThe basic `reflex_local_auth.LocalUser` model provides password hashing and\nverification, and an enabled flag. Additional functionality can be added by\ncreating a new `UserInfo` model and creating a foreign key relationship to the\n`user.id` field.\n\n```python\nimport sqlmodel\nimport reflex as rx\nimport reflex_local_auth\n\n\nclass UserInfo(rx.Model, table=True):\n email: str\n is_admin: bool = False\n created_from_ip: str\n\n user_id: int = sqlmodel.Field(foreign_key=\"user.id\")\n```\n\nTo populate the extra fields, you can create a custom registration page and\nstate that asks for the extra info, or it can be added via other event handlers.\n\nA custom registration state and form might look like:\n\n```python\nimport reflex as rx\nimport reflex_local_auth\nfrom reflex_local_auth.pages.components import input_100w, MIN_WIDTH, PADDING_TOP\n\n\nclass MyRegisterState(reflex_local_auth.RegistrationState):\n # This event handler must be named something besides `handle_registration`!!!\n def handle_registration_email(self, form_data):\n registration_result = super().handle_registration(form_data)\n if self.new_user_id >= 0:\n with rx.session() as session:\n session.add(\n UserInfo(\n email=form_data[\"email\"],\n created_from_ip=self.router.headers.get(\n \"x_forwarded_for\",\n self.router.session.client_ip,\n ),\n user_id=self.new_user_id,\n )\n )\n session.commit()\n return registration_result\n\n\ndef register_error() -> rx.Component:\n \"\"\"Render the registration error message.\"\"\"\n return rx.cond(\n reflex_local_auth.RegistrationState.error_message != \"\",\n rx.callout(\n reflex_local_auth.RegistrationState.error_message,\n icon=\"triangle_alert\",\n color_scheme=\"red\",\n role=\"alert\",\n width=\"100%\",\n ),\n )\n\n\ndef register_form() -> rx.Component:\n \"\"\"Render the registration form.\"\"\"\n return rx.form(\n rx.vstack(\n rx.heading(\"Create an account\", size=\"7\"),\n register_error(),\n rx.text(\"Username\"),\n input_100w(\"username\"),\n rx.text(\"Email\"),\n input_100w(\"email\"),\n rx.text(\"Password\"),\n input_100w(\"password\", type=\"password\"),\n rx.text(\"Confirm Password\"),\n input_100w(\"confirm_password\", type=\"password\"),\n rx.button(\"Sign up\", width=\"100%\"),\n rx.center(\n rx.link(\"Login\", on_click=lambda: rx.redirect(reflex_local_auth.routes.LOGIN_ROUTE)),\n width=\"100%\",\n ),\n min_width=MIN_WIDTH,\n ),\n on_submit=MyRegisterState.handle_registration_email,\n )\n\n\ndef register_page() -> rx.Component:\n \"\"\"Render the registration page.\n\n Returns:\n A reflex component.\n \"\"\"\n\n return rx.center(\n rx.cond(\n reflex_local_auth.RegistrationState.success,\n rx.vstack(\n rx.text(\"Registration successful!\"),\n ),\n rx.card(register_form()),\n ),\n padding_top=PADDING_TOP,\n )\n```\n\n\nFinally you can create a substate of `reflex_local_auth.LocalAuthState` which fetches\nthe associated `UserInfo` record and makes it available to your app.\n\n```python\nimport sqlmodel\nimport reflex as rx\nimport reflex_local_auth\n\n\nclass MyLocalAuthState(reflex_local_auth.LocalAuthState):\n @rx.cached_var\n def authenticated_user_info(self) -> UserInfo | None:\n if self.authenticated_user.id < 0:\n return\n with rx.session() as session:\n return session.exec(\n sqlmodel.select(UserInfo).where(\n UserInfo.user_id == self.authenticated_user.id\n ),\n ).one_or_none()\n\n\n@rx.page()\n@reflex_local_auth.require_login\ndef user_info():\n return rx.vstack(\n rx.text(f\"Username: {MyLocalAuthState.authenticated_user.username}\"),\n rx.cond(\n MyLocalAuthState.authenticated_user_info,\n rx.fragment(\n rx.text(f\"Email: {MyLocalAuthState.authenticated_user_info.email}\"),\n rx.text(f\"Account Created From: {MyLocalAuthState.authenticated_user_info.created_from_ip}\"),\n ),\n ),\n )\n```\n\n## Migrating from 0.0.x to 0.1.x\n\nThe `User` model has been renamed to `LocalUser` and the `AuthSession` model has\nbeen renamed to `LocalAuthSession`. If your app was using reflex-local-auth 0.0.x,\nthen you will need to make manual changes to migration script to copy existing user\ndata into the new tables _after_ running `reflex db makemigrations`.\n\nSee [`local_auth_demo/alembic/version/cb01e050df85_.py`](local_auth_demo/alembic/version/cb01e050df85_.py) for an example migration script.\n\nImportantly, your `upgrade` function should include the following lines, after creating\nthe new tables and before dropping the old tables:\n\n```python\n op.execute(\"INSERT INTO localuser SELECT * FROM user;\")\n op.execute(\"INSERT INTO localauthsession SELECT * FROM authsession;\")\n```\n",
"bugtrack_url": null,
"license": "Apache-2.0",
"summary": "Local DB user authentication for Reflex apps",
"version": "0.2.2",
"project_urls": {
"Homepage": "https://github.com/masenf/reflex-local-auth"
},
"split_keywords": [
"reflex",
" reflex-custom-components"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "70028ecaf032e1b5ee351e2a2dd624559377d253c2a5280f7564bca6c1936fef",
"md5": "dabdec7abfb464e704b2191e78eec4a5",
"sha256": "9eba898cb9d5e53b2cbe7b56a56790c25dba58d28463411ed4729ce3af6fa729"
},
"downloads": -1,
"filename": "reflex_local_auth-0.2.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "dabdec7abfb464e704b2191e78eec4a5",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 11818,
"upload_time": "2024-08-07T18:47:36",
"upload_time_iso_8601": "2024-08-07T18:47:36.311934Z",
"url": "https://files.pythonhosted.org/packages/70/02/8ecaf032e1b5ee351e2a2dd624559377d253c2a5280f7564bca6c1936fef/reflex_local_auth-0.2.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "39c04d8f2f1303a1523d059a3e934153f816d86c3b7eaee699ec4f74ffefa073",
"md5": "c94991f1425943cf3b4fa4a935c6db8e",
"sha256": "56efc191343b0a07e01c92e8a23c5047a22aa269d21cef1bd48d96c48ada1b07"
},
"downloads": -1,
"filename": "reflex_local_auth-0.2.2.tar.gz",
"has_sig": false,
"md5_digest": "c94991f1425943cf3b4fa4a935c6db8e",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 10028,
"upload_time": "2024-08-07T18:47:39",
"upload_time_iso_8601": "2024-08-07T18:47:39.030448Z",
"url": "https://files.pythonhosted.org/packages/39/c0/4d8f2f1303a1523d059a3e934153f816d86c3b7eaee699ec4f74ffefa073/reflex_local_auth-0.2.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-08-07 18:47:39",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "masenf",
"github_project": "reflex-local-auth",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "reflex-local-auth"
}