DNorthrup DNorthrup - 1 month ago 9
Ruby Question

Rails 4 - flash-alert with plain text and ERB

I'm unsure if the way I'm treating it is the issue, or the ERB.

Right now when a user registers, I sent an activation email. If they haven't activated, and try to log back in, they're prompted "Sorry, you're not authorized." I want to modify it to so it offers them to be resent the email, as well.

SessionsController

class SessionsController < ApplicationController
def new
end

def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link, or click" + <%= link_to "here", :controller => :user, :action => :resend_email %>+ "to have it resent!"
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end

def destroy
log_out if logged_in?
redirect_to root_url
end

end


Within the Users controller I made a 'resend_email' function. Essentially just majority of the create one, so a bit redundant.

UsersController

class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy

def index
@users = User.where(activated: true).paginate(page: params[:page])
end

def show
@user = User.find(params[:id])
redirect_to root_url and return unless @user.activated?
end

def new
@user = User.new
end

def create
@user = User.new(user_params)
if @user.save
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end

def resend_email
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else

def edit
@user = User.find(params[:id])
end

def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end

def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end

private

def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end

# Before filters

# Confirms a logged-in user.
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end

# Confirms the correct user.
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end

def admin_user
redirect_to(root_url) unless current_user.admin?
end

end


I've tried modifying the 'message' to several different versions and each time I get a response similar to

/home/ubuntu/workspace/sample_app/app/controllers/sessions_controller.rb:16: syntax error, unexpected '<' <%= link_to "here", :controlle... ^ /home/ubuntu/workspace/sample_app/app/controllers/sessions_controller.rb:16: syntax error, unexpected ',', expecting keyword_end ...o "here", :controller => :user, :action => :resend_email %> ... ^ /home/ubuntu/workspace/sample_app/app/controllers/sessions_controller.rb:16: syntax error, unexpected '>' ...r, :action => :resend_email %> ... ^


I got this syntax from previous experience, and people with similar needs to mine.

So I'm wondering what's the best way to do this, without a gem (I did find Devise functionality, but I'm trying to learn how to do it on my own)

I also read that sometimes ERB and plain text don't work well. However, I also get an error with message is only
<%= link_to "here", :controller => :user, :action => :resend_email %>
as well.

I'm unsure if any other controllers are needed.

EDIT INFO

Leaving original copy for other people with the issue.
After reading what max provided in his answer below, I have modified a few things.

Changes to SessionsController

link = view_context.instance_exec do
ERB.new("<%= link_to 'here', :controller => :users, :action => :resend_activation %>").result(binding)
end
message = "Account not activated. "
message += "Check your email for the activation."
message += link # This is for demo purposes, just needed an output


User Controller

def resend_activation
@user = User.find(params[:email])
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
end

Partial View for Warnings

<% flash.each do |message_type, message| %>
<%= content_tag(:div, sanitize(message), class: "alert alert-#{message_type}") %>
<% end %>


Up to this point I am now seeing the link I expected, and my issue is when I click it.

Couldn't find User with 'id'=
- I've tried different uses of
User
and even told it re-search based on the email params.

So I tried updating my routes to

Routes

Rails.application.routes.draw do

root 'static_pages#home'
get '/home', to: 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
post '/signup', to: 'users#create'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
post '/resend_activation:email' => 'account_activations#resend_activation',
:constraints => { :email => /[^\/]+/ }
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end


Which made progress (I think), and it now prompts me "no get resource found". I'm doing research on when to use get vs post, and I thought I went the right method. But why is it trying to reference that path if right after it sends the email, it's supposed to go back to root_url?

Thanks again.

Update # 2:

I was able to get the errors to stop by adding in route switches, and modifying my sessions_controller to

def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation."
message += " #{view_context.link_to "Resend Activation E-Mail", { action: "resend_activation",
controller: "account_activations", email: user.email }, method: :post}"
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end


But now no emails are going out, everything just routes back to login.

heroku logs --tail


Tells me

Heroku Logs

