I am building a simple blog app using Ruby on Rails that allows users to log in/out, sign up and perform actions on their articles and profiles based on permissions and restrictions.
I have encountered a problem with the destroy User action. In the users/index view(where all existing users are listed), it causes no errors due to the url path containing no {:id}, but the redirect_to root_path does not work. If the same action is executed in the users/show page(personal profile page with some info and associated articles), due to the url being localhost/users/id, when the user is deleted I get "Couldn't find User with 'id'=33" error shown below. If I manually go to the root route, the successful account deletion message shows up and the action is performed correctly. So this is not a metter of DESTROY not working, but of redirection I believe. I have tried redirecting to different paths but it still doesn't work. Here are the related files:
routes.rb
Rails.application.routes.draw do
root "pages#home"
get "about", to: "pages#about"
resources :articles
get "signup", to: "users#new"
resources :users, except: [:new]
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
get 'logout' => :destroy, to: 'sessions#destroy'
end
pages_controller
class PagesController < ApplicationController
def home
redirect_to articles_path if logged_in?
end
def about
end
end
users_controller
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
before_action :require_user, only: [:edit, :update]
before_action :require_same_user, only: [:edit, :update, :destroy]
def index
#users = User.all
end
def show
#articles = #user.articles
end
def new
#user = User.new
end
def edit
end
def create
#user = User.new(user_params)
if(#user.save)
session[:user_id] = #user.id #logs user in automatically once they are signed up
flash[:notice] = "Welcome to AlphaBlog, #{#user.username}!"
redirect_to articles_path
else
render 'new'
end
end
def update
if #user.update(user_params)
flash[:notice] = "Account updated!"
redirect_to #user
else
render 'edit'
end
end
def destroy
#user.destroy
session[:user_id] = nil
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
def set_user
#user = User.find(params[:id])
end
def require_same_user
if current_user != #user
flash[:alert] = "You can only edit your own profile!"
redirect_to current_user
end
end
end
sessions_controller
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
flash[:notice] = "Logged in successfully!"
redirect_to user
else
flash.now[:alert] = "There was something wrong with your login details!"
render 'new'
end
end
def destroy
session[:user_id] = nil
flash[:notice] = "Logged out."
redirect_to root_path
end
end
users/index.html.erb
<div class = "header">
<h1>
AlphaBlog
<% if logged_in? %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<% else %>
<%= link_to 'Home', root_path(), method: :get, class: "index-button-to" %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<% end %>
<%= render 'layouts/log_in_out_navigation'%>
</h1>
</div>
<h2>Alpha Bloggers</h2>
<div class="index-container">
<%# cycle through all articles and show them all in a table %>
<% #users.each do |user| %>
<div class = "index-article-container">
<div class="index-article-user" style = "color:rgb(16, 136, 255);">
<%= user.username %>
</div>
<div class="white">
<div class="index-article-title">
<%= gravatar_for(user, size: 150) %>
</div>
<div class="index-article-description">
<%# gives the plural word for multiple articles %>
<%= pluralize(user.articles.count, "article") %>
</div>
<div class="index-article-actions">
<%# shows selected article page %>
<%= link_to 'View Profile', user, class: "index-link-to show" %>
<% if logged_in? && current_user.username == user.username %>
<%# shows selected article EDIT page. edit_article_path because in routes,
the prefix for edit is edit_article && (article) because we need the id for the path as well%>
<%= link_to 'Edit Profile', edit_user_path(user), data: { turbo_method:
:get}, class: "index-link-to edit" %>
<%= link_to 'Delete Profile', user_path(current_user), data: {
turbo_method: :delete, turbo_confirm: "Are you sure? (This will also delete all of your
articles)" }, class: "index-link-to delete" %>
<% end %>
</div>
</div>
<div class="index-created-updated">
Joined <%= time_ago_in_words(user.created_at) %> ago.
</div>
</div>
<% end %>
users/show.html.erb
<div class = "header">
<h1>
AlphaBlog
<% if logged_in? %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<%= link_to 'Bloggers', users_path, method: :get, class: "index-button-to" %>
<% else %>
<%= link_to 'Home', root_path(), method: :get, class: "index-button-to" %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<%= link_to 'Bloggers', users_path, method: :get, class: "index-button-to" %>
<% end %>
<%= render 'layouts/log_in_out_navigation'%>
</h1>
</div>
<h2> <%= #user.username %>'s profile </h2>
<div class="show-users-image">
<%# gravatar_for method created in helpers/application_helper %>
<%= gravatar_for #user, size: 200 %>
<% if logged_in? && current_user.username == #user.username %>
<div class="index-profile-actions">
<%= link_to "Edit Profile", edit_user_path(#user), class: "index-link-to edit" %>
<%= link_to 'Delete Profile', user_path(current_user), data: { turbo_method: :delete,
turbo_confirm: "Are you sure? (This will also delete all of your articles)" }, class: "index-
link-
to delete", style: "margin-top:0.3vh" %>
</div>
<% end %>
</div>
<h3 style = "text-align:center">Articles</h3>
<%= render 'articles/article' %>
error page
The way to do this in Rails 7 is to update the destroy action in the UsersController by adding status: :see_other after the redirect, as follows:
def destroy
#user.destroy
session[:user_id] = nil
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path, status: :see_other
end
I think the answer here is really a very different layout of your routes and controller (or to not reivent the wheel in the first place). Passing the user id through the parameters would be fine if your making a system where you are managing other users - but its pretty wonky when users are managing their own profiles.
For example this is how users CRUD their own profiles in a vanilla Devise setup:
Verb URI Pattern Controller#Action
------------------------------------------------------------------------
GET /users/cancel(.:format) devise/registrations#cancel
GET /users/sign_up(.:format) devise/registrations#new
GET /users/edit(.:format) devise/registrations#edit
PATCH /users(.:format) devise/registrations#update
PUT /users(.:format) devise/registrations#update
DELETE /users(.:format) devise/registrations#destroy
POST /users(.:format) devise/registrations#create
Note the lack of the :id parameter in the URI Pattern. Thats because its implied that the resource in question is the currently signed in user, and that the user is identified through the session (or a token).
The controller is named Registrations to avoid the ambiguity if the programmer later wants to add a UsersController to manage other users.
If you want to do something similiar you can generate singular routes by using the resource macro instead of resources.
# routes for user registration
resource :registrations,
only: [:new, :edit, :update, :create, :destroy]
# routes for viewing other users
resources :users, only: [:index, :show]
Which will generate:
Prefix Verb URI Pattern Controller#Action
-----------------------------------------------------------------------
new_registrations GET /registrations/new(.:format) registrations#new
edit_registrations GET /registrations/edit(.:format) registrations#edit
registrations GET /registrations(.:format) registrations#show
PATCH /registrations(.:format) registrations#update
PUT /registrations(.:format) registrations#update
DELETE /registrations(.:format) registrations#destroy
POST /registrations(.:format) registrations#create
Name it whatever you want. The core takeaway here is to not confuse two completely different problems - user management and user registrations and have separate endpoints and controllers for each responsibilty.
Then in your controller you simply authenticate the user from the session and redirect the user if they are not authenticated:
# Handles user account registration, updates and deleting accounts
class RegistrationsController < ApplicationController
before_action :require_user, except: [:new, :create]
# Displays the form for signing up a user
# GET /registrations
def new
#user = User.new
end
# Register a new user and sign them in
# POST /registrations
def create
#user = User.new(user_params)
if #user.save
reset_session # avoids session fixation attacks
session[:user_id] = #user.id #logs user in automatically once they are signed up
flash[:notice] = "Welcome to AlphaBlog, #{#user.username}!"
redirect_to articles_path
else
render :new
end
end
# Form for editing the users own profile
# GET /registrations/edit
def edit
#user = current_user
end
# Update the currently signed in user
# PATCH /registrations
def update
#user = current_user
if #user.update(user_params)
flash[:notice] = "Account updated!"
redirect_to current_user
else
render :new
end
end
# Cancel the current users registration
# DELETE /registrations
def delete
current_user.delete
reset_session # avoids session fixation attacks
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
end
# Displays users
# Managing accounts is handled by RegistrationsController
class UsersController < ApplicationController
# GET /users
def index
#users = User.all
end
# GET /users/1
def show
#user = User.find(params[:id])
#articles = #user.articles
end
end
Since their is no id in the path you you need to set the delete button to send to the right path:
<%= button_to "Delete your account", registrations_path, method: :delete %>
And adjust your forms:
<%= form_with(model: #user, url: registrations_path) do |form| %>
# ...
<% end %>
Doing this the correct way would really be to do it the same way that Devise does and have a normal link that sends a GET request to a "Are you sure you want to delete your account?" page when then requires the user to enter their password or email and submits a DELETE request so that users don't accidentially delete their accounts.
But then again don't reinvent the authentication wheel unless you want a long and tedious lesson into wheelmaking.
I'm not sure you have truly got to the bottom of this. In your original approach I suspect two things are going on:
In users/index.html.erb you have link_to 'Delete Profile', user_path(current_user) but I think you want user_path(user). What you currently have will have every delete button try to delete the same user.
The fact that error says your are attempting to execute 'show' rather than 'destroy' makes me suspect that you do not have turbo loaded properly. You don't say what version of rails you are using, but for versions earlier than 7 you don't get turbo out of the box, and you should use UJS instead.
compare this https://guides.rubyonrails.org/getting_started.html#deleting-an-article with this https://guides.rubyonrails.org/v6.1/getting_started.html#deleting-an-article
I am trying to build an admin section in my rails application.
I am quite to new rails and I have some difficulties about the methodology.
So the routes
Rails.application.routes.draw do
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users, except: [:index]
# namespace :admin do
# root 'base#home'
# get 'login' => 'sessions#home'
# post 'login' => 'admin/sessions#create'
# get 'dashboard' => 'base#dashboard'
# resources :users
# end
namespace :admin do
root 'base#home'
end
end
There is a model User avalaible in the front.
So users can login via a login from
views/sessions/new.html.erb
<div id="login_form">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
</div>
and edit their profile with the SessionController
app/controllers/sessions_controller
class SessionsController < ApplicationController
def new
end
def create
#user = User.find_by(email: params[:session][:email].downcase)
if #user && #user.authenticate(params[:session][:password])
log_in #user
params[:session][:remember_me] == '1' ? remember(#user) : forget(#user)
redirect_to #user
remember #user
else
flash.now[:danger] = 'Invalid email/password combination' # Not quite right!
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
And a base controller app/controllers/admin/base_controller.rb
class Admin::BaseController < ApplicationController
layout 'admin'
before_action :logged_in_user
def home
end
private
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
(redirect_to login_url)
end
end
end
That's where I am confused because in case of a non-admin user, he's redirected to his profile page (it works fine) but the admin user should be redirected to the admin home page.
I tried to have two different login forms but I had an infinite loop redirection because I was redirecting to the login page in case of non logged in but
before_action :require_login
was called for every action event the login page
What would be the right methodology ?
Thanks :)
As far as I understand your admin users log in the same way a regular user would: via SessionsController#create. It also seems like you have a second controller SessionsController in the Admin namespace, which is not used?
That's where I am confused because in case of a non-admin user, he's redirected to his profile page (it works fine) but the admin user should be redirected to the admin home page.
You currently don't differentiate between the "normal" user and an admin, therefore both are redirect_to #user. (the users profile page).
What would be the right methodology?
You could add field to your user model (boolean admin) which you could use to differentiate in your SessionsController.
if #user && #user.authenticate(params[:session][:password])
log_in #user
params[:session][:remember_me] == '1' ? remember(#user) : forget(#user)
if #user.admin?
redirect_to 'admin_home_page'
else
redirect_to #user
end
remember #user
That way you don't need a second SessionsController. Furthermore
you could write an admin "filter" in your controllers which would only allow access to certain controller actions if the #user is an admin.
class Controller
before_filter :admin_only
private
def admin_only
redirect_to :back, unless #user.admin?
end
end
Hope this helps.
I am new to ruby on rails. I am developing an application which has authentication system.
my problem is I am getting error when logging in to the application in production(Heroku). It is working in development.
Error
I production after i typing url https://akashpinnaka.herokuapp.com/login,
it is redirecting to https://akashpinnaka.herokuapp.comlogin. I am missing the '/' between root_url and 'login' for POST login.
Note: Working in development environment.
My routes are
Rails.application.routes.draw do
get 'welcome/index'
root 'welcome#index'
resources :articles
resources :projects
resources :users
get '/login' => 'sessions#new'
post '/login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
end
Sessions Controller
class SessionsController < ApplicationController
def new
end
def create
#user = User.find_by_email(params[:session][:email])
if #user && #user.authenticate(params[:session][:password])
session[:user_id] = #user.id
redirect_to root_path
else
redirect_to 'login'
end
end
def destroy
session[:user_id] = nil
redirect_to '/'
end
end
Sessions#new
<%= form_for(:session, url: login_path) do |f| %>
<%= f.email_field :email, :placeholder => "Email" %>
<%= f.password_field :password, :placeholder => "Password" %>
<%= f.submit "Log in" %>
<% end %>
Usually, when your form can't be saved, you don't redirect. You show the same form, with error explanation.
def create
#user = User.find_by_email(params[:session][:email])
if #user && #user.authenticate(params[:session][:password])
session[:user_id] = #user.id
redirect_to root_path
else
# should've been login_path
# redirect_to 'login'
render 'new' # this is better
end
end
If you are sure that you want to redirect, by all means, go ahead. But supply the correct path :)
You need use redirect_to '/login' or redirect_to login_path instead of redirect_to 'login'
#Sergio Tulentsev's answer is pretty good.
You should fix your routes:
#config/routes.rb
root "welcome#index"
resources :articles, :projects, :users
resources sessions, only: [:new, :create, :destroy], path_names: { new: "login", create: "login", destroy: "logout" }
Rails has two sets of path helpers - _path and _url
_path, as we know, is there to provide relative routes (/path).
_url is there to provide direct routes (http://domain.com/path)
Thus, when you have:
get "/login" (with leading slash) in your routes, it will almost certainly cause problems with your applications' relative link helpers.
As mentioned by #Sergio Tulentsev, your create method, and the destroy method, should be fixed to use the correct path helpers:
def create
#user = User.find_by email: params[:session][:email]
if #user && #user.authenticate(params[:session][:password])
session[:user_id] = #user.id
redirect_to root_path
else
redirect_to login_path
end
end
def destroy
...
redirect_to root_path
end
It's worth taking #Sergio's advice on the render :new command too!
I am trying to create a posts pages. Where users can see all the public posts. I can successfully create posts but I can't see them.
ActionController::UrlGenerationError in Ribbits#index Showing
C:/Sites/myBlog/app/views/ribbits/index.html.erb where line #8
raised:
No route matches {:action=>"show", :controller=>"users", :id=>nil}
missing required keys: [:id]
The index.html.erb
<% #ribbits.each do |ribbit| %>
<div class="ribbitWrapper">
<a href="<%= user_path ribbit.user %>">
<img class="avatar" src="<% ribbit.user.avatar_url %>" />
<span class="name"> <%= ribbit.user.name %> </span>
</a>
#<%= ribbit.user.username %>
<span class="time"><%= time_ago_in_words(ribbit.created_at) %></span>
<p> <%= ribbit.content %></p>
</div>
<% end %>
The users controller:
def new
#user = User.new
end
def create
#user = User.create(user_params)
if #user.save
session[:user_id] = #user.id
redirect_to #user, notice: "Thank you for signing up!"
else
render 'new'
end
end
def show
#user = User.find(params[:id])
#ribbit = Ribbit.new
end
The ribbits controller
def index
#ribbits = Ribbit.all
#ribbit = Ribbit.new
end
def create
#ribbit = Ribbit.create(user_ribbits)
#ribbit.user_id = current_user.id
if #ribbit.save
redirect_to current_user
else
flash[:error] = "Problem!"
redirect_to current_user
end
end
private
def user_ribbits
params.require(:ribbit).permit(:content, :userid)
end
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
And also, the routes file
resources :sessions
resources :users
resources :ribbits
get 'logout', to: 'sessions#destroy', as: 'logout'
root to: 'users#new'
Would really appreciate the help!
As we discovered in the comments to the question, some of the #ribbits do not have user_id set.
To address your further question of "How do I connect user_id with my ribbits, my guess is that you have ribbits with user_id null because your user_ribbits method permits :userid rather than :user_id. This is assuming of course that you're properly passing user_id from your view to your controller on the creation of a ribbit.
In order to ensure that ribbits contain a user_id, you can add the following to your Ribbit model (ribbit.rb):
validates :user_id, presence: true
If there are other issues, this should at the very least prevent you from creating a ribbit without a user_id.
I hope this helps!
Using Rails 3.2. I'm using current_page? to determine the current page to add an active to the appropriate navigation link.
When the login fails, it returns to user_sessions#new, but then current_page? doesn't recognize the login_path anymore, thus not adding the class active to the link.
I can't use redirect_to because I want to catch the errors in the controller.
How can I get current_page? to recognize the user_sessions#new as well?
Below is my code:
# routes.rb
# Following 2 lines ensure that the login url is maintained as "login" even there are errors after submitting the form
match 'login' => 'user_sessions#new', :as => :login, :via => :get
match 'login' => 'user_sessions#create', :as => :login, :via => :post
# user_sessions_controller.rb
class UserSessionsController < ApplicationController
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
flash[:success] = "Login successful!"
redirect_back_or_default profile_account_url
else
render 'new'
end
end
end
# _sub_nav.html.erb
<ul class="nav nav-tabs">
<li<%= " class=active" if current_page?(login_path) %>>
Login
</li>
<li<%= " class=active" if current_page?(signup_path) %>>
Signup
</li>
</ul>
I believe changing
<li<%= " class=active" if current_page?(login_path) %>>
to
<li<%= " class=active" if current_page?(:controller => 'user_session', :action => 'new') %>>
should work.