mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-10 23:59:10 -06:00
auth example improvements
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
from .footer import Footer as Footer
|
||||
from .navbar import Navbar as Navbar
|
||||
from .news_article import NewsArticle as NewsArticle
|
||||
from .no_such_page import NoSuchPage as NoSuchPage
|
||||
from .root_component import RootComponent as RootComponent
|
||||
from .testimonial import Testimonial as Testimonial
|
||||
from .user_sign_up_form import UserSignUpForm as UserSignUpForm
|
||||
|
||||
@@ -47,8 +47,8 @@ class Navbar(rio.Component):
|
||||
self.session.detach(data_models.AppUser)
|
||||
self.session.detach(data_models.UserSession)
|
||||
|
||||
# Navigate to the login page, since login page is our root page we need to navigate
|
||||
# to "/" as the root page
|
||||
# Navigate to the login page to prevent the user being on a page that is
|
||||
# prohibited without being logged in.
|
||||
self.session.navigate_to("/")
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
@@ -56,27 +56,29 @@ class Navbar(rio.Component):
|
||||
# Which page is currently active? This will be used to highlight the
|
||||
# correct navigation button.
|
||||
#
|
||||
# `active_page_instances` contains the same `rio.Page` instances that
|
||||
# you've passed the app during creation. Since multiple pages can be
|
||||
# active at a time (e.g. /foo/bar/baz), this is a list.
|
||||
# `active_page_instances` contains the same `rio.Page` instances
|
||||
# that you've passed the app during creation. Since multiple pages
|
||||
# can be active at a time (e.g. /foo/bar/baz), this is a list.
|
||||
active_page = self.session.active_page_instances[1]
|
||||
active_page_url_segment = active_page.url_segment
|
||||
except IndexError:
|
||||
# Handle the case where there are no active pages. e.g. when the user is
|
||||
# not logged in.
|
||||
# Handle the case where there are no active sub-pages. e.g. when the
|
||||
# user is not logged in.
|
||||
active_page_url_segment = None
|
||||
# You might want to log this or handle it in another way
|
||||
# For example, you could set a default value or raise a custom exception
|
||||
# logging.warning("No active page instances found.")
|
||||
|
||||
# Check if the user is logged in and display the appropriate buttons based on
|
||||
# the user's status
|
||||
# Check if the user is logged in and display the appropriate buttons
|
||||
# based on the user's status
|
||||
try:
|
||||
self.session[data_models.AppUser]
|
||||
user_settings = True
|
||||
|
||||
# If no user is attached, nobody is logged in
|
||||
except KeyError:
|
||||
user_settings = False
|
||||
|
||||
# If a user is attached, they are logged in
|
||||
else:
|
||||
user_settings = True
|
||||
|
||||
# The navbar should appear above all other components. This is easily
|
||||
# done by using a `rio.Overlay` component.
|
||||
return rio.Overlay(
|
||||
@@ -170,17 +172,18 @@ class Navbar(rio.Component):
|
||||
spacing=1,
|
||||
margin=1,
|
||||
),
|
||||
# Set the fill of the rectangle to the neutral color of the theme and
|
||||
# Add a corner radius
|
||||
# Set the fill of the rectangle to the neutral color of the
|
||||
# theme
|
||||
fill=self.session.theme.neutral_color,
|
||||
# Round the corners
|
||||
corner_radius=self.session.theme.corner_radius_medium,
|
||||
# Add shadow properties
|
||||
# Add a shadow to make the navbar stand out above other content
|
||||
shadow_radius=0.8,
|
||||
shadow_color=self.session.theme.shadow_color,
|
||||
shadow_offset_y=0.2,
|
||||
# Overlay assigns the entire screen to its child component.
|
||||
# Since the navbar isn't supposed to take up all space, assign
|
||||
# an alignment.
|
||||
# Since the navbar isn't supposed to take up all space, align
|
||||
# it.
|
||||
align_y=0,
|
||||
margin_x=5,
|
||||
margin_y=2,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import rio
|
||||
|
||||
|
||||
# <component>
|
||||
class NoSuchPage(rio.Component):
|
||||
"""
|
||||
This component will be displayed when the user navigates to a page that does
|
||||
not exist.
|
||||
|
||||
Think of it as a 404 page.
|
||||
"""
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
return rio.Card(
|
||||
rio.Column(
|
||||
rio.Row(
|
||||
rio.Icon(
|
||||
"material/error",
|
||||
fill="warning",
|
||||
min_width=4,
|
||||
min_height=4,
|
||||
),
|
||||
rio.Text(
|
||||
"This page does not exist!",
|
||||
style=rio.TextStyle(
|
||||
font_size=3,
|
||||
fill=self.session.theme.warning_palette.background,
|
||||
),
|
||||
),
|
||||
spacing=2,
|
||||
align_x=0.5,
|
||||
),
|
||||
rio.Text(
|
||||
"The entered URL does not exist on this website. Please check your input or navigate back to the homepage.",
|
||||
wrap=True,
|
||||
),
|
||||
rio.Button(
|
||||
"To homepage",
|
||||
on_press=lambda: self.session.navigate_to("/"),
|
||||
),
|
||||
spacing=3,
|
||||
margin=4,
|
||||
min_width=20,
|
||||
),
|
||||
color="primary",
|
||||
align_x=0.5,
|
||||
align_y=0.35,
|
||||
)
|
||||
|
||||
|
||||
# </component>
|
||||
@@ -10,16 +10,16 @@ from .. import components as comps
|
||||
# <component>
|
||||
class RootComponent(rio.Component):
|
||||
"""
|
||||
This page will be used as the root component for the app. This means, that
|
||||
it will always be visible, regardless of which page is currently active.
|
||||
This component will be used as the root for the app. This means that it will
|
||||
always be visible, regardless of which page is currently active.
|
||||
|
||||
This makes it the perfect place to put components that should be visible on
|
||||
all pages, such as a navbar or a footer.
|
||||
|
||||
Additionally, the root page will contain a `rio.PageView`. Page views don't
|
||||
have any appearance on their own, but they are used to display the content
|
||||
of the currently active page. Thus, we'll always see the navbar and footer,
|
||||
with the content of the current page in between.
|
||||
Additionally, the root will contain a `rio.PageView`. Page views don't have
|
||||
any appearance of their own, but they are used to display the content of the
|
||||
currently active page. Thus, we'll always see the navbar and footer, with
|
||||
the content of the current page sandwiched in between.
|
||||
"""
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
@@ -31,7 +31,9 @@ class RootComponent(rio.Component):
|
||||
rio.Spacer(min_height=10, grow_y=True),
|
||||
# The page view will display the content of the current page.
|
||||
rio.PageView(
|
||||
# Make sure the page view takes up all available space.
|
||||
# Make sure the page view takes up all available space. Without
|
||||
# this the navbar would be assigned the same space as the page
|
||||
# content.
|
||||
grow_y=True,
|
||||
),
|
||||
# The footer is also common to all pages, so place it here.
|
||||
|
||||
@@ -8,7 +8,8 @@ import rio
|
||||
# <component>
|
||||
class Testimonial(rio.Component):
|
||||
"""
|
||||
Displays 100% legitimate testimonials from real, totally not made-up people.
|
||||
Displays 100% legitimate testimonials from real, most definitely not made-up
|
||||
people.
|
||||
"""
|
||||
|
||||
# The quote somebody has definitely said about this company.
|
||||
|
||||
@@ -46,7 +46,7 @@ class UserSignUpForm(rio.Component):
|
||||
# so we can easily access it from anywhere.
|
||||
pers = self.session[persistence.Persistence]
|
||||
|
||||
# Check if username and passwords are empty
|
||||
# Make sure all fields are populated
|
||||
if (
|
||||
not self.username_sign_up
|
||||
or not self.password_sign_up
|
||||
@@ -70,7 +70,7 @@ class UserSignUpForm(rio.Component):
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.error_message = "User already exists"
|
||||
self.error_message = "This username is already taken"
|
||||
self.username_valid = False
|
||||
self.passwords_valid = True
|
||||
return
|
||||
|
||||
@@ -37,7 +37,6 @@ def guard(event: rio.GuardEvent) -> str | None:
|
||||
class InnerAppPage(rio.Component):
|
||||
def build(self) -> rio.Component:
|
||||
return rio.PageView(
|
||||
fallback_build=comps.NoSuchPage,
|
||||
grow_y=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -15,23 +15,16 @@ from .. import data_models, persistence
|
||||
# <component>
|
||||
def guard(event: rio.GuardEvent) -> str | None:
|
||||
"""
|
||||
Create a guard that checks if the user is already logged in.
|
||||
|
||||
If the user is already logged in, the login page will be skipped and the user will
|
||||
be redirected to the home page.
|
||||
|
||||
## Parameters
|
||||
|
||||
`event`: The event that triggered the guard containing the `session` and `active_pages`.
|
||||
A guard which only allows the user to access this page if they are not
|
||||
logged in yet. If the user is already logged in, the login page will be
|
||||
skipped and the user will be redirected to the home page instead.
|
||||
"""
|
||||
# If the user is already logged in, there is no reason to show the login page.
|
||||
|
||||
# Check if the user is authenticated by looking for a user session
|
||||
try:
|
||||
event.session[data_models.AppUser]
|
||||
|
||||
# User is not logged in, no redirection needed
|
||||
except KeyError:
|
||||
# User is not logged in, no redirection needed
|
||||
return None
|
||||
|
||||
# User is logged in, redirect to the home page
|
||||
@@ -46,26 +39,9 @@ def guard(event: rio.GuardEvent) -> str | None:
|
||||
class LoginPage(rio.Component):
|
||||
"""
|
||||
Login page for accessing the website.
|
||||
|
||||
This page will be used as the root component for the app. It will contain the login form
|
||||
and the sign up form. The login form consists of a username and password input field and a
|
||||
login button. The sign up form consists of a username and password input field and a sign up
|
||||
button. The sign up button will open a pop up with the sign up form when clicked.
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
`username`: The username of the user.
|
||||
|
||||
`password`: The password of the user.
|
||||
|
||||
`error_message`: The error message to display if the login fails.
|
||||
|
||||
`popup_open`: A boolean to determine if the sign up pop up is open.
|
||||
|
||||
`_currently_logging_in`: A boolean to determine if the user is currently logging in.
|
||||
"""
|
||||
|
||||
# These are used to store the currently entered values from the user
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
|
||||
@@ -87,27 +63,23 @@ class LoginPage(rio.Component):
|
||||
self._currently_logging_in = True
|
||||
await self.force_refresh()
|
||||
|
||||
# Perform the login
|
||||
# Try to find a user with this name
|
||||
pers = self.session[persistence.Persistence]
|
||||
|
||||
try:
|
||||
user_info = await pers.get_user_by_username(
|
||||
username=self.username
|
||||
)
|
||||
may_login = user_info is not None and user_info.password_equals(
|
||||
self.password
|
||||
)
|
||||
print("login accepted" if may_login else "login failed")
|
||||
except KeyError:
|
||||
may_login = False
|
||||
self.error_message = "Invalid username. Please try again or create a new account."
|
||||
return
|
||||
|
||||
# If the user isn't authorized, inform them about it
|
||||
if not may_login:
|
||||
self.error_message = "Incorrect username or password. Please try again or create a new account."
|
||||
# Make sure their password matches
|
||||
if not user_info.password_equals(self.password):
|
||||
self.error_message = "Invalid password. Please try again or create a new account."
|
||||
return
|
||||
|
||||
# The login was successful
|
||||
assert user_info is not None
|
||||
user_info.last_login = datetime.now(timezone.utc)
|
||||
self.error_message = ""
|
||||
|
||||
@@ -116,13 +88,13 @@ class LoginPage(rio.Component):
|
||||
user_id=user_info.id,
|
||||
)
|
||||
|
||||
# Attach it
|
||||
# Attach the session and userinfo. This indicates to any other
|
||||
# component in the app that somebody is logged in, and who that is.
|
||||
self.session.attach(user_session)
|
||||
|
||||
# Attach the userinfo
|
||||
self.session.attach(user_info)
|
||||
|
||||
# Permanently store the session token with the connected client
|
||||
# Permanently store the session token with the connected client.
|
||||
# This way they can be recognized again should they reconnect later.
|
||||
settings = self.session[data_models.UserSettings]
|
||||
settings.auth_token = user_session.id
|
||||
self.session.attach(settings)
|
||||
@@ -141,19 +113,22 @@ class LoginPage(rio.Component):
|
||||
self.popup_open = True
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
# create a banner with the error message if there is one
|
||||
error_banner = (
|
||||
[rio.Banner(text=self.error_message, style="danger", margin_top=1)]
|
||||
if self.error_message
|
||||
else []
|
||||
)
|
||||
# Create a banner with the error message if there is one
|
||||
|
||||
return rio.Card(
|
||||
rio.Column(
|
||||
rio.Text("Login", style="heading1", justify="center"),
|
||||
# show error message if there is one
|
||||
*error_banner,
|
||||
# create the login form consisting of a username and password input field,
|
||||
# a login button and a sign up button
|
||||
# Show error message if there is one
|
||||
#
|
||||
# Banners automatically appear invisible if they don't have
|
||||
# anything to show, so there is no need for a check here.
|
||||
rio.Banner(
|
||||
text=self.error_message,
|
||||
style="danger",
|
||||
margin_top=1,
|
||||
),
|
||||
# Create the login form consisting of a username and password
|
||||
# input field, a login button and a sign up button
|
||||
rio.TextInput(
|
||||
text=self.bind().username,
|
||||
label="Username",
|
||||
@@ -163,10 +138,11 @@ class LoginPage(rio.Component):
|
||||
rio.TextInput(
|
||||
text=self.bind().password,
|
||||
label="Password",
|
||||
# Make the password field a secret field, so the password is not visible
|
||||
# the user can make it visible by clicking on the eye icon
|
||||
# Mark the password field as secret so the password is
|
||||
# hidden while typing
|
||||
is_secret=True,
|
||||
# ensure the login function is called when the user presses enter
|
||||
# Ensure the login function is called when the user presses
|
||||
# enter
|
||||
on_confirm=self.login,
|
||||
),
|
||||
rio.Row(
|
||||
@@ -175,17 +151,18 @@ class LoginPage(rio.Component):
|
||||
on_press=self.login,
|
||||
is_loading=self._currently_logging_in,
|
||||
),
|
||||
# Create a sign up button that opens a pop up with a sign up form
|
||||
# the sign up form consists of a username and password input field.
|
||||
# Create a sign up button that opens a pop up with a sign up
|
||||
# form
|
||||
rio.Popup(
|
||||
anchor=rio.Button(
|
||||
"Sign up",
|
||||
on_press=self.on_open_popup,
|
||||
),
|
||||
content=comps.UserSignUpForm(
|
||||
# bind popup_open to the popup_open attribute of the login page
|
||||
# this way the popup_open attribute of the login page will be set to
|
||||
# the value of the popup_open attribute of the sign up form when changed
|
||||
# Bind `popup_open` to the `popup_open` attribute of
|
||||
# the login page. This way the page's attribute will
|
||||
# always have the same value as that of the form,
|
||||
# even when one changes.
|
||||
popup_open=self.bind().popup_open,
|
||||
),
|
||||
position="fullscreen",
|
||||
|
||||
@@ -117,7 +117,7 @@ class Persistence:
|
||||
async def get_user_by_username(
|
||||
self,
|
||||
username: str,
|
||||
) -> data_models.AppUser | None:
|
||||
) -> data_models.AppUser:
|
||||
"""
|
||||
Retrieve a user from the database by username.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user