2016-08-24T09:44:48.990703+00:00 app[web.1]: I, [2016-08-24T09:44:48.990609 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Started GET "/resend_activation/test@example.com" for 100.15.65.126 at 2016-08-24 09:44:48 +0000
2016-08-24T09:44:48.992317+00:00 app[web.1]: I, [2016-08-24T09:44:48.992217 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Processing by StaticPagesController#home as
2016-08-24T09:44:48.992394+00:00 app[web.1]: I, [2016-08-24T09:44:48.992349 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Parameters: {"email"=>"test@example.com"}
2016-08-24T09:44:48.997712+00:00 app[web.1]: I, [2016-08-24T09:44:48.997648 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Rendering static_pages/home.html.erb within layouts/application
2016-08-24T09:44:48.999032+00:00 app[web.1]: I, [2016-08-24T09:44:48.998965 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Rendered static_pages/home.html.erb within layouts/application (1.1ms)
2016-08-24T09:44:49.010260+00:00 app[web.1]: I, [2016-08-24T09:44:49.010186 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Rendered layouts/_shim.html.erb (0.4ms)
2016-08-24T09:44:49.010516+00:00 app[web.1]: I, [2016-08-24T09:44:49.010461 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Rendered layouts/_shim.html.erb (0.0ms)
2016-08-24T09:44:49.010642+00:00 app[web.1]: I, [2016-08-24T09:44:49.010591 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Rendered layouts/_headElement.html.erb (7.6ms)
2016-08-24T09:44:49.020206+00:00 app[web.1]: D, [2016-08-24T09:44:49.020136 #5] DEBUG -- : [8cfcee3c-133c-489e-8877-523578821d67] User Load (1.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 103], ["LIMIT", 1]]
2016-08-24T09:44:49.020630+00:00 app[web.1]: I, [2016-08-24T09:44:49.020565 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Rendered layouts/_header.html.erb (3.8ms)
2016-08-24T09:44:49.025024+00:00 app[web.1]: I, [2016-08-24T09:44:49.024957 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Rendered layouts/_footer.html.erb (0.7ms)
2016-08-24T09:44:49.025337+00:00 app[web.1]: I, [2016-08-24T09:44:49.025273 #5] INFO -- : [8cfcee3c-133c-489e-8877-523578821d67] Completed 200 OK in 33ms (Views: 26.1ms | ActiveRecord: 1.8ms)


It finds the member, and renders it, but is no longer mailing. I think it's because of the get route I had to establish to make it 'work'?

Answer

With the following code:

    message += 
      "..."" +
      <%= link_to "here", :controller => :user, :action => :resend_email  %> + 
      "..."

You're trying to use ERB tags in regular ruby - that won't work. You can only use ERB in templates.

Normally I'd advise using Ruby's standard #{} string interpolation but that won't actually solve the problem here.

link_to is only available to views by default, although you can get access to it via the view_context object:

link = "#{view_context.link_to 'here', :controller => :user, :action => :resend_email}"

By the way, it possible to use ERB in controllers if you compile it yourself:

link = ERB.new("<%= view_context.link_to(...) %>").result(binding)

You can change the variables/methods available to ERB by invoking it in a different context, i.e.:

link = view_context.instance_exec do
  ERB.new("<%= link_to(...) %>").result(binding)
end

This works with the standard #{} string interpolation too:

link = view_context.instance_exec do
  "#{link_to(...)}"
end

It's worth mentioning that if you make a custom html string in the controller (like you are doing here with flash), when you display the text on the view you will need to add some custom methods to make the html display as real html:

# in controller
flash[:test] = "<span>some html</span>"

# in view
<%= raw flash[:test].html_safe %>

This way only the text some html would be displayed, and not the entire string <span>some html</span>

There's a reason raw and html_safe is necessary, and that's because printing html has security risks and Rails is designed to make it more difficult.

Say your user sets their username as "<script>alert("hacked")</script>" and this string somehow makes it on to the page as true html. You would have just exposed your users to XSS (Cross Site Scripting), which you don't want to do. So make sure that when you use raw <string>.html_safe you are not displaying anything user-generated.