Right now, if we try to create a new fortune through our app, we will get an error because
no user_id
is set. We can get this value from our current_user
method as this is available
once we’ve logged in.
We will need to update our Fortune actions, as well as our SaveFortune
operation to include
this.
Open up the SaveFortune
operation in src/operations/save_fortune.cr
. We need to tell this operation
that it needs
the current_user
. Update with this code:
# src/operations/save_fortune.cr
class SaveFortune < Fortune::SaveOperation
permit_columns text
needs current_user : User
before_save do
user_id.value = current_user.id
end
end
We are telling this operation that it must include the current_user
object. Then before we save
the fortune record we assign a value to user_id
attribute.
For more information on attributes read the What are attributes guide in Callbacks and Validations.
With the addition of the needs
any time we instantiate the SaveFortune
, we must pass in the current_user
object.
For now this is in the following fortune actions: Fortunes::New
, Fortunes::Create
, Fortunes::Edit
, and Fortunes::Update
.
We will start in the Fortunes::New
action in src/actions/fortunes/new.cr
. Update with this code:
# src/actions/fortunes/new.cr
html NewPage, operation: SaveFortune.new(current_user: current_user)
Next is our Fortunes::Create
action in src/actions/fortunes/create.cr
. Update with this code:
# src/actions/fortunes/create.cr
SaveFortune.create(params, current_user: current_user) do |operation, fortune|
Next is the Fortunes::Edit
action in src/actions/fortunes/edit.cr
. Update with this code:
# src/actions/fortunes/edit.cr
html EditPage,
operation: SaveFortune.new(fortune, current_user: current_user),
fortune: fortune
And last is the Fortunes::Update
action in src/actions/fortunes/update.cr
. Update with this code:
# src/actions/fortunes/update.cr
SaveFortune.update(fortune, params, current_user: current_user) do |operation, updated_fortune|
We’ve updated a lot of code. Now is a good time to boot your app and make sure everything compiles as it should.
Once booted you can sign in to your account and visit the /fortunes/new
page. See that creating a new fortune
will assign it to your account.
Since each fortune is specific to a user no other user should be allowed to edit or delete another user’s fortune. We must detect that a fortune doesn’t belong to a user, and handle this appropriately.
We will need to update several actions: Fortunes::Edit
, Fortunes::Update
, and Fortunes::Delete
to
ensure we’re protecting against unauthorized updates. This is a good time to try a mixin.
Mixins are just modules you can reuse in multiple classes. You’ll find some existing mixins in the
src/actions/mixins/
directory. We will create a new one called OnlyAllowCurrentUser
in src/actions/mixins/only_allow_current_user.cr
.
The idea of this mixin will be to check that the current_user
owns the fortune
. If not, then we will raise an error.
Add this file with this code:
# src/actions/mixins/only_allow_current_user.cr
module OnlyAllowCurrentUser
class UnauthorizedEntryError < Lucky::Error
end
def ensure_owned_by_current_user!(fortune : Fortune)
if fortune.user_id != current_user.id
raise UnauthorizedEntryError.new
end
end
end
Now we will take a look at custom error handling. Any error that happens in your application will be caught by
the Errors::Show
action. This allows you to keep error handling in a single location, as well as customize
how the errors are displayed. You can even send errors off to a 3rd party reporting service in here.
Open up the src/actions/errors/show.cr
action. You’ll be adding an extra render
method in here, but keep in
mind that the order of these matters in Crystal. We will add our code after the Avram::InvalidOperationError
overload.
# src/actions/errors/show.cr
# Add this after the `Avram::InvalidOperationError` overload
def render(error : OnlyAllowCurrentUser::UnauthorizedEntryError)
error_html "You're not authorized to do this", status: 401
end
The next step is to include our new mixin to the actions we want to apply it to.
We will try this in our Fortunes::Edit
action first. Open the src/actions/fortunes/edit.cr
file, and update this code:
# src/actions/fortunes/edit.cr
class Fortunes::Edit < BrowserAction
# include our module mixin here
include OnlyAllowCurrentUser
get "/fortunes/:fortune_id/edit" do
fortune = FortuneQuery.find(fortune_id)
# Then use the method defined in that module
ensure_owned_by_current_user!(fortune)
html EditPage,
operation: SaveFortune.new(fortune, current_user: current_user),
fortune: fortune
end
end
Save your files, boot your app, and give it a shot. Try editing a fortune that doesn’t belong to you; you
should see the exception page with a code snippet, and
stack trace. This page is helpful when debugging, but only shows up in development. To see what your users
will see in production, open up config/error_handler.cr
, and set the show_debug_output
setting to false
.
Once your app recompiles, try the action again, and you’ll now see the default Lucky error page with your custom message, and the 401 status. Be sure to set that setting back once you’ve had a chance to check it out.
For more information on error handling, read the Error Handling guide.
We’ve blocked the fortune actions for fortunes we don’t own, but the action links still exist. We can now
update the Fortunes::ShowPage
to only display action links when we own a fortune. Open the Fortunes::ShowPage
in src/pages/fortunes/show_page.cr
, and update with this code:
def content
link "Back to all Fortunes", Fortunes::Index
h1 "Fortune with id: #{fortune.id}"
if fortune.user_id == current_user.id
render_actions
end
render_fortune_fields
end
It’s as simple as that!
This tutorial is only meant to give you a quick overview and taste of how a Lucky app is structured. When it comes to developing applications, you will have your own preferences regarding where you want code to go, what you want to name things, and how logic should be structured.
We recommend deleting your app, and giving this tutorial a shot again. Think of it like a movie, you may have missed something the first time around! Maybe the second time will allow you to get a little more adventurous with your code.
As always, if you run in to any issues, please join us in the Discord Chat and someone will be around more than willing to help you out.
If you find any issues in this tutorial, please Open an issue on the Lucky website repo so we can correct it.