When a full Lucky app is generated with auth you will get:
By default actions that inherit from BrowserAction
(src/actions/browser_action.cr
) will require sign in because
BrowserAction
includes the Auth::RequireSignIn
module
(src/actions/mixins/auth/require_sign_in.cr
). This is a great way to
make sure that your actions are protected by default.
If your action requires sign in and you are rendering HTML, you will
likely make the page you are rendering inherit from MainLayout
. This is
because MainLayout
requires that a current_user
is available.
The next section talks about what to do when sign in is optional.
To allow guests (signed out users) to view an action, include the
Auth::AllowGuests
mixin in the action.
This will allow guests to request this action. It will also make the
current_user
method nilable (if no user is signed in current_user
will return
nil
).
class PublicPosts < BrowserAction
include Auth::AllowGuests
end
Now we have to figure out how to handle the possibility of a guest in our pages and layouts.
By default there are 2 layouts generated:
AuthLayout
MainLayout
This is the layout that “auth” pages will use, such as
PasswordsResets::NewPage
or SignUps::NewPage
. The AuthLayout
does
not need a current_user
because these pages are meant to be used by
guests that have not yet signed in/up.
This is the layout used for pages where a user has been signed in. It
requires a current_user
because it declares that it needs current_user : User
.
abstract class MainLayout
# 'needs current_user : User' makes it so that the current_user
# is always required for pages using MainLayout
needs current_user : User
end
There are a few options available when rendering a page for a guest user that has not signed in.
AuthLayout
if the page is related to authentication.MainLayout
to allow signed out users.If you have a page that you’d like to use for authentication it is probably
best to use the AuthLayout
. This could be useful for pages like
adding 2 factor auth, social logins, etc.
You can use this layout like any other layout:
class TwoFactorAuth::NewPage < AuthLayout
end
This is usually the best option because you can keep the MainLayout
for
signed in users, and a new layout for things like marketing pages, and
other public pages.
For example, if you want to add some marketing pages you could create a
MarketingLayout
that either does not need a signed in user, or accepts
signed in users and guests.
abstract class MarketingLayout
include Lucky::HTMLPage
# You may want to add 'needs current_user : User?'
#
# This might be useful if you want to show a "Go to app" button in the
# header if current_user is already signed in.
#
# needs current_user : User?
end
Some other ideas for naming layouts:
PublicLayout
GuestLayout
SupportLayout
This might be a good option if you are building an app where the pages are available to both guests and signed in users. For example, Reddit allows signed in users and guests (signed out users) to view most pages.
If this is the type of page you want to build, you can change the needs current_user
in MainLayout
to allow nil
:
# src/pages/main_layout.cr
abstract class MainLayout
# Append '?' to make it so current_user can be 'nil'
needs current_user : User?
end
You will also need to check if the current_user
is present in a few places.
For example in MainLayout
you’ll have to change where the signed in user
is displayed in the header.
Add conditional to MainLayout#render_signed_in_user
:
private def render_signed_in_user
user = @current_user
if user
text user.email
text " - "
link "Sign out", to: SignIns::Delete, flow_id: "sign-out-button"
end
end
You can now make pages that inherit from this layout and they will work for signed in users and guests.
Your action might look like this:
class Posts::Index < BrowserAction
include Auth::AllowGuests
get "/posts" do
html Posts::IndexPage, posts: PostQuery.new
end
end
And the page might look like this:
class Posts::IndexPage < MainLayout
needs posts : PostQuery
def content
# html the page contents
end
end
Note that you can still use this layout for actions that require sign in, but you will need to check for the signed in user before displaying anything that needs the current user.
Let’s say we have this action that requires sign in:
class Settings::Edit < BrowserAction
get "/settings/edit" do
html Settings::EditPage
end
end
This will not work because @current_user
might be nil:
class Settings::EditPage < MainLayout
def content
h1 "#{@current_user.name}'s Settings'"
end
end
Instead you need to either check that the current_user is really there, or since this page is used for actions that require sign in you could add a method that tells Crystal it is not nil:
We’ll use getter!
so that Crystal treats current_user
as not nil.
class Settings::EditPage < MainLayout
# Defines a getter that says current_user can't be nil
getter! current_user
def content
# Use the 'current_user' method
h1 "#{current_user.name}'s Settings'"
end
end
This works, but please note that if you accidentally use this page with
an action that allows guests, you will get a runtime error because it
will try to get the current_user’s name, but the current_user will be
nil
for guests.
Instead, we recommend creating a new layout just for signed in users in cases like this. It will catch more bugs and you won’t have as much conditional logic. You can share common HTML across layouts with components.