Make Persona the only login option (with the ability to access old accounts)

openid
Marcin Kulik 11 years ago
parent bc2b91f5a2
commit d4cbc3e504

@ -1,8 +1,13 @@
p.notice, p.alert, p.error
background: #396
p.notice, p.alert
color: white
font-size: 15px
height: 38px
line-height: 36px
margin: 0
text-align: center
p.notice
background: #396
p.alert
background: #d52

@ -0,0 +1,26 @@
class AccountMergesController < ApplicationController
def create
user = find_user
if user
user.update_attribute(:email, store.delete(:new_user_email))
self.current_user = user
redirect_back_or_to root_url, notice: 'Welcome!'
else
redirect_to new_user_path,
alert: 'Sorry, no account found. Try a different provider.'
end
end
private
def store
session
end
def find_user
User.for_credentials(omniauth_credentials)
end
end

@ -18,6 +18,8 @@ class ApplicationController < ActionController::Base
end
end
private
def current_user=(user)
if user
@current_user = user
@ -28,12 +30,14 @@ class ApplicationController < ActionController::Base
end
end
private
def ensure_authenticated!
raise Unauthorized unless current_user
end
def omniauth_credentials
OmniAuthCredentials.new(request.env['omniauth.auth'])
end
def store_location
session[:return_to] = request.path
end

@ -3,57 +3,34 @@ class SessionsController < ApplicationController
def new; end
def create
@user = user_from_omniauth
user = find_user
if @user.persisted?
self.current_user = @user
if user
self.current_user = user
redirect_back_or_to root_url, :notice => "Logged in!"
else
store_sensitive_user_data_in_session
store[:new_user_email] = omniauth_credentials.email
redirect_to new_user_path
end
end
def destroy
self.current_user = nil
redirect_to root_url, :notice => "Logged out!"
redirect_to root_path, :notice => "Logged out!"
end
def failure
redirect_to root_url, :alert => "Authentication failed. Maybe try again?"
redirect_to root_path, :alert => "Authentication failed. Maybe try again?"
end
private
def store_sensitive_user_data_in_session
session[:new_user] = {
:provider => @user.provider,
:uid => @user.uid,
:avatar_url => @user.avatar_url
}
def store
session
end
def user_from_omniauth
omniauth = request.env['omniauth.auth']
find_user(omniauth) || build_user(omniauth)
end
def find_user(omniauth)
query = { :provider => omniauth['provider'], :uid => omniauth['uid'].to_s }
User.where(query).first
end
def build_user(omniauth)
user = User.new
user.provider = omniauth['provider']
user.uid = omniauth['uid']
user.nickname = omniauth['info']['nickname']
user.name = omniauth['info']['name'] unless user.provider == 'browser_id'
user.email = omniauth["info"]["email"]
user.avatar_url = OmniAuthHelper.get_avatar_url(omniauth)
user
def find_user
User.for_email(omniauth_credentials.email)
end
end

@ -5,8 +5,7 @@ class UsersController < ApplicationController
before_filter :ensure_authenticated!, :only => [:edit, :update]
def new
@user = User.new
load_sensitive_user_data_from_session
@user = build_user
end
def show
@ -22,15 +21,14 @@ class UsersController < ApplicationController
end
def create
@user = User.new(params[:user])
load_sensitive_user_data_from_session
@user = build_user
if @user.save
clear_sensitive_session_user_data
store.delete(:new_user_email)
self.current_user = @user
redirect_back_or_to root_url, :notice => "Welcome!"
redirect_back_or_to root_path, :notice => "Welcome!"
else
render 'users/new', :status => 422
render :new, :status => 422
end
end
@ -46,17 +44,15 @@ class UsersController < ApplicationController
private
def load_sensitive_user_data_from_session
if session[:new_user]
@user.provider = session[:new_user][:provider]
@user.uid = session[:new_user][:uid]
@user.avatar_url = session[:new_user][:avatar_url]
@user.email = @user.uid if @user.provider == 'browser_id'
end
def store
session
end
def clear_sensitive_session_user_data
session.delete(:new_user)
def build_user
user = User.new(params[:user])
user.email = store[:new_user_email]
user
end
end

@ -0,0 +1,21 @@
class OmniAuthCredentials
attr_reader :omniauth_hash
def initialize(omniauth_hash)
@omniauth_hash = omniauth_hash
end
def provider
omniauth_hash['provider']
end
def uid
omniauth_hash['uid']
end
def email
omniauth_hash['info'] && omniauth_hash['info']['email']
end
end

@ -11,8 +11,19 @@ class UserDecorator < ApplicationDecorator
def img_link(options = {})
link(options) do
h.avatar_image_tag(model)
h.avatar_image_tag(self)
end
end
def avatar_url
model.avatar_url || gravatar_url
end
private
def gravatar_url
hash = Digest::MD5.hexdigest(model.email.to_s.downcase)
"http://gravatar.com/avatar/#{hash}?s=64"
end
end

@ -21,8 +21,8 @@ module ApplicationHelper
def browser_id_user
user = current_user || @user
if user && user.provider == 'browser_id'
"'#{user.uid}'".html_safe
if user
"'#{user.email}'".html_safe
else
'null'
end

@ -5,8 +5,6 @@ class User < ActiveRecord::Base
has_many :comments, :dependent => :destroy
has_many :likes, :dependent => :destroy
validates :provider, :presence => true
validates :uid, :presence => true
validates :nickname, :presence => true
validates_uniqueness_of \
@ -20,6 +18,12 @@ class User < ActiveRecord::Base
attr_accessible :nickname, :email, :name
scope :for_credentials, -> (credentials) {
where(provider: credentials.provider, uid: credentials.uid).first
}
scope :for_email, -> (email) { where(email: email).first }
def to_param
nickname
end

@ -11,14 +11,14 @@
<li><%= link_to image_tag('persona_sign_in_blue.png', :title => "Log in via Persona"), '#', :id => 'persona-button' %></li>
</ul>
<p>
Or just log in with your GitHub or Twitter account:
</p>
<%# <p> %>
<%# Or just log in with your GitHub or Twitter account: %>
<%# </p> %>
<ul id="login">
<li><%= link_to "Log in via Github", github_auth_path, :class => 'btn-auth btn-github' %></li>
<li><%= link_to "Log in via Twitter", twitter_auth_path, :class => 'btn-auth btn-twitter' %></li>
</ul>
<%# <ul id="login"> %>
<%# <li><%= link_to "Log in via Github", github_auth_path, :class => 'btn-auth btn-github' %1></li> %>
<%# <li><%= link_to "Log in via Twitter", twitter_auth_path, :class => 'btn-auth btn-twitter' %1></li> %>
<%# </ul> %>
</div>
<div class="vertical-expander"></div>

@ -1,35 +0,0 @@
<section class="supplimental">
<div class="wrapper">
<%= form_for @user, :url => '/user', :html => { :class => 'form-horizontal' } do |f| %>
<fieldset>
<legend>Your new account</legend>
<div class="control-group <%= 'error' if @user.errors[:nickname].present? %>">
<%= f.label :nickname, 'Username', :class => 'control-label' %>
<div class="controls">
<%= f.text_field :nickname, :class => 'text_field' %>
<span class="help-inline"><%= @user.errors[:nickname].first %></span>
</div>
</div>
<div class="control-group">
<%= f.label :name, 'Real name', :class => 'control-label' %>
<div class="controls">
<%= f.text_field :name, :class => 'text_field' %>
</div>
</div>
<div class="control-group">
<%= f.label :email, :class => 'control-label' %>
<div class="controls">
<%= f.text_field :email, :class => 'text_field' %>
</div>
</div>
<div class="form-actions">
<%= f.submit 'Create', :class => 'btn btn-primary' %>
</div>
</fieldset>
<% end %>
</div>
</section>

@ -0,0 +1,32 @@
section.supplimental
.wrapper
p There is no account with email #{@user.email}.
h2 That's because I am here for the first time
h3 Pick your username:
= simple_form_for @user, :url => user_path do |f|
= f.input :nickname, label: 'Username', required: true
= f.button :submit
hr
/* p */
/* | We recommend logging in with your email using */
/* = link_to "Mozilla Persona", "https://login.persona.org/" */
/* | . Look */
/* = link_to "here", "https://developer.mozilla.org/en-US/docs/Persona" */
/* | for more details. */
h2 That's because I've been logging in via Github/Twitter
p
| If you have been previously logging in via Github or Twitter
| pls do it again. One time!
ul.login
li = link_to "Log in via Github", github_auth_path, :class => 'btn-auth btn-github'
li = link_to "Log in via Twitter", twitter_auth_path, :class => 'btn-auth btn-twitter'
.vertical-expander

@ -14,7 +14,8 @@ Asciinema::Application.routes.draw do
get "/docs" => "docs#show", :page => 'gettingstarted', :as => :docs_index
get "/docs/:page" => "docs#show", :as => :docs
get "/auth/:provider/callback" => "sessions#create"
get "/auth/browser_id/callback" => "sessions#create"
get "/auth/:provider/callback" => "account_merges#create"
get "/auth/failure" => "sessions#failure"
get "/login" => "sessions#new"

@ -0,0 +1,6 @@
class AllowNullForUserProviderAndUid < ActiveRecord::Migration
def change
change_column :users, :provider, :string, null: true
change_column :users, :uid, :string, null: true
end
end

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20131011181418) do
ActiveRecord::Schema.define(version: 20131019165955) do
create_table "asciicasts", force: true do |t|
t.integer "user_id"
@ -85,8 +85,8 @@ ActiveRecord::Schema.define(version: 20131011181418) do
add_index "user_tokens", ["user_id"], name: "index_user_tokens_on_user_id", using: :btree
create_table "users", force: true do |t|
t.string "provider", null: false
t.string "uid", null: false
t.string "provider"
t.string "uid"
t.string "email"
t.string "name"
t.string "avatar_url"

@ -1,16 +0,0 @@
class OmniAuthHelper
def self.get_avatar_url(auth)
case auth["provider"]
when "twitter"
auth["info"]["image"]
when "github"
auth["extra"]["raw_info"]["avatar_url"]
when "browser_id"
email = auth["uid"]
hash = Digest::MD5.hexdigest(email.to_s.downcase)
"http://gravatar.com/avatar/#{hash}?s=64"
end
end
end

@ -0,0 +1,65 @@
require 'spec_helper'
describe AccountMergesController do
describe '#create' do
subject { get :create, provider: 'twitter' }
let(:credentials) { double('credentials') }
let(:store) { {} }
before do
allow(controller).to receive(:omniauth_credentials) { credentials }
allow(controller).to receive(:store) { store }
allow(User).to receive(:for_credentials).with(credentials) { user }
store[:new_user_email] = 'foo@bar.com'
end
context "when user can be found for given credentials" do
let(:user) { stub_model(User) }
before do
allow(user).to receive(:update_attribute)
allow(controller).to receive(:current_user=)
subject
end
it 'updates the email on the user' do
expect(user).to have_received(:update_attribute).
with(:email, 'foo@bar.com')
end
it 'removes the email from the store' do
expect(store.key?(:new_user_email)).to be(false)
end
it 'sets the current_user' do
expect(controller).to have_received(:current_user=).with(user)
end
it 'redirects to the root_path with a notice' do
expect(flash[:notice]).to_not be_blank
should redirect_to(root_path)
end
end
context "when user can't be found for given credentials" do
let(:user) { nil }
before do
subject
end
it "doesn't remove the email from the store" do
expect(store.key?(:new_user_email)).to be(true)
end
it 'redirects to new user page with an alert' do
expect(flash[:alert]).to_not be_blank
should redirect_to(new_user_path)
end
end
end
end

@ -2,89 +2,80 @@ require 'spec_helper'
describe SessionsController do
describe "#create" do
subject { get :create, :provider => 'twitter' }
let(:store) { {} }
shared_examples_for 'successful path' do
it "creates the session" do
subject
before do
allow(controller).to receive(:store) { store }
end
expect(controller).to have_received(:current_user=).with(user)
end
describe '#new' do
subject { get :new }
it "sets the flash message" do
subject
it 'renders "new" template' do
should render_template('new')
end
end
expect(flash[:notice]).to eq('Logged in!')
end
describe '#create' do
subject { get :create, provider: 'twitter' }
it "redirects to the root url" do
expect(subject).to redirect_to(root_url)
end
end
let(:credentials) { double('credentials', email: 'foo@bar.com') }
before do
request.env['asciiio.user'] = user
allow(controller).to receive(:current_user=)
allow(controller).to receive(:omniauth_credentials) { credentials }
allow(User).to receive(:for_email).with('foo@bar.com') { user }
end
context "when user was persisted" do
let(:user) { mock_model(User) }
context "when user can be found for given credentials" do
let(:user) { stub_model(User) }
it_behaves_like 'successful path'
end
before do
allow(controller).to receive(:current_user=)
context "when user doesn't exist" do
let(:user_attributes) { {
:uid => '1234',
:provider => 'github',
:avatar_url => 'http://foo'
} }
let(:user) { mock_model(User, user_attributes).as_new_record }
subject
end
context "and creation succeeds" do
before do
allow(user).to receive(:save) { true }
end
it 'sets the current_user' do
expect(controller).to have_received(:current_user=).with(user)
end
it_behaves_like 'successful path'
it 'redirects to the root_path with a notice' do
expect(flash[:notice]).to_not be_blank
should redirect_to(root_path)
end
end
context "and creation fails" do
before do
allow(user).to receive(:save) { false }
end
context "when user can't be found for given credentials" do
let(:user) { nil }
it "stores uid and provider in session " do
subject
before do
subject
end
expect(session[:new_user]).to eq({
:uid => '1234', :provider => 'github', :avatar_url => 'http://foo'
})
end
it 'stores the email' do
expect(store[:new_user_email]).to eq('foo@bar.com')
end
it "renders users/new" do
expect(subject).to render_template('users/new')
end
it 'redirects to the new user page' do
should redirect_to(new_user_path)
end
end
end
describe "#destroy" do
before do
session[:user_id] = "123"
allow(controller).to receive(:current_user=)
get :destroy
end
it "should destroy session" do
session[:user_id].should be_nil
@controller.current_user.should be_nil
it 'sets current_user to nil' do
expect(controller).to have_received(:current_user=).with(nil)
end
it "should redirects to root_url" do
flash[:notice].should == "Logged out!"
should redirect_to(root_url)
it "redirects to root_path with a notice" do
expect(flash[:notice]).to_not be_blank
should redirect_to(root_path)
end
end
@ -93,9 +84,9 @@ describe SessionsController do
get :failure
end
it "should redirect to root_url and set error message" do
flash[:alert].should =~ /Authentication failed/
should redirect_to(root_url)
it "redirects to root_url with an alert" do
expect(flash[:alert]).to_not be_blank
should redirect_to(root_path)
end
end

@ -2,62 +2,85 @@ require 'spec_helper'
describe UsersController do
describe "#create" do
let(:user) { mock_model(User).as_null_object }
let(:store) { {} }
before do
allow(controller).to receive(:store) { store }
end
describe '#new' do
before do
User.stub(:new).and_return(user)
store[:new_user_email] = 'foo@bar.com'
get :new
end
it 'assigns user with a stored email' do
expect(assigns(:user).email).to eq('foo@bar.com')
end
context "when user saved" do
let(:provider) { 'foo' }
let(:uid) { '123' }
let(:avatar_url) { 'url' }
it 'renders new template' do
should render_template('new')
end
end
describe "#create" do
let!(:user) { stub_model(User) }
subject { post :create, user: { nickname: 'jola' } }
before do
allow(controller).to receive(:current_user=)
allow(User).to receive(:new).with('nickname' => 'jola') { user }
store[:new_user_email] = 'foo@bar.com'
end
context "when user is persisted" do
before do
session[:new_user] = {
:provider => provider,
:uid => uid,
:avatar_url => avatar_url
}
allow(user).to receive(:save) { true }
user.stub(:save => true)
subject
end
it "assigns provider and uid" do
user.should_receive(:provider=).with(provider).and_return(true)
user.should_receive(:uid=).with(uid).and_return(true)
user.should_receive(:avatar_url=).with(avatar_url).and_return(true)
it 'removes the email from the store' do
expect(store.key?(:new_user_email)).to be(false)
end
post :create
it 'assigns the stored email to the user' do
expect(user.email).to eq('foo@bar.com')
end
it "sets current_user" do
post :create
@controller.current_user.should_not be_nil
it 'sets the current_user' do
expect(controller).to have_received(:current_user=).with(user)
end
it "clears user session data" do
post :create
it 'redirects to the root_path with a notice' do
expect(flash[:notice]).to_not be_blank
should redirect_to(root_path)
end
end
session[:new_user].should be_nil
context "when user isn't persisted" do
before do
allow(user).to receive(:save) { false }
subject
end
it "redirects back" do
post :create
should redirect_to(root_url)
it "doesn't remove the email from the store" do
expect(store.key?(:new_user_email)).to be(true)
end
end
it "doesn't set the current_user" do
expect(controller).to_not have_received(:current_user=)
end
context "when not valid data" do
before do
user.stub(:save => false)
it 'assigns user with a stored email' do
expect(assigns(:user).email).to eq('foo@bar.com')
end
it "renders user/new" do
post :create
should render_template('users/new')
it 'renders "new" template' do
should render_template('new')
end
end
end
@ -73,4 +96,5 @@ describe UsersController do
describe '#update' do
it 'should have specs'
end
end

