Features
Until now, users once logged in, had no way of logging out of our application.
Let us implement the logout feature:
-
We can add a logout button in our navbar.
-
Clicking the logout button should terminate the session and redirect the user
to the login page.
-
From now onwards, we should keep track of who created which task. Later, we
can use this to limit access to a task to its owner and its assignee only.
-
Task detail page should also contain the name of task owner.
Technical design
We will do the following to implement the given requirements:
-
We will create a destroy
action in SessionsController
. We use this action
to clean up the session on the backend when the user logs out. We can map this
action to the DELETE
request to the /session
URL.
-
After calling the API, we will clear our browser's localStorage and reset the
headers from axios configuration.
-
Now, to add information about the task owner, we will need a new column
task_owner_id
in tasks
table.
-
We will add a foreign_key constraint on task_owner_id
to create a
relationship between a task and its corresponding user who is the owner of the
task.
-
We will declare a has_many
association with a custom name of created_tasks
in the User
model and a belongs_to
association for task_owner
in the
Task
model.
-
Inside the TasksController
, we will get the currently logged-in user details
via the current_user
method in the ApplicationController
. We can use the
current user's created_tasks
association to create a new task.
-
To send the task owner's name in JSON response, we will have to update the
show action's corresponding Jbuilder view builder.
-
To display the task creator's name in task details page, we will need to
update the Show
component of Tasks
to render task owner's name received
from backend along with other task details.
Sessions controller
Open app/controllers/sessions_controller.rb
and add the following lines:
In the above code, along with adding a destroy
action, we have also updated
the skip_before_action
filter to be effective only when the request is for
create
action.
Session routes
Let's modify routes.rb
and add a destroy action for session
:
Frontend changes
Open apis/auth.js
and add following lines:
Let's open apis/axios.js
and create a function to clear the default Axios
headers when the user is logged out. Add the resetAuthTokens
method at the end
of the file and also update the export statement like this:
Now, let's open the NavBar/index.jsx
component and make use of the logout API
that we added before to make the logout logic complete. Replace the whole
content of NavBar/index.jsx
with this code:
We are making use of the setToLocalStorage
helper method created in the
previous section to clear the localStorage. The user will be then redirected to
the login page if the logout was successfully done in the server too, where we
destroy the @current_user
instance variable.
Now let's commit these changes:
Storing information about who created the task
As discussed earlier, let's add a new column task_owner_id
to tasks table
using a new migration. We will also add a foreign key constraint on
task_owner_id
column which will contain the referenced primary key from a
record in the users
table.
Run the following command to generate the migration:
The on_delete: :cascade
option makes sure that the referencing rows, which
here are the rows of the task table, also get deleted when deleting the rows of
the referenced table, which is the user table in this case.
In our case, when deleting users, all the tasks created by them will also get
deleted.
Note that rows of users table won't be affected when tasks are deleted. The
foreign key relation is unidirectional.
Again, once we create a new migration, let's persist that into our database:
Adding association for task owner
Every task in the database is associated with the user who created it and a user
can create multiple such tasks. Hence we should declare appropriate associations
in the User
and Task
models.
Add the following line of code inside app/models/user.rb
to add a has_many
association called created_tasks
for the User
model:
Similarly we should update the Task
model and add a belongs_to
association
called task_owner
like this:
Now we have created associations for both task owner and the assigned user. We
know that all tasks will be deleted when the user who created those tasks is
deleted.
But what happens when the assigned user is deleted? Ideally the task should be
assigned back to the task owner but in our case the assigned user will be set to
nil
when that happens.
To fix this, let us add a method inside the User
model to assign back the
tasks to the task owner in the event of assigned user getting deleted. Update
app/models/user.rb
with the following lines of code:
When assign_tasks_to_task_owners
is invoked on a user object, all the
assigned_tasks
for that user are fetched and out of those tasks we are
selecting only those tasks which are created by another user and saving the
result in tasks_to_be_reassigned
variable.
Tasks owned by the user should be deleted and it doesn't make sense for us to
perform the reassigning operation on a task which is about to be deleted from
the database. Besides if the task_owner
and the assigned_user
are same then
reassigning isn't required.
By default before_destroy
callback method which is
assign_tasks_to_task_owner
will be invoked every time, right before a user
record gets deleted.
Creating a task using Task owner
In our application all tasks are associated to users. We cannot create a task
unless there is no user. Each task is created by a user who is related to the
task using the task_owner
association and the task is also assigned to a user.
Task assignee is related to a task using the assigned_user
association.
So far we had only been saving tasks with an assigned_user
. We also need to
add a task owner for tasks. To do so, we can pass the task_owner_id
in the
task_params
while creating a new task. While this works, we'd have to
explicitly pass the task_owner_id
as parameter.
We know that the task_owner will always be the currently logged in user. In our
application, current_user
refers to the currently logged in user. We can use
the current_user.created_tasks
association to create a task. This way we do
not have to specify the task_owner_id
in task_params
. When we use
current_user.created_tasks
, Rails knows that the new task's task_owner_id
is
equal to current user's id.
Rails knows this because we have specified the task_owner_id
foreign key for
created_tasks
association in the User
model. This saves time, and makes code
more fluent as well as adhering to the business logic.
It's generally frowned upon to use Model.new
for creating new records.
Whenever possible use the already available association or instance variables
itself.
Update the create
method of TasksController
like this:
Before moving on, we should delete the existing task records in our database as
they do not have a task_owner
and this can lead to errors later on. Use the
following command to delete all the existing task records and their associations
from the database.
Showing task owner
task_owner
can be accessed in the show action's corresponding Jbuilder
template file where we can include it as a key in the response JSON along with
other task attributes.
Let's update the show.json.jbuilder
view template for tasks and add the
task_owner
key to the response JSON. To do so, add the following lines to
/app/views/tasks/show.json.jbuilder
:
Now, update the Show
component to display the task owner name along with other
task details. Add the following lines of code to the Show
component:
Now let's commit these changes: