Skip to content

Commit

Permalink
Merge pull request #171 from blackcandy-org/api
Browse files Browse the repository at this point in the history
Add API for user authentication
  • Loading branch information
aidewoode authored Jun 2, 2022
2 parents f64967b + 282ac1f commit d70b16e
Show file tree
Hide file tree
Showing 20 changed files with 254 additions and 31 deletions.
31 changes: 31 additions & 0 deletions app/controllers/api/v1/api_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Api
module V1
class ApiController < ApplicationController
skip_before_action :verify_authenticity_token

private

# If user already has logged in then authenticate with current session,
# otherwise authenticate with api token.
def find_current_user
Current.user = UserSession.find&.user

return if logged_in?

authenticate_with_http_token do |token, options|
user = User.find_by(api_token: token)
return unless user.present?

# Compare the tokens in a time-constant manner, to mitigate timing attacks.
Current.user = user if ActiveSupport::SecurityUtils.secure_compare(user.api_token, token)
end
end

def require_login
head :unauthorized unless logged_in?
end
end
end
end
31 changes: 31 additions & 0 deletions app/controllers/api/v1/authentications_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Api
module V1
class AuthenticationsController < ApiController
skip_before_action :find_current_user
skip_before_action :require_login

def create
session = UserSession.new(session_params.merge({remember_me: true}).to_h)

if params[:with_session]
head :unauthorized unless session.save
else
head :unauthorized unless session.valid?
end

@current_user = User.find_by(email: session_params[:email])
head :unauthorized unless @current_user.present?

@current_user.regenerate_api_token if @current_user.api_token.blank?
end

private

def session_params
params.require(:user_session).permit(:email, :password)
end
end
end
end
12 changes: 12 additions & 0 deletions app/controllers/api/v1/songs_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Api
module V1
class SongsController < ApiController
def show
@song = Song.find(params[:id])
@song_format = need_transcode?(@song.format) ? Stream::TRANSCODE_FORMAT : @song.format
end
end
end
end
11 changes: 11 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class ApplicationController < ActionController::Base
end
end

rescue_from ActionController::InvalidAuthenticityToken do
logout_current_user
end

def browser
@browser ||= Browser.new(
request.headers["User-Agent"],
Expand Down Expand Up @@ -64,4 +68,11 @@ def require_login
def require_admin
raise BlackCandyError::Forbidden unless is_admin?
end

def logout_current_user
UserSession.find&.destroy
cookies.delete(:user_id)

redirect_to new_session_path
end
end
5 changes: 1 addition & 4 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ def create

def destroy
return unless logged_in?

UserSession.find.destroy
cookies.delete(:user_id)
redirect_to new_session_path
logout_current_user
end

private
Expand Down
5 changes: 0 additions & 5 deletions app/controllers/songs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,4 @@ def index
records = Song.includes(:artist, :album).order(:name)
@pagy, @songs = pagy(records)
end

def show
@song = Song.find(params[:id])
@song_format = need_transcode?(@song.format) ? Stream::TRANSCODE_FORMAT : @song.format
end
end
2 changes: 1 addition & 1 deletion app/javascript/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Player {
this.isPlaying = true;

if (!song.howl) {
fetchRequest(`/songs/${song.id}`)
fetchRequest(`/api/v1/songs/${song.id}`)
.then((response) => {
return response.json();
})
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class User < ApplicationRecord

include ScopedSetting

has_secure_token :api_token
has_setting :theme, default: DEFAULT_THEME

before_create :downcase_email
Expand Down
1 change: 1 addition & 0 deletions app/views/api/v1/authentications/create.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
json.call(@current_user, :api_token)
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# frozen_string_literal: true

json.call(@song, :id, :name, :duration)
json.url new_stream_path(song_id: @song.id)
json.album_name @song.album.title
Expand Down
9 changes: 8 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
resources :stream, only: [:new]
resources :transcoded_stream, only: [:new]
resources :cached_transcoded_stream, only: [:new]
resources :songs, only: [:index, :show]
resources :songs, only: [:index]
resources :albums, only: [:index, :show, :edit, :update], concerns: :playable

resources :users, except: [:show] do
Expand Down Expand Up @@ -49,4 +49,11 @@
get "/404", to: "errors#not_found", as: :not_found
get "/422", to: "errors#unprocessable_entity", as: :unprocessable_entity
get "/500", to: "errors#internal_server_error", as: :internal_server_error

namespace :api do
namespace :v1 do
resource :authentication, only: [:create]
resources :songs, only: [:show]
end
end
end
6 changes: 6 additions & 0 deletions db/migrate/20220531070546_add_api_token_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddApiTokenToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :api_token, :string
add_index :users, :api_token, unique: true
end
end
25 changes: 13 additions & 12 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2021_12_21_081317) do

ActiveRecord::Schema[7.0].define(version: 2022_05_31_070546) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
enable_extension "pg_trgm"
Expand All @@ -20,8 +19,8 @@
create_table "albums", force: :cascade do |t|
t.string "name"
t.string "image"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "artist_id"
t.index ["artist_id", "name"], name: "index_albums_on_artist_id_and_name", unique: true
t.index ["artist_id"], name: "index_albums_on_artist_id"
Expand All @@ -31,17 +30,17 @@
create_table "artists", force: :cascade do |t|
t.string "name"
t.string "image"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "is_various", default: false
t.index ["name"], name: "index_artists_on_name", unique: true
end

create_table "playlists", force: :cascade do |t|
t.string "name"
t.string "type"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "user_id"
t.index ["user_id"], name: "index_playlists_on_user_id"
end
Expand All @@ -65,8 +64,8 @@
t.string "md5_hash", null: false
t.float "duration", default: 0.0, null: false
t.integer "tracknum"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "album_id"
t.bigint "artist_id"
t.index ["album_id"], name: "index_songs_on_album_id"
Expand All @@ -78,11 +77,13 @@
t.string "email", null: false
t.string "crypted_password", null: false
t.boolean "is_admin", default: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.hstore "settings"
t.string "password_salt"
t.string "persistence_token"
t.string "api_token"
t.index ["api_token"], name: "index_users_on_api_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["persistence_token"], name: "index_users_on_persistence_token", unique: true
end
Expand Down
29 changes: 29 additions & 0 deletions test/controllers/api/v1/api_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

require "test_helper"

class Api::V1::ApiControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:visitor1)
@song = songs(:mp3_sample)
end

test "should authenticate when have user session" do
login(@user)
get api_v1_song_url(@song), as: :json

assert_response :success
end

test "should authenticate when have api token" do
get api_v1_song_url(@song), as: :json, headers: api_token_header(@user)

assert_response :success
end

test "should not authenticate when do not have user seesion or api token" do
get api_v1_song_url(@song), as: :json

assert_response :unauthorized
end
end
67 changes: 67 additions & 0 deletions test/controllers/api/v1/authentications_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require "test_helper"

class Api::V1::AuthenticationsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:visitor1)
end

test "should create authentication without session" do
post api_v1_authentication_url, as: :json, params: {
user_session: {
email: @user.email,
password: "foobar"
}
}

response = @response.parsed_body

assert_response :success
assert_nil session[:user_credentials]
assert_equal @user.reload.api_token, response["api_token"]
end

test "should create authentication with session" do
post api_v1_authentication_url, as: :json, params: {
with_session: true,
user_session: {
email: @user.email,
password: "foobar"
}
}

response = @response.parsed_body

assert_response :success
assert_not_nil session[:user_credentials]
assert_equal @user.reload.api_token, response["api_token"]
end

test "should not create authentication with wrong credential" do
post api_v1_authentication_url, as: :json, params: {
user_session: {
email: @user.email,
password: "fake"
}
}

assert_response :unauthorized
assert_nil session[:user_credentials]
assert_empty @response.body
end

test "should not create authentication and session with wrong credential" do
post api_v1_authentication_url, as: :json, params: {
with_session: true,
user_session: {
email: @user.email,
password: "fake"
}
}

assert_response :unauthorized
assert_nil session[:user_credentials]
assert_empty @response.body
end
end
17 changes: 17 additions & 0 deletions test/controllers/api/v1/songs_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require "test_helper"

class Api::V1::SongsControllerTest < ActionDispatch::IntegrationTest
setup do
@song = songs(:mp3_sample)
end

test "should show song" do
get api_v1_song_url(@song), as: :json, headers: api_token_header(users(:visitor1))
response = @response.parsed_body

assert_response :success
assert_equal @song.name, response["name"]
end
end
10 changes: 10 additions & 0 deletions test/controllers/sessions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
assert_empty cookies[:user_id]
assert_redirected_to new_session_url
end

test "should have forgery protection" do
with_forgery_protection do
login @user

assert_nil session[:user_credentials]
assert_nil cookies[:user_id]
assert_redirected_to new_session_url
end
end
end
6 changes: 0 additions & 6 deletions test/controllers/songs_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,4 @@ class SongsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
end

test "should show song" do
assert_login_access(url: song_url(songs(:mp3_sample)), xhr: true) do
assert_response :success
end
end
end
Loading

0 comments on commit d70b16e

Please sign in to comment.