auth example improvements

This commit is contained in:
Jakob Pinterits
2024-09-21 14:46:05 +02:00
parent d87ccf3856
commit 35fa963654
9 changed files with 73 additions and 143 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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",

View File

@@ -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.