@ -0,0 +1,40 @@
require 'spec_helper'
describe OmniAuthCredentials do
let(:credentials) { described_class.new(omniauth_hash) }
let(:omniauth_hash) { {
'provider' => 'twitter',
'uid' => '1234567',
'info' => { 'email' => 'foo@bar.com' }
} }
describe '#provider' do
subject { credentials.provider }
it { should eq('twitter') }
end
describe '#uid' do
subject { credentials.uid }
it { should eq('1234567') }
end
describe '#email' do
subject { credentials.email }
it { should eq('foo@bar.com') }
context "when no info section in hash" do
let(:omniauth_hash) { {
'provider' => 'twitter',
'uid' => '1234567'
} }
it { should be(nil) }
end
end
end

@ -2,58 +2,12 @@ require 'spec_helper'
feature "User session" do
scenario "Logging in when nickname isn't returned by oauth provider" do
set_omniauth(:github)
visit root_path
click_on 'Log in'
click_on 'Log in via Github'
fill_in 'Username', :with => 'foobar'
click_button 'Create'
expect(page).to have_content('Logged in!')
within('header') do
expect(page).to have_link('foobar')
end
end
scenario 'Logging in when nickname is returned by oauth provider' do
set_omniauth(:github, :nickname => 'hasiok')
visit root_path
click_on 'Log in'
click_on 'Log in via Github'
expect(page).to have_content('Logged in!')
within('header') do
expect(page).to have_link('hasiok')
end
end
scenario 'Logging in when error is returned by oauth provider' do
set_omniauth(:github, :message => :access_denied)
visit root_path
click_on 'Log in'
click_on 'Log in via Github'
expect(page).to have_content('Authentication failed')
end
scenario 'Logging out' do
set_omniauth(:github, :nickname => 'hasiok')
scenario "Creating a session" do
visit root_path
click_on 'Log in'
click_on 'Log in via Github'
click_on 'Log out'
expect(page).to have_content('Logged out!')
expect(page).to have_content('Persona')
end
end

@ -1,44 +0,0 @@
require 'spec_helper'
describe OmniAuthHelper do
describe ".avatar_url" do
let(:avatar_url) { "http://foo.bar/foo.png" }
context "when github auth" do
let(:auth) do
{
"provider" => "github",
"extra" => {
"raw_info" => {
"avatar_url" => avatar_url
}
}
}
end
it { OmniAuthHelper.get_avatar_url(auth).should == avatar_url }
end
context "when twitter auth" do
let(:auth) do
{
"provider" => "twitter",
"info" => {
"image" => avatar_url
}
}
end
it { OmniAuthHelper.get_avatar_url(auth).should == avatar_url }
end
context "when other provider" do
let(:auth) do
{ "provider" => "other" }
end
it { OmniAuthHelper.get_avatar_url(auth).should be_nil }
end
end
end

@ -1,16 +0,0 @@
require 'spec_helper'
describe "users/new" do
let(:user) { FactoryGirl.build(:user) }
before do
assign(:user, user)
end
it "renders form with attr" do
render
rendered.should =~ /user\[nickname\]/
rendered.should =~ /user\[name\]/
rendered.should =~ /user\[email\]/
end
end
Loading…
Cancel
Save