Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rails like session/flash management for Amber. #138

Merged
merged 17 commits into from
Jul 18, 2017
Merged

Conversation

eliasjpr
Copy link
Contributor

@eliasjpr eliasjpr commented Jul 15, 2017

Description of the Change

This PR adds Rails like session/flash management to Amber.

I have used the following amber blog app to test the functionality https://github.com/eliasjpr/blog

The Session

A session store that uses an Amber:Router::Session::Store to store the sessions. This store is most useful if you don't store critical data in your sessions and you don't need them to live for extended periods of time.

This cookie-based session store is the Amber default. It is dramatically faster than the alternatives.

The cookie store used for storage is automatically configured to be the best possible option given your application's configuration.

Sessions typically contain at most a user_id and flash message; both fit within the 4K cookie size limit. A CookieOverflow exception is raised if you attempt to store more than 4K of data.

To configure the session:

#  Cookie Store
Amber::Server.instance.session = {
  :key     => "name.session",
  :store   => :cookie,
  :expires => 120, # 0, will make the session last as long as the browser is open, upon closing, session will be terminated
  :secret  => "secret"
}

# Redis Store
Amber::Server.instance.session = {
  :key       => "name.session",
  :store     => :redis,
  :expires   => 120,
  :secret    => "secret",
  :redis_url => "redis://localhost:6379",
}

Configure the Pipeline

 # Keep in mind the order of the Pipes. Session hash needs to be populated before trying to access the session flash scope, the flash depends on the session. 
 pipeline :web do
    plug Amber::Pipe::Session.new
    plug Amber::Pipe::Logger.new
    plug Amber::Pipe::Flash.new
    plug Amber::Pipe::CSRF.new
  end

Accessing the session

class ApplicationController < Amber::Controller::Base
 
  private
 
  # Finds the User with the ID stored in the session with the key
  # :current_user_id This is a common way to handle user login in
  # a Amber application; logging in sets the session value and
  # logging out removes it.
  def current_user
    @_current_user ||= session[:current_user_id] &&
      User.find_by(id: session[:current_user_id])
  end
end

To store something in the session, just assign it to the key like a hash:

class LoginsController < ApplicationController
  # "Create" a login, aka "log the user in"
  def create
    if user = User.authenticate(params[:username], params[:password])
      # Save the user ID in the session so it can be used in
      # subsequent requests
      session[:current_user_id] = user.id
      redirect_to root_url
    end
  end
end

To remove something from the session, assign that key to be nil or use session.delete(key)

class LoginsController < ApplicationController
  # "Delete" a login, aka "log the user out"
  def destroy
    # Remove the user id from the session
    @_current_user = session[:current_user_id] = nil
    redirect_to root_url
  end
end

The Flash

The flash is a special part of the session which is cleared with each request. This means that values stored there will only be available on the next request, which is useful for passing error messages etc.

It is accessed in much the same way as the session, as a hash.

Let's use the act of logging out as an example. The controller can send a message which will be displayed to the user on the next request:

class LoginsController < ApplicationController
  def destroy
    session[:current_user_id] = nil
    #  Alternatively, `flash.notice=` could be use.
    flash[:notice] = "You have successfully logged out."
    redirect_to root_url
  end
end

Rendering the flash message

<html>
  <!-- <head/> -->
  <body>
    <% flash.each do |name, msg| -%>
      <%= content_tag :div, msg, class: name %>
    <% end -%>
 
    <!-- more content -->
  </body>
</html>

If you want a flash value to be carried over to another request, use the keep method:

class MainController < ApplicationController
  # Let's say this action corresponds to root_url, but you want
  # all requests here to be redirected to UsersController#index.
  # If an action sets the flash and redirects here, the values
  # would normally be lost when another redirect happens, but you
  # can use 'keep' to make it persist for another request.
  def index
    # Will persist all flash values.
    flash.keep
 
    # You can also use a key to keep only some kind of value.
    # flash.keep(:notice)
    redirect_to users_url
  end
end

Flash.now

By default, adding values to the flash will make them available to the next request, but sometimes you may want to access those values in the same request. For example, if the create action fails to save a resource and you render the new template directly, that's not going to result in a new request, but you may still want to display a message using the flash. To do this, you can use flash.now in the same way you use the normal flash.

class ClientsController < ApplicationController
  def create
    @client = Client.new(params[:client])
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end

Possible Drawbacks

Current all sessions are encrypted. It has been proposed to sign and/or only encrypt by passing a config param.

@eliasjpr eliasjpr force-pushed the ep/session-storage branch from fcbfeb9 to 893ccae Compare July 15, 2017 20:58
@@ -1,4 +1,4 @@
language: generic
language: crystal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be in a different PR. I know Dru was in favor of using docker which was why I used this fix. Crystal Core team also uses docker for their travis tests.

Copy link
Contributor Author

@eliasjpr eliasjpr Jul 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to remove it since the crystal v0.23.1 works on Travis. Redis server was not being installed with the docker option causing the specs for this branch to fail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend adding the redis service to dockercompose.yml and reenabling docker for running our tests. This will eliminate failures because of differences between environments and gives us finer control of the testing environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@drujensen I would not know how to do this. Let me know when you have some time I would like if you can walk me thru this setup.

Copy link
Member

@elorest elorest left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I'd definitely change the expires time though. A session isn't very useful if it expires in 2 hours.

src/amber.cr Outdated
@session = {
:key => "session_id",
:store => :cookie,
:expires => 2 * 60 * 60, # 2 Hours
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session should last as long as the browser is open like the other MVC's instead of 2 hours.

def find_entry(key)
super(key.to_s)
end
context.session.set_session
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing cookies is expensive time wise, now more than ever due to encryption. We should only save cookies if something has changed.

Copy link
Contributor Author

@eliasjpr eliasjpr Jul 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's iterate over this in a separate PR. On those upcoming PRs the signed and encryption should be addressed as well

@@ -0,0 +1,65 @@
module Amber::Router::Session
class SessionHash < Hash(String, String)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that you utilized this.

@eliasjpr eliasjpr force-pushed the ep/session-storage branch from b938056 to 825d6de Compare July 15, 2017 22:05
@eliasjpr eliasjpr force-pushed the ep/session-storage branch from 8f31748 to 08cc2d4 Compare July 15, 2017 22:13
html_output = <<-HTML
<form action="/posts" method="post">
<input type="hidden" name="_csrf" value="#{Amber::Pipe::CSRF.new.token(context)}" />
<input type="hidden" name="_csrf" value="#{csrf_token}" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this more.

Copy link
Contributor

@fridgerator fridgerator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will defer to @elorest comments, but I like where this is headed 👍

@eliasjpr eliasjpr force-pushed the ep/session-storage branch from 08cc2d4 to e9d49a1 Compare July 16, 2017 12:23
@eliasjpr
Copy link
Contributor Author

eliasjpr commented Jul 16, 2017

Comments have been addressed. When setting expires: 0 to the session settings, sessions will only last until the browser is closed.
screen_shot_2017-07-16_at_8_53_08_am

@drujensen
Copy link
Member

LGTM

Please squash the commits before merging.

Add tickets for re-enabling cookie optimization and encryption options.

Copy link
Member

@drujensen drujensen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a new amber project and scaffolding:

amber new blog -d mysql --deps
cd blog
amber g scaffold post title:string body:text

I updated amber to point to this branch:
branch: ep/session-storage
shards update

I added the config for cookie session to the config/application.cr

Amber::Server.instance.session = {
  :key     => "blog.session",
  :store   => :cookie,
  :expires => 120,
  :secret  => "secret"
}

I launched the app and created a new blog post.

The flash message for flash["success"] is not being displayed as expected.

@eliasjpr
Copy link
Contributor Author

eliasjpr commented Jul 17, 2017

@drujensen the reason that the flash message is not being rendered for the new generated app is that the session pipe needs to come before the flash and csrf pipe, as stated above in the PR description.

Currently the amber new {project name} will add the session to the end of the pipeline.

 # Keep in mind the order of the Pipes. Session hash needs to be populatedd before trying to access the session flash scope, the flash depends of the session. 
 pipeline :web do
    plug Amber::Pipe::Session.new
    plug Amber::Pipe::Logger.new
    plug Amber::Pipe::Flash.new
    plug Amber::Pipe::CSRF.new
  end

@drujensen
Copy link
Member

@eliasjpr moving the session fixed it. I also tested with redis and everything seems to be working correctly.

Copy link
Member

@elorest elorest left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok we can deal with slow downs and options for encrypted vs signed later. Lets get this merged.

@eliasjpr eliasjpr merged commit c540fbb into master Jul 18, 2017
@eliasjpr eliasjpr deleted the ep/session-storage branch July 18, 2017 15:13
marksiemers pushed a commit to marksiemers/amber that referenced this pull request Oct 28, 2017
* Adds Session Management

- Adds Cookies Session using new cookies storage
- Adds Redis Session storage

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management
elorest pushed a commit that referenced this pull request Nov 17, 2017
* Adds Session Management

- Adds Cookies Session using new cookies storage
- Adds Redis Session storage

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management

* fixup! Adds Session Management
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants