diff --git a/.gitignore b/.gitignore
index 5b61ab0..457fe60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,6 @@
/log/*
!/log/.keep
/tmp
+
+# Ignore application configuration
+/config/application.yml
diff --git a/Gemfile b/Gemfile
index 10ce034..855d124 100644
--- a/Gemfile
+++ b/Gemfile
@@ -27,6 +27,10 @@ gem 'sdoc', '~> 0.4.0', group: :doc
# Use Unicorn as the app server
gem 'unicorn'
+gem 'omniauth-oauth2'
+
+gem 'figaro'
+
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
diff --git a/Gemfile.lock b/Gemfile.lock
index dc36f86..820e935 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -68,8 +68,13 @@ GEM
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
+ faraday (0.10.1)
+ multipart-post (>= 1.2, < 3)
+ figaro (1.1.1)
+ thor (~> 0.14)
globalid (0.3.7)
activesupport (>= 4.1.0)
+ hashie (3.5.1)
i18n (0.8.0)
jbuilder (2.6.1)
activesupport (>= 3.0.0, < 5.1)
@@ -79,6 +84,7 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
+ jwt (1.5.6)
kgio (2.11.0)
launchy (2.4.3)
addressable (~> 2.3)
@@ -92,8 +98,22 @@ GEM
mini_portile2 (2.1.0)
minitest (5.10.1)
multi_json (1.12.1)
+ multi_xml (0.6.0)
+ multipart-post (2.0.0)
nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0)
+ oauth2 (1.3.0)
+ faraday (>= 0.8, < 0.11)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ multi_xml (~> 0.5)
+ rack (>= 1.2, < 3)
+ omniauth (1.4.1)
+ hashie (>= 1.2, < 4)
+ rack (>= 1.0, < 3)
+ omniauth-oauth2 (1.4.0)
+ oauth2 (~> 1.0)
+ omniauth (~> 1.2)
pg (0.19.0)
public_suffix (2.0.5)
rack (1.6.5)
@@ -194,9 +214,11 @@ DEPENDENCIES
coffee-rails (~> 4.1.0)
database_cleaner
factory_girl_rails
+ figaro
jbuilder (~> 2.0)
jquery-rails
launchy
+ omniauth-oauth2
pg (~> 0.15)
rails (= 4.2.6)
rails_12factor
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d83690e..b7633d9 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,4 +2,10 @@ class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
+
+ helper_method :current_user
+
+ def current_user
+ @current_user ||= User.find(session[:user_id]) if session[:user_id]
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
new file mode 100644
index 0000000..2484f4a
--- /dev/null
+++ b/app/controllers/sessions_controller.rb
@@ -0,0 +1,14 @@
+class SessionsController < ApplicationController
+ def create
+ @user = User.find_or_create_from_auth(request.env['omniauth.auth'])
+ if @user
+ session[:user_id] = @user.id
+ redirect_to playlists_path
+ end
+ end
+
+ def destroy
+ session.clear
+ redirect_to playlists_path
+ end
+end
diff --git a/app/models/playlist.rb b/app/models/playlist.rb
index 34ecad3..f192fbe 100644
--- a/app/models/playlist.rb
+++ b/app/models/playlist.rb
@@ -1,4 +1,7 @@
class Playlist < ActiveRecord::Base
+ validates :name, presence: true, uniqueness: true
+
has_many :playlist_songs
has_many :songs, through: :playlist_songs
+
end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..0ff8f12
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,9 @@
+class User < ActiveRecord::Base
+ def self.find_or_create_from_auth(auth)
+ User.find_or_create_by(
+ provider: auth["provider"],
+ uid: auth["info"]["id"],
+ name: auth["info"]["display_name"]
+ )
+ end
+end
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 1146239..571ca1e 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -8,6 +8,12 @@
+ <% if current_user %>
+ Welcome, <%= current_user.name %>
+ <%= link_to "Sign out", logout_path, method: :delete %>
+ <% else %>
+ <%= link_to "Sign in with Spotify", "/auth/spotify" %>
+ <% end %>
<%= yield %>
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
new file mode 100644
index 0000000..8e40cc4
--- /dev/null
+++ b/config/initializers/omniauth.rb
@@ -0,0 +1,27 @@
+require 'omniauth-oauth2'
+
+module OmniAuth
+ module Strategies
+ class Spotify < OmniAuth::Strategies::OAuth2
+ option :name, 'spotify'
+
+ option :client_options, {
+ site: 'https://api.spotify.com/v1/',
+ authorize_url: 'https://accounts.spotify.com/authorize',
+ token_url: 'https://accounts.spotify.com/api/token'
+ }
+
+ def info
+ @raw_info ||= access_token.get('me').parsed
+ end
+
+ def callback_url
+ full_host + script_name + callback_path
+ end
+ end
+ end
+end
+
+Rails.application.config.middleware.use OmniAuth::Builder do
+ provider :spotify, ENV['SPOTIFY_KEY'], ENV['SPOTIFY_SECRET']
+end
diff --git a/config/routes.rb b/config/routes.rb
index 2d4eb95..7600f07 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -5,6 +5,10 @@
resources :songs, only: [:show, :edit, :update]
resources :playlists
+
+ get '/auth/spotify/callback', to: 'sessions#create'
+
+ delete '/logout', to: 'sessions#destroy'
end
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
diff --git a/db/migrate/20170209152757_create_users.rb b/db/migrate/20170209152757_create_users.rb
new file mode 100644
index 0000000..c4cb589
--- /dev/null
+++ b/db/migrate/20170209152757_create_users.rb
@@ -0,0 +1,11 @@
+class CreateUsers < ActiveRecord::Migration
+ def change
+ create_table :users do |t|
+ t.string :name
+ t.string :provider
+ t.string :uid
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e980c57..937754d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170209023045) do
+ActiveRecord::Schema.define(version: 20170209152757) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -46,6 +46,14 @@
t.integer "artist_id"
end
+ create_table "users", force: :cascade do |t|
+ t.string "name"
+ t.string "provider"
+ t.string "uid"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
add_foreign_key "playlist_songs", "playlists"
add_foreign_key "playlist_songs", "songs"
end
diff --git a/spec/controllers/artists_controller_spec.rb b/spec/controllers/artists_controller_spec.rb
new file mode 100644
index 0000000..0ce2d39
--- /dev/null
+++ b/spec/controllers/artists_controller_spec.rb
@@ -0,0 +1,123 @@
+require 'rails_helper'
+
+RSpec.describe ArtistsController, type: :controller do
+ describe "GET #index" do
+ it "assigns all artists as @artists and renders the index template" do
+ artist = create(:artist)
+
+ get(:index)
+
+ expect(assigns(:artists)).to eq([artist])
+ expect(response).to render_template("index")
+ end
+ end
+
+ describe "GET#show" do
+ it "assigns the requested artist as @artist and renders the show template" do
+ artist = create(:artist)
+ get(:show, {:id => artist.to_param})
+ expect(assigns(:artist)).to eq(artist)
+ expect(response).to render_template("show")
+ end
+ end
+
+ describe "GET#new" do
+ it "assigns the new artist as @artist and renders the show template" do
+ get(:new)
+ expect(assigns(:artist)).to be_a_new(Artist)
+ end
+ end
+
+ describe "GET#edit" do
+ it "assigns the requested artist as @artist" do
+ artist = create(:artist)
+ get(:edit, {:id => artist.to_param})
+ expect(assigns(:artist)).to eq(artist)
+ end
+ end
+
+ describe "POST#create" do
+ context "with valid params" do
+ it "creates a new artist" do
+ expect {
+ post :create, {:artist => attributes_for(:artist)}
+ }.to change(Artist, :count).by(1)
+ end
+
+ it "assigns a newly created artist as @artist" do
+ post :create, {:artist => attributes_for(:artist)}
+ expect(assigns(:artist)).to be_a(Artist)
+ expect(assigns(:artist)).to be_persisted
+ end
+
+ it "redirects to the created artist" do
+ post :create, {:artist => attributes_for(:artist)}
+ expect(response).to redirect_to(Artist.last)
+ end
+
+ context "with invalid params" do
+ it "assigns a newly created but unsaved artist as @artist" do
+ post :create, {:artist => attributes_for(:artist, name: nil)}
+ expect(assigns(:artist)).to be_a_new(Artist)
+ end
+
+ it "re-renders the 'new' template" do
+ post :create, {:artist => attributes_for(:artist, name: nil)}
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+ end
+
+ describe "PUT#update" do
+ context "with valid params" do
+ it "updates the requested artist" do
+ artist = create(:artist)
+ put :update, {:id => artist.to_param, :artist => attributes_for(:artist, name: "New name")}
+ artist.reload
+ expect(artist.name).to eq("New name")
+ end
+
+ it "assigns the requested artist as @artist" do
+ artist = create(:artist)
+ put :update, {:id => artist.to_param, :artist => attributes_for(:artist, name: "New name")}
+ expect(assigns(:artist)).to eq(artist)
+ end
+
+ it "redirects to the artist" do
+ artist = create(:artist)
+ put :update, {:id => artist.to_param, :artist => attributes_for(:artist, name: "New name")}
+ expect(response).to redirect_to(artist)
+ end
+ end
+
+ context "with invalid params" do
+ it "assigns the artist as @artist" do
+ artist = create(:artist)
+ put :update, {:id => artist.to_param, :artist => attributes_for(:artist, name:nil)}
+ expect(assigns(:artist)).to eq(artist)
+ end
+
+ it "re-render the 'edit' template" do
+ artist = create(:artist)
+ put :update, {:id => artist.to_param, :artist => attributes_for(:artist, name: nil)}
+ expect(response).to render_template("edit")
+ end
+ end
+ end
+
+ describe "DELETE#destroy" do
+ it "destroys the requested artist" do
+ artist = create(:artist)
+ expect {
+ delete :destroy, {:id => artist.to_param}
+ }.to change(Artist, :count).by(-1)
+ end
+
+ it "redirects to the artists list" do
+ artist = create(:artist)
+ delete :destroy, {:id => artist.to_param}
+ expect(response).to redirect_to(artists_path)
+ end
+ end
+end
diff --git a/spec/controllers/playlists_controller_spec.rb b/spec/controllers/playlists_controller_spec.rb
new file mode 100644
index 0000000..13a1064
--- /dev/null
+++ b/spec/controllers/playlists_controller_spec.rb
@@ -0,0 +1,72 @@
+# require 'rails_helper'
+#
+# RSpec.describe PlaylistsController, type: :controller do
+# describe "POST#create" do
+# context "with valid params" do
+# expect {
+# post :create, {:playlist => attributes_for(:playlist)}
+# }.to change(Playlist, :count).by(1)
+# end
+#
+# it "assigns a newly created playlist as @playlist" do
+# post :create, {:playlist => attributes_for(:playlist)}
+# expect(assigns(:playlist)).to be_a(Playlist)
+# expect(assigns(:playlist)).to be_persisted
+# end
+#
+# it "redirects to the created playlist" do
+# post :create, {:playlist => attributes_for(:playlist)}
+# expect(response).to redirect_to(Playlist.last)
+# end
+#
+# context "with invalid params" do
+# it "assigns a newly created but unsaved playlist as @playlist" do
+# post :create, {:playlist => attributes_for(:playlist, name: nil)}
+# expect(assigns(:playlist)).to be_a_new(Playlist)
+# end
+#
+# it "re-renders the 'new' template" do
+# post :create, {:playlist =>attributes_for(:playlist, name: nil)}
+# expect(response).to render_template(:new)
+# end
+# end
+# end
+#
+# describe "PUT#update" do
+# context "with valid params" do
+# it "updates the requested artist" do
+# playlist = create(:playlist)
+# put :update, {:id => playlist.to_param, :playlist => attributes_for(:playlist, name: "New name")}
+# playlist.reload
+# expect(playlist.name).to eq("New name")
+# end
+#
+# it "assigns the requested playlist as @playlist" do
+# playlist = create(:playlist)
+# put :update, {:id => playlist.to_param, :playlist => attributes_for(:playlist, name: "New name")}
+# expect(assigns(:playlist)).to eq(playlist)
+# end
+#
+# it "redirects to the playlist" do
+# playlist = create(:playlist)
+# put :update, {:id => playlist.to_param, :playlist => attributes_for(:playlist, name: "New name")}
+# expect(response).to redirect_to(playlist)
+# end
+# end
+#
+# context "with invalid params" do
+# it "assigns the playlist as @playlist" do
+# playlist = create(:playlist)
+# put :update, {:id => playlist.to_param, :playlist => attributes_for(:playlist, name:nil)}
+# expect(assigns(:playlist)).to eq(playlist)
+# end
+#
+# it "re-render the 'edit' template" do
+# artist = create(:artist)
+# put :update, {:id => artist.to_param, :artist => attributes_for(:artist, name: nil)}
+# expect(response).to render_template("edit")
+# end
+# end
+# end
+#
+# end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
new file mode 100644
index 0000000..9670114
--- /dev/null
+++ b/spec/factories/users.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :user do
+ name "MyString"
+ provider "MyString"
+ uid "MyString"
+ end
+end
diff --git a/spec/features/user_signs_in_spec.rb b/spec/features/user_signs_in_spec.rb
new file mode 100644
index 0000000..65a624b
--- /dev/null
+++ b/spec/features/user_signs_in_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+RSpec.feature "User signs in with Spotify" do
+ scenario "they see a link to sign out" do
+ auth_data = {
+ 'provider' => 'spotify',
+ 'info' => {
+ 'display_name' => 'Fake User',
+ 'id' => '12345'
+ }
+ }
+
+ OmniAuth.config.mock_auth[:spotify] = auth_data
+
+ visit playlists_path
+ click_link "Sign in with Spotify"
+ expect(page).to have_content("Sign out")
+ expect(page).to have_content(auth_data['info']['display_name'])
+ expect(page).to_not have_content("Sign in with Spotify")
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
new file mode 100644
index 0000000..47a31bb
--- /dev/null
+++ b/spec/models/user_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe User, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 47f50b3..fb86789 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -14,6 +14,8 @@
end
end
+OmniAuth.config.test_mode = true
+
# Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in