From 728e1da7eaa0ff5f2f1820bf45b1796245ba5f22 Mon Sep 17 00:00:00 2001 From: Pascal Zumkehr Date: Wed, 8 Mar 2017 22:29:09 +0100 Subject: [PATCH] refactor controllers into base and admin namespaces --- .../archive_formats_controller.rb | 10 +- .../audio_encodings_controller.rb | 10 +- app/controllers/admin/authenticatable.rb | 25 + .../downgrade_actions_controller.rb | 10 +- .../playback_formats_controller.rb | 10 +- .../{v1 => admin}/profiles_controller.rb | 10 +- app/controllers/admin/shows_controller.rb | 33 + app/controllers/admin/users_controller.rb | 16 + app/controllers/apidocs_controller.rb | 121 ++ app/controllers/audio_files_controller.rb | 191 ++ app/controllers/broadcasts_controller.rb | 141 ++ app/controllers/concerns/authenticatable.rb | 7 - app/controllers/concerns/swaggerable.rb | 16 - app/controllers/login_controller.rb | 70 + app/controllers/shows_controller.rb | 61 + app/controllers/v1/apidocs_controller.rb | 120 -- app/controllers/v1/audio_files_controller.rb | 193 -- app/controllers/v1/broadcasts_controller.rb | 143 -- app/controllers/v1/login_controller.rb | 53 - app/controllers/v1/shows_controller.rb | 52 - app/controllers/v1/users_controller.rb | 43 - .../archive_format_serializer.rb | 4 +- .../audio_encoding_serializer.rb | 2 +- .../downgrade_action_serializer.rb | 4 +- .../playback_format_serializer.rb | 4 +- .../{v1 => admin}/profile_serializer.rb | 4 +- .../{v1 => admin}/show_serializer.rb | 6 +- .../{v1 => admin}/user_serializer.rb | 8 +- app/serializers/audio_file_serializer.rb | 33 + app/serializers/broadcast_serializer.rb | 25 + app/serializers/show_serializer.rb | 17 + .../unprocessable_entity_serializer.rb | 12 + app/serializers/user_serializer.rb | 27 + app/serializers/v1/audio_file_serializer.rb | 35 - app/serializers/v1/broadcast_serializer.rb | 27 - .../v1/unprocessable_entity_serializer.rb | 14 - config/routes.rb | 40 +- doc/api.md | 4 +- doc/deployment.md | 2 +- doc/swagger.json | 1575 +++++++---------- .../archive_formats_controller_test.rb | 2 +- .../audio_encodings_controller_test.rb | 2 +- .../downgrade_actions_controller_test.rb | 2 +- .../playback_formats_controller_test.rb | 2 +- .../{v1 => admin}/profiles_controller_test.rb | 2 +- .../{v1 => admin}/shows_controller_test.rb | 37 +- .../{v1 => admin}/users_controller_test.rb | 21 +- test/controllers/apidocs_controller_test.rb | 11 + .../audio_files_controller_test.rb | 262 +++ .../controllers/broadcasts_controller_test.rb | 107 ++ test/controllers/login_controller_test.rb | 70 + test/controllers/shows_controller_test.rb | 41 + .../controllers/v1/apidocs_controller_test.rb | 13 - .../v1/audio_files_controller_test.rb | 264 --- .../v1/broadcasts_controller_test.rb | 109 -- test/controllers/v1/login_controller_test.rb | 48 - test/integration/api/authorization_test.rb | 94 +- test/integration/api/media_type_test.rb | 46 + 58 files changed, 2096 insertions(+), 2215 deletions(-) rename app/controllers/{v1 => admin}/archive_formats_controller.rb (74%) rename app/controllers/{v1 => admin}/audio_encodings_controller.rb (64%) create mode 100644 app/controllers/admin/authenticatable.rb rename app/controllers/{v1 => admin}/downgrade_actions_controller.rb (79%) rename app/controllers/{v1 => admin}/playback_formats_controller.rb (64%) rename app/controllers/{v1 => admin}/profiles_controller.rb (62%) create mode 100644 app/controllers/admin/shows_controller.rb create mode 100644 app/controllers/admin/users_controller.rb create mode 100644 app/controllers/apidocs_controller.rb create mode 100644 app/controllers/audio_files_controller.rb create mode 100644 app/controllers/broadcasts_controller.rb create mode 100644 app/controllers/login_controller.rb create mode 100644 app/controllers/shows_controller.rb delete mode 100644 app/controllers/v1/apidocs_controller.rb delete mode 100644 app/controllers/v1/audio_files_controller.rb delete mode 100644 app/controllers/v1/broadcasts_controller.rb delete mode 100644 app/controllers/v1/login_controller.rb delete mode 100644 app/controllers/v1/shows_controller.rb delete mode 100644 app/controllers/v1/users_controller.rb rename app/serializers/{v1 => admin}/archive_format_serializer.rb (92%) rename app/serializers/{v1 => admin}/audio_encoding_serializer.rb (98%) rename app/serializers/{v1 => admin}/downgrade_action_serializer.rb (93%) rename app/serializers/{v1 => admin}/playback_format_serializer.rb (94%) rename app/serializers/{v1 => admin}/profile_serializer.rb (91%) rename app/serializers/{v1 => admin}/show_serializer.rb (82%) rename app/serializers/{v1 => admin}/user_serializer.rb (74%) create mode 100644 app/serializers/audio_file_serializer.rb create mode 100644 app/serializers/broadcast_serializer.rb create mode 100644 app/serializers/show_serializer.rb create mode 100644 app/serializers/unprocessable_entity_serializer.rb create mode 100644 app/serializers/user_serializer.rb delete mode 100644 app/serializers/v1/audio_file_serializer.rb delete mode 100644 app/serializers/v1/broadcast_serializer.rb delete mode 100644 app/serializers/v1/unprocessable_entity_serializer.rb rename test/controllers/{v1 => admin}/archive_formats_controller_test.rb (99%) rename test/controllers/{v1 => admin}/audio_encodings_controller_test.rb (97%) rename test/controllers/{v1 => admin}/downgrade_actions_controller_test.rb (99%) rename test/controllers/{v1 => admin}/playback_formats_controller_test.rb (99%) rename test/controllers/{v1 => admin}/profiles_controller_test.rb (99%) rename test/controllers/{v1 => admin}/shows_controller_test.rb (74%) rename test/controllers/{v1 => admin}/users_controller_test.rb (80%) create mode 100644 test/controllers/apidocs_controller_test.rb create mode 100644 test/controllers/audio_files_controller_test.rb create mode 100644 test/controllers/broadcasts_controller_test.rb create mode 100644 test/controllers/login_controller_test.rb create mode 100644 test/controllers/shows_controller_test.rb delete mode 100644 test/controllers/v1/apidocs_controller_test.rb delete mode 100644 test/controllers/v1/audio_files_controller_test.rb delete mode 100644 test/controllers/v1/broadcasts_controller_test.rb delete mode 100644 test/controllers/v1/login_controller_test.rb create mode 100644 test/integration/api/media_type_test.rb diff --git a/app/controllers/v1/archive_formats_controller.rb b/app/controllers/admin/archive_formats_controller.rb similarity index 74% rename from app/controllers/v1/archive_formats_controller.rb rename to app/controllers/admin/archive_formats_controller.rb index c8b6ed9..248a5e9 100644 --- a/app/controllers/v1/archive_formats_controller.rb +++ b/app/controllers/admin/archive_formats_controller.rb @@ -1,12 +1,12 @@ -module V1 +module Admin class ArchiveFormatsController < CrudController - before_action :require_admin + include Admin::Authenticatable self.permitted_attrs = [:codec, :initial_bitrate, :initial_channels, :max_public_bitrate] - crud_swagger_paths(route_prefix: '/v1/profiles/{profile_id}', - data_class: 'V1::ArchiveFormat', + crud_swagger_paths(route_prefix: '/admin/profiles/{profile_id}', + data_class: 'Admin::ArchiveFormat', tags: [:admin], prefix_parameters: [ { name: :profile_id, @@ -25,7 +25,7 @@ def model_scope end def entry_url - v1_profile_archive_format_url(profile, entry) + admin_profile_archive_format_url(profile, entry) end def profile diff --git a/app/controllers/v1/audio_encodings_controller.rb b/app/controllers/admin/audio_encodings_controller.rb similarity index 64% rename from app/controllers/v1/audio_encodings_controller.rb rename to app/controllers/admin/audio_encodings_controller.rb index 0b5a245..f687fb9 100644 --- a/app/controllers/v1/audio_encodings_controller.rb +++ b/app/controllers/admin/audio_encodings_controller.rb @@ -1,14 +1,14 @@ -module V1 +module Admin class AudioEncodingsController < ApplicationController - before_action :require_admin + include Admin::Authenticatable - swagger_path '/v1/audio_encodings' do + swagger_path '/admin/audio_encodings' do operation :get do key :description, 'Returns a list of available audio encodings.' key :tags, [:audio_encoding, :admin] - response_entities('V1::AudioEncoding') + response_entities('Admin::AudioEncoding') security http_token: [] security api_token: [] @@ -17,7 +17,7 @@ class AudioEncodingsController < ApplicationController def index render json: AudioEncoding.list.sort_by(&:codec), - each_serializer: V1::AudioEncodingSerializer + each_serializer: Admin::AudioEncodingSerializer end end diff --git a/app/controllers/admin/authenticatable.rb b/app/controllers/admin/authenticatable.rb new file mode 100644 index 0000000..3e89df0 --- /dev/null +++ b/app/controllers/admin/authenticatable.rb @@ -0,0 +1,25 @@ +module Admin + module Authenticatable + + extend ActiveSupport::Concern + + included do + before_action :require_admin + end + + private + + def require_admin + require_authentication + if current_user && !current_user.admin? + render json: { errors: 'Forbidden' }, status: :forbidden + end + end + + # In admin section, a user MUST be authenticated by a REMOTE_USER header + def fetch_current_user + User.from_remote(*remote_user_params) + end + + end +end diff --git a/app/controllers/v1/downgrade_actions_controller.rb b/app/controllers/admin/downgrade_actions_controller.rb similarity index 79% rename from app/controllers/v1/downgrade_actions_controller.rb rename to app/controllers/admin/downgrade_actions_controller.rb index 321ca11..e2e3ee0 100644 --- a/app/controllers/v1/downgrade_actions_controller.rb +++ b/app/controllers/admin/downgrade_actions_controller.rb @@ -1,13 +1,13 @@ -module V1 +module Admin class DowngradeActionsController < CrudController - before_action :require_admin + include Admin::Authenticatable self.permitted_attrs = [:months, :bitrate, :channels] - crud_swagger_paths(route_prefix: '/v1/profiles/{profile_id}/archive_formats/' \ + crud_swagger_paths(route_prefix: '/admin/profiles/{profile_id}/archive_formats/' \ '{archive_format_id}', - data_class: 'V1::DowngradeAction', + data_class: 'Admin::DowngradeAction', tags: [:admin], prefix_parameters: [ { name: :profile_id, @@ -30,7 +30,7 @@ def model_scope end def entry_url - v1_profile_archive_format_downgrade_action_url(profile, archive_format, entry) + admin_profile_archive_format_downgrade_action_url(profile, archive_format, entry) end def archive_format diff --git a/app/controllers/v1/playback_formats_controller.rb b/app/controllers/admin/playback_formats_controller.rb similarity index 64% rename from app/controllers/v1/playback_formats_controller.rb rename to app/controllers/admin/playback_formats_controller.rb index ce4da6c..ceffa36 100644 --- a/app/controllers/v1/playback_formats_controller.rb +++ b/app/controllers/admin/playback_formats_controller.rb @@ -1,14 +1,14 @@ -module V1 +module Admin class PlaybackFormatsController < CrudController + include Admin::Authenticatable + self.permitted_attrs = [:name, :description, :codec, :bitrate, :channels] self.search_columns = %w(name description codec bitrate) - before_action :require_admin - - crud_swagger_paths(route_prefix: '/v1', - data_class: 'V1::PlaybackFormat', + crud_swagger_paths(route_prefix: '/admin', + data_class: 'Admin::PlaybackFormat', tags: [:admin], query_params: [:q]) diff --git a/app/controllers/v1/profiles_controller.rb b/app/controllers/admin/profiles_controller.rb similarity index 62% rename from app/controllers/v1/profiles_controller.rb rename to app/controllers/admin/profiles_controller.rb index 0810e0f..17fabac 100644 --- a/app/controllers/v1/profiles_controller.rb +++ b/app/controllers/admin/profiles_controller.rb @@ -1,14 +1,14 @@ -module V1 +module Admin class ProfilesController < CrudController + include Admin::Authenticatable + self.permitted_attrs = [:name, :description, :default] self.search_columns = %w(name description) - before_action :require_admin - - crud_swagger_paths(route_prefix: '/v1', - data_class: 'V1::Profile', + crud_swagger_paths(route_prefix: '/admin', + data_class: 'Admin::Profile', tags: [:admin], query_params: [:q]) diff --git a/app/controllers/admin/shows_controller.rb b/app/controllers/admin/shows_controller.rb new file mode 100644 index 0000000..59e2373 --- /dev/null +++ b/app/controllers/admin/shows_controller.rb @@ -0,0 +1,33 @@ +module Admin + class ShowsController < CrudController + + include Admin::Authenticatable + + self.search_columns = %w(name details) + + crud_swagger_paths(route_prefix: '/admin', + data_class: 'Admin::Show', + tags: [:admin]) + + private + + def fetch_entries + super.includes(:profile) + end + + # Only allow a trusted parameter "white list" through. + def model_params + attrs = nested_param(:data, :attributes) || ActionController::Parameters.new + profile_id = nested_param(:data, :relationships, :profile, :data, :id) + attrs[:profile_id] = profile_id if profile_id + attrs.permit(:name, :details, :profile_id) + end + + def nested_param(*keys) + value = params + keys.each { |key| value = value[key] if value } + value + end + + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 0000000..d589bce --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,16 @@ +module Admin + class UsersController < CrudController + + include Admin::Authenticatable + + self.permitted_attrs = [:username, :first_name, :last_name, :groups] + + self.search_columns = %w(username first_name last_name) + + crud_swagger_paths(route_prefix: '/admin', + data_class: 'Admin::User', + tags: [:admin], + query_params: [:q]) + + end +end diff --git a/app/controllers/apidocs_controller.rb b/app/controllers/apidocs_controller.rb new file mode 100644 index 0000000..c692d03 --- /dev/null +++ b/app/controllers/apidocs_controller.rb @@ -0,0 +1,121 @@ +class ApidocsController < ApplicationController + + # A list of all classes that have swagger_* declarations. + SWAGGERED_CLASSES = [ + # paths + self, + AudioFilesController, + BroadcastsController, + LoginController, + ShowsController, + Admin::ArchiveFormatsController, + Admin::AudioEncodingsController, + Admin::DowngradeActionsController, + Admin::PlaybackFormatsController, + Admin::ProfilesController, + Admin::ShowsController, + Admin::UsersController, + # entities + AudioFileSerializer, + BroadcastSerializer, + ShowSerializer, + UnprocessableEntitySerializer, + UserSerializer, + Admin::ArchiveFormatSerializer, + Admin::AudioEncodingSerializer, + Admin::DowngradeActionSerializer, + Admin::PlaybackFormatSerializer, + Admin::ProfileSerializer, + Admin::ShowSerializer, + Admin::UserSerializer + ].freeze + + swagger_root do + key :swagger, '2.0' + info do + key :version, '1.0' + key :title, 'RAAR Radio Archive API' + key :description, + 'RAAR Radio Archive API. ' \ + 'Some endpoints are public, other are restricted to admins.' + license name: 'AGPL' + end + key :consumes, ['application/vnd.api+json'] + key :produces, ['application/vnd.api+json'] + + security_definition :http_token do + key :type, :basic + key :description, + 'API token is passed as HTTP token authentication header: ' \ + '`Authorization: Token token="abc"`' + end + security_definition :api_token do + key :type, :apiKey + key :name, :api_token + key :in, :query + key :description, 'API token is passed as a query parameter' + end + + response :unprocessable_entity do + key :description, 'unprocessable entity' + schema do + property :errors, type: :array do + items '$ref' => 'UnprocessableEntity' + end + end + end + + parameter :page_number do + key :name, 'page[number]' + key :in, :query + key :description, 'The page number of the list.' + key :required, false + key :type, :integer + end + + parameter :page_size do + key :name, 'page[size]' + key :in, :query + key :description, + 'Maximum number of entries that are returned per page. Defaults to 50, maximum is 500.' + key :required, false + key :type, :integer + end + + parameter :sort do + key :name, 'sort' + key :in, :query + key :description, + 'Name of the sort field, optionally prefixed with a `-` for descending order.' + key :required, false + key :type, :string + end + + parameter :q do + key :name, :q + key :in, :query + key :description, 'Query string to search for.' + key :required, false + key :type, :string + end + end + + def index + render json: root_json + end + + private + + def root_json + Swagger::Blocks.build_root_json(SWAGGERED_CLASSES).merge(host_info) + end + + def host_info + secrets = Rails.application.secrets + {}.tap do |hash| + hash['host'] = secrets.host_name if secrets.host_name.present? + hash['basePath'] = secrets.base_path if secrets.base_path.present? + end + end + +end diff --git a/app/controllers/audio_files_controller.rb b/app/controllers/audio_files_controller.rb new file mode 100644 index 0000000..b3d9777 --- /dev/null +++ b/app/controllers/audio_files_controller.rb @@ -0,0 +1,191 @@ +class AudioFilesController < ListController + + NOT_FOUND_PATH = Rails.root.join('public', 'not_found.mp3') + THE_FUTURE_PATH = Rails.root.join('public', 'the_future.mp3') + + swagger_path '/broadcasts/{broadcast_id}/audio_files' do + operation :get do + key :description, 'Returns a list of available audio files for a given broadcast.' + key :tags, [:audio_file, :public] + + parameter name: :broadcast_id, + in: :path, + description: 'Id of the broadcast to list the audio files for.', + required: true, + type: :integer + + parameter :page_number + parameter :page_size + parameter :sort + + response_entities('AudioFile') + end + end + + swagger_path '/audio_files/{year}/{month}/{day}/{hour}{minute}{second}_' \ + '{playback_format}.{format}' do + operation :get do + key :description, 'Returns an audio file in the requested format.' + key :produces, AudioEncoding.list.collect(&:mime_type).sort + key :tags, [:audio_file, :public] + + parameter name: :year, + in: :path, + description: 'Four-digit year to get the audio file for.', + required: true, + type: :integer + + parameter name: :month, + in: :path, + description: 'Two-digit month to get the audio file for.', + required: true, + type: :integer + + parameter name: :day, + in: :path, + description: 'Two-digit day to get the audio file for.', + required: true, + type: :integer + + parameter name: :hour, + in: :path, + description: 'Two-digit hour to get the audio file for.', + required: true, + type: :integer + + parameter name: :minute, + in: :path, + description: 'Two-digit minute to get the audio file for.', + required: true, + type: :integer + + parameter name: :second, + in: :path, + description: 'Optional two-digit second to get the audio file for.', + required: true, # false, actually. Swagger path params must be required. + type: :integer + + parameter name: :playback_format, + in: :path, + description: 'Name of the playback format to get the audio file for. ' \ + "Use '#{AudioPath::BEST_FORMAT}' to get the best available quality.", + required: true, + type: :string + + parameter name: :format, + in: :path, + description: 'File extension of the audio encoding to get the audio file for.', + required: true, + type: :string + + parameter name: :download, + in: :query, + description: 'Logged-in users may pass this flag to get the file with ' \ + 'Content-Disposition attachment.', + required: false, + type: :boolean + + response 200 do + key :description, 'successfull operation' + schema type: :file + end + end + end + + def show + if file_playable? + send_audio(entry.absolute_path, entry.audio_format.mime_type) + else + handle_unplayable + end + end + + private + + def file_playable? + entry && ((entry.public? && !params[:download]) || current_user) + end + + def handle_unplayable + if timestamp < Time.zone.now + if entry + head :unauthorized + else + send_missing(NOT_FOUND_PATH) + end + else + send_missing(THE_FUTURE_PATH) + end + end + + def send_missing(path) + if File.exist?(path) + send_audio(path, AudioEncoding::Mp3.mime_type, :not_found) + else + head :not_found + end + end + + def send_audio(path, mime, status = :ok) + if request.headers['HTTP_RANGE'] && Rails.env.development? + send_range(path, mime) + else + send_file(path, send_file_options(path, mime, status)) + end + end + + def send_range(path, mime) + size = File.size(path) + bytes = Rack::Utils.byte_ranges(request.headers, size)[0] + + set_range_headers(bytes, size) + send_data(IO.binread(path, bytes.size, bytes.begin), send_file_options(path, mime, 206)) + end + + def set_range_headers(bytes, size) + response.header['Accept-Ranges'] = 'bytes' + response.header['Content-Range'] = "bytes #{bytes.begin}-#{bytes.end}/#{size}" + response.header['Content-Length'] = bytes.size.to_s + end + + def send_file_options(path, mime, status) + { type: mime, + status: status, + disposition: params[:download] ? :attachment : :inline, + filename: File.basename(path) } + end + + def fetch_entries + entries = super.where(broadcast_id: params[:broadcast_id]) + .includes(:playback_format, :broadcast) + if current_user + entries + else + entries.only_public + end + end + + def fetch_entry + if params[:playback_format] == AudioPath::BEST_FORMAT + AudioFile.best_at(timestamp, detect_codec) + else + playback_format = PlaybackFormat.find_by!(name: params[:playback_format], + codec: detect_codec) + AudioFile.playback_format_at(timestamp, playback_format) + end + end + + def detect_codec + encoding = AudioEncoding.for_extension(params[:format]) + raise ActionController::UnknownFormat unless encoding + encoding.codec + end + + def timestamp + @timestamp ||= + Time.zone.local(*params.values_at(:year, :month, :day, :hour, :min, :sec)) + rescue ArgumentError + not_found + end + +end diff --git a/app/controllers/broadcasts_controller.rb b/app/controllers/broadcasts_controller.rb new file mode 100644 index 0000000..1c2d682 --- /dev/null +++ b/app/controllers/broadcasts_controller.rb @@ -0,0 +1,141 @@ +class BroadcastsController < ListController + + TIME_PARTS = [:year, :month, :day, :hour, :min, :sec].freeze + + self.search_columns = %w(label people details shows.name shows.details) + + before_action :assert_params_given, only: :index + + # Convenience module to extract common swagger documentation in this controller. + module SwaggerOperationMethods + + def parameter_date(name) + parameter name: name, + in: :path, + description: "Optional two-digit #{name} to get the broadcasts for. " \ + 'Requires all preceeding parameters.', + required: true, # false, actually. Swagger path params must be required. + type: :integer + end + + # rubocop:disable Metrics/MethodLength + def response_broadcasts + response 200 do + key :description, 'successfull operation' + schema do + property :data, type: :array do + items '$ref' => 'Broadcast' + end + property :included, type: :array do + items '$ref' => 'Show' + end + end + end + end + # rubocop:enable Metrics/MethodLength + + end + include_missing(Swagger::Blocks::Nodes::OperationNode, SwaggerOperationMethods) + + swagger_path '/broadcasts' do + operation :get do + key :description, 'Searches and returns a list of broadcasts.' + key :tags, [:broadcast, :public] + + parameter :q + parameter :page_number + parameter :page_size + parameter :sort + + response_broadcasts + end + end + + swagger_path '/broadcasts/{year}/{month}/{day}/{hour}{minute}{second}' do + operation :get do + key :description, 'Returns a list of broadcasts at the given date/time span.' + key :tags, [:broadcast, :public] + + parameter name: :year, + in: :path, + description: 'The four-digit year to get the broadcasts for.', + required: true, + type: :integer + + parameter_date :month + parameter_date :day + parameter_date :hour + parameter_date :minute + parameter_date :second + + parameter :q + parameter :page_number + parameter :page_size + parameter :sort + + response_broadcasts + end + end + + swagger_path '/shows/{show_id}/broadcasts' do + operation :get do + key :description, 'Returns a list of broadcasts of the given show.' + key :tags, [:broadcast, :public] + + parameter name: :show_id, + in: :path, + description: 'ID of the show to list the broadcasts for', + required: true, + type: :integer + + parameter :q + parameter :page_number + parameter :page_size + parameter :sort + + response_broadcasts + end + end + + def index + render json: fetch_entries, each_serializer: model_serializer, include: [:show] + end + + private + + def fetch_entries + scope = super.joins(:show).includes(:show) + scope = scope.within(*start_finish) if params[:year] + scope = scope.where(show_id: params[:show_id]) if params[:show_id] + scope + end + + def start_finish + parts = params.values_at(*TIME_PARTS).compact + start = get_timestamp(parts) + finish = start + range(parts) + [start, finish] + end + + def range(parts) + range = TIME_PARTS[parts.size - 1] + case range + when :min then 1.minute + when :sec then 1.second + else 1.send(range) + end + end + + def get_timestamp(parts) + Time.zone.local(*parts) + rescue ArgumentError + not_found + end + + def assert_params_given + if params[:show_id].blank? && params[:year].blank? && params[:q].blank? + not_found + end + end + +end diff --git a/app/controllers/concerns/authenticatable.rb b/app/controllers/concerns/authenticatable.rb index 6fe62d5..4626987 100644 --- a/app/controllers/concerns/authenticatable.rb +++ b/app/controllers/concerns/authenticatable.rb @@ -9,13 +9,6 @@ def require_authentication end end - def require_admin - require_authentication - if current_user && !current_user.admin? - render json: { errors: 'Forbidden' }, status: :forbidden - end - end - def current_user if defined?(@current_user) @current_user diff --git a/app/controllers/concerns/swaggerable.rb b/app/controllers/concerns/swaggerable.rb index 28ca7b6..680cf3a 100644 --- a/app/controllers/concerns/swaggerable.rb +++ b/app/controllers/concerns/swaggerable.rb @@ -50,8 +50,6 @@ def crud_swagger_paths(options = {}) parameter :sort response_entities(data_class) - - security_infos(tags_read) end operation :post do @@ -63,8 +61,6 @@ def crud_swagger_paths(options = {}) response_entity(data_class, 201) response_unprocessable - - security_infos(tags_write) end end @@ -77,8 +73,6 @@ def crud_swagger_paths(options = {}) parameter_id(model_name, 'fetch') response_entity(data_class) - - security_infos(tags_read) end operation :patch do @@ -91,8 +85,6 @@ def crud_swagger_paths(options = {}) response_entity(data_class) response_unprocessable - - security_infos(tags_write) end operation :delete do @@ -107,8 +99,6 @@ def crud_swagger_paths(options = {}) end response_unprocessable - - security_infos(tags_write) end end end @@ -144,12 +134,6 @@ def parameter_attrs(model_name, action, data_class) end end - def security_infos(tags) - return unless tags.include?(:admin) - security http_token: [] - security api_token: [] - end - def response_entity(data_class, status = 200) response status do key :description, 'successfull operation' diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb new file mode 100644 index 0000000..3a95aa6 --- /dev/null +++ b/app/controllers/login_controller.rb @@ -0,0 +1,70 @@ +class LoginController < ApplicationController + + before_action :require_authentication, only: :regenerate_api_key + + swagger_path('/login') do + operation :get do + key :description, + 'Get the user object of the currently logged in user.' + key :tags, [:user] + + response_entity('User') + response 401 do + key :description, 'not authorized' + end + end + + operation :post do + key :description, + 'Login with username and password. ' \ + 'Returns the user object including the api_token for further requests.' + key :tags, [:user] + key :consumes, ['application/x-www-form-urlencoded'] + + parameter name: :username, + in: :formData, + description: 'The username of the user to login.', + required: true, + type: :string + + parameter name: :password, + in: :formData, + description: 'The password of the user to login.', + required: true, + type: :string + + response_entity('User') + response 401 do + key :description, 'not authorized' + end + end + end + + swagger_path('/login/api_key') do + operation :put do + key :description, 'Regenerates the api key of the current user.' + key :tags, [:user] + + response_entity('User') + response 401 do + key :description, 'not authorized' + end + end + end + + # GET/POST /login: Placeholder login action to act as FreeIPA endpoint. + def login + if current_user + render json: current_user, serializer: UserSerializer + else + render json: { errors: request.headers['EXTERNAL_AUTH_ERROR'] || 'Not authenticated' }, + status: :unauthorized + end + end + + def regenerate_api_key + current_user.regenerate_api_key! + render json: current_user, serializer: UserSerializer + end + +end diff --git a/app/controllers/shows_controller.rb b/app/controllers/shows_controller.rb new file mode 100644 index 0000000..6178c57 --- /dev/null +++ b/app/controllers/shows_controller.rb @@ -0,0 +1,61 @@ +class ShowsController < ListController + + self.search_columns = %w(name details) + + self.sort_mappings = { last_broadcast_at: 'MAX(broadcasts.started_at)' } + + swagger_path '/shows' do + operation :get do + key :description, 'Searches and returns a list of shows.' + key :tags, [:show, :public] + + parameter :q + parameter :page_number + parameter :page_size + parameter :sort + parameter name: :since, + description: 'Filter the shows by date of their last broadcast.', + format: :date, + in: :query, + required: false, + type: :string + + response 200 do + key :description, 'successfull operation' + schema do + property :data, type: :array do + items '$ref' => 'Show' + end + end + end + end + end + + swagger_path '/shows/{id}' do + operation :get do + key :description, 'Returns a single show.' + key :tags, [:show, :public] + + parameter_id('show', 'fetch') + + response_entity('Show') + end + end + + private + + def fetch_entries + if params[:since] || sort_with_order.first == 'last_broadcast_at' + with_last_broadcast(super) + else + super + end + end + + def with_last_broadcast(scope) + scope = scope.left_joins(:broadcasts).group('shows.id') + scope = scope.having('MAX(broadcasts.started_at) > ?', params[:since]) if params[:since] + scope + end + +end diff --git a/app/controllers/v1/apidocs_controller.rb b/app/controllers/v1/apidocs_controller.rb deleted file mode 100644 index 881232c..0000000 --- a/app/controllers/v1/apidocs_controller.rb +++ /dev/null @@ -1,120 +0,0 @@ -module V1 - class ApidocsController < ApplicationController - - # A list of all classes that have swagger_* declarations. - SWAGGERED_CLASSES = [ - # paths - self, - V1::ArchiveFormatsController, - V1::AudioEncodingsController, - V1::AudioFilesController, - V1::BroadcastsController, - V1::DowngradeActionsController, - V1::LoginController, - V1::PlaybackFormatsController, - V1::ProfilesController, - V1::ShowsController, - V1::UsersController, - # entities - V1::ArchiveFormatSerializer, - V1::AudioEncodingSerializer, - V1::AudioFileSerializer, - V1::BroadcastSerializer, - V1::DowngradeActionSerializer, - V1::PlaybackFormatSerializer, - V1::ProfileSerializer, - V1::ShowSerializer, - V1::UserSerializer, - V1::UnprocessableEntitySerializer - ].freeze - - swagger_root do - key :swagger, '2.0' - info do - key :version, '1.0' - key :title, 'RAAR Radio Archive API' - key :description, - 'RAAR Radio Archive API. ' \ - 'Some endpoints are public, other are restricted to admins.' - license name: 'AGPL' - end - key :consumes, ['application/vnd.api+json'] - key :produces, ['application/vnd.api+json'] - - security_definition :http_token do - key :type, :basic - key :description, - 'API token is passed as HTTP token authentication header: ' \ - '`Authorization: Token token="abc"`' - end - security_definition :api_token do - key :type, :apiKey - key :name, :api_token - key :in, :query - key :description, 'API token is passed as a query parameter' - end - - response :unprocessable_entity do - key :description, 'unprocessable entity' - schema do - property :errors, type: :array do - items '$ref' => 'V1::UnprocessableEntity' - end - end - end - - parameter :page_number do - key :name, 'page[number]' - key :in, :query - key :description, 'The page number of the list.' - key :required, false - key :type, :integer - end - - parameter :page_size do - key :name, 'page[size]' - key :in, :query - key :description, - 'Maximum number of entries that are returned per page. Defaults to 50, maximum is 500.' - key :required, false - key :type, :integer - end - - parameter :sort do - key :name, 'sort' - key :in, :query - key :description, - 'Name of the sort field, optionally prefixed with a `-` for descending order.' - key :required, false - key :type, :string - end - - parameter :q do - key :name, :q - key :in, :query - key :description, 'Query string to search for.' - key :required, false - key :type, :string - end - end - - def index - render json: root_json - end - - private - - def root_json - Swagger::Blocks.build_root_json(SWAGGERED_CLASSES).merge(host_info) - end - - def host_info - secrets = Rails.application.secrets - {}.tap do |hash| - hash['host'] = secrets.host_name if secrets.host_name.present? - hash['basePath'] = secrets.base_path if secrets.base_path.present? - end - end - - end -end diff --git a/app/controllers/v1/audio_files_controller.rb b/app/controllers/v1/audio_files_controller.rb deleted file mode 100644 index a1580e5..0000000 --- a/app/controllers/v1/audio_files_controller.rb +++ /dev/null @@ -1,193 +0,0 @@ -module V1 - class AudioFilesController < ListController - - NOT_FOUND_PATH = Rails.root.join('public', 'not_found.mp3') - THE_FUTURE_PATH = Rails.root.join('public', 'the_future.mp3') - - swagger_path '/v1/broadcasts/{broadcast_id}/audio_files' do - operation :get do - key :description, 'Returns a list of available audio files for a given broadcast.' - key :tags, [:audio_file, :public] - - parameter name: :broadcast_id, - in: :path, - description: 'Id of the broadcast to list the audio files for.', - required: true, - type: :integer - - parameter :page_number - parameter :page_size - parameter :sort - - response_entities('V1::AudioFile') - end - end - - swagger_path '/v1/audio_files/{year}/{month}/{day}/{hour}{minute}{second}_' \ - '{playback_format}.{format}' do - operation :get do - key :description, 'Returns an audio file in the requested format.' - key :produces, AudioEncoding.list.collect(&:mime_type).sort - key :tags, [:audio_file, :public] - - parameter name: :year, - in: :path, - description: 'Four-digit year to get the audio file for.', - required: true, - type: :integer - - parameter name: :month, - in: :path, - description: 'Two-digit month to get the audio file for.', - required: true, - type: :integer - - parameter name: :day, - in: :path, - description: 'Two-digit day to get the audio file for.', - required: true, - type: :integer - - parameter name: :hour, - in: :path, - description: 'Two-digit hour to get the audio file for.', - required: true, - type: :integer - - parameter name: :minute, - in: :path, - description: 'Two-digit minute to get the audio file for.', - required: true, - type: :integer - - parameter name: :second, - in: :path, - description: 'Optional two-digit second to get the audio file for.', - required: true, # false, actually. Swagger path params must be required. - type: :integer - - parameter name: :playback_format, - in: :path, - description: 'Name of the playback format to get the audio file for. ' \ - "Use '#{AudioPath::BEST_FORMAT}' to get the best available quality.", - required: true, - type: :string - - parameter name: :format, - in: :path, - description: 'File extension of the audio encoding to get the audio file for.', - required: true, - type: :string - - parameter name: :download, - in: :query, - description: 'Logged-in users may pass this flag to get the file with ' \ - 'Content-Disposition attachment.', - required: false, - type: :boolean - - response 200 do - key :description, 'successfull operation' - schema type: :file - end - end - end - - def show - if file_playable? - send_audio(entry.absolute_path, entry.audio_format.mime_type) - else - handle_unplayable - end - end - - private - - def file_playable? - entry && ((entry.public? && !params[:download]) || current_user) - end - - def handle_unplayable - if timestamp < Time.zone.now - if entry - head :unauthorized - else - send_missing(NOT_FOUND_PATH) - end - else - send_missing(THE_FUTURE_PATH) - end - end - - def send_missing(path) - if File.exist?(path) - send_audio(path, AudioEncoding::Mp3.mime_type, :not_found) - else - head :not_found - end - end - - def send_audio(path, mime, status = :ok) - if request.headers['HTTP_RANGE'] && Rails.env.development? - send_range(path, mime) - else - send_file(path, send_file_options(path, mime, status)) - end - end - - def send_range(path, mime) - size = File.size(path) - bytes = Rack::Utils.byte_ranges(request.headers, size)[0] - - set_range_headers(bytes, size) - send_data(IO.binread(path, bytes.size, bytes.begin), send_file_options(path, mime, 206)) - end - - def set_range_headers(bytes, size) - response.header['Accept-Ranges'] = 'bytes' - response.header['Content-Range'] = "bytes #{bytes.begin}-#{bytes.end}/#{size}" - response.header['Content-Length'] = bytes.size.to_s - end - - def send_file_options(path, mime, status) - { type: mime, - status: status, - disposition: params[:download] ? :attachment : :inline, - filename: File.basename(path) } - end - - def fetch_entries - entries = super.where(broadcast_id: params[:broadcast_id]) - .includes(:playback_format, :broadcast) - if current_user - entries - else - entries.only_public - end - end - - def fetch_entry - if params[:playback_format] == AudioPath::BEST_FORMAT - AudioFile.best_at(timestamp, detect_codec) - else - playback_format = PlaybackFormat.find_by!(name: params[:playback_format], - codec: detect_codec) - AudioFile.playback_format_at(timestamp, playback_format) - end - end - - def detect_codec - encoding = AudioEncoding.for_extension(params[:format]) - raise ActionController::UnknownFormat unless encoding - encoding.codec - end - - def timestamp - @timestamp ||= - Time.zone.local(*params.values_at(:year, :month, :day, :hour, :min, :sec)) - rescue ArgumentError - not_found - end - - end -end diff --git a/app/controllers/v1/broadcasts_controller.rb b/app/controllers/v1/broadcasts_controller.rb deleted file mode 100644 index 59e85d7..0000000 --- a/app/controllers/v1/broadcasts_controller.rb +++ /dev/null @@ -1,143 +0,0 @@ -module V1 - class BroadcastsController < ListController - - TIME_PARTS = [:year, :month, :day, :hour, :min, :sec].freeze - - self.search_columns = %w(label people details shows.name shows.details) - - before_action :assert_params_given, only: :index - - # Convenience module to extract common swagger documentation in this controller. - module SwaggerOperationMethods - - def parameter_date(name) - parameter name: name, - in: :path, - description: "Optional two-digit #{name} to get the broadcasts for. " \ - 'Requires all preceeding parameters.', - required: true, # false, actually. Swagger path params must be required. - type: :integer - end - - # rubocop:disable Metrics/MethodLength - def response_broadcasts - response 200 do - key :description, 'successfull operation' - schema do - property :data, type: :array do - items '$ref' => 'V1::Broadcast' - end - property :included, type: :array do - items '$ref' => 'V1::Show' - end - end - end - end - # rubocop:enable Metrics/MethodLength - - end - include_missing(Swagger::Blocks::Nodes::OperationNode, SwaggerOperationMethods) - - swagger_path '/v1/broadcasts' do - operation :get do - key :description, 'Searches and returns a list of broadcasts.' - key :tags, [:broadcast, :public] - - parameter :q - parameter :page_number - parameter :page_size - parameter :sort - - response_broadcasts - end - end - - swagger_path '/v1/broadcasts/{year}/{month}/{day}/{hour}{minute}{second}' do - operation :get do - key :description, 'Returns a list of broadcasts at the given date/time span.' - key :tags, [:broadcast, :public] - - parameter name: :year, - in: :path, - description: 'The four-digit year to get the broadcasts for.', - required: true, - type: :integer - - parameter_date :month - parameter_date :day - parameter_date :hour - parameter_date :minute - parameter_date :second - - parameter :q - parameter :page_number - parameter :page_size - parameter :sort - - response_broadcasts - end - end - - swagger_path '/v1/shows/{show_id}/broadcasts' do - operation :get do - key :description, 'Returns a list of broadcasts of the given show.' - key :tags, [:broadcast, :public] - - parameter name: :show_id, - in: :path, - description: 'ID of the show to list the broadcasts for', - required: true, - type: :integer - - parameter :q - parameter :page_number - parameter :page_size - parameter :sort - - response_broadcasts - end - end - - def index - render json: fetch_entries, each_serializer: model_serializer, include: [:show] - end - - private - - def fetch_entries - scope = super.joins(:show).includes(:show) - scope = scope.within(*start_finish) if params[:year] - scope = scope.where(show_id: params[:show_id]) if params[:show_id] - scope - end - - def start_finish - parts = params.values_at(*TIME_PARTS).compact - start = get_timestamp(parts) - finish = start + range(parts) - [start, finish] - end - - def range(parts) - range = TIME_PARTS[parts.size - 1] - case range - when :min then 1.minute - when :sec then 1.second - else 1.send(range) - end - end - - def get_timestamp(parts) - Time.zone.local(*parts) - rescue ArgumentError - not_found - end - - def assert_params_given - if params[:show_id].blank? && params[:year].blank? && params[:q].blank? - not_found - end - end - - end -end diff --git a/app/controllers/v1/login_controller.rb b/app/controllers/v1/login_controller.rb deleted file mode 100644 index 704ebcc..0000000 --- a/app/controllers/v1/login_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -module V1 - class LoginController < ApplicationController - - swagger_path('/v1/login') do - operation :get do - key :description, - 'Get the user object of the currently logged in user.' - key :tags, [:user] - - response_entity('V1::User') - response 401 do - key :description, 'not authorized' - end - end - - operation :post do - key :description, - 'Login with username and password. ' \ - 'Returns the user object including the api_token for further requests.' - key :tags, [:user] - key :consumes, ['application/x-www-form-urlencoded'] - - parameter name: :username, - in: :formData, - description: 'The username of the user to login.', - required: true, - type: :string - - parameter name: :password, - in: :formData, - description: 'The password of the user to login.', - required: true, - type: :string - - response_entity('V1::User') - response 401 do - key :description, 'not authorized' - end - end - end - - # GET/POST /login: Placeholder login action to act as FreeIPA endpoint. - def login - if current_user - render json: current_user, serializer: V1::UserSerializer - else - render json: { errors: request.headers['EXTERNAL_AUTH_ERROR'] || 'Not authenticated' }, - status: :unauthorized - end - end - - end -end diff --git a/app/controllers/v1/shows_controller.rb b/app/controllers/v1/shows_controller.rb deleted file mode 100644 index 5639440..0000000 --- a/app/controllers/v1/shows_controller.rb +++ /dev/null @@ -1,52 +0,0 @@ -module V1 - class ShowsController < CrudController - - self.search_columns = %w(name details) - - self.sort_mappings = { last_broadcast_at: 'MAX(broadcasts.started_at)' } - - before_action :require_admin, except: [:index, :show] - - crud_swagger_paths(route_prefix: '/v1', - data_class: 'V1::Show', - tags_read: [:public], - tags_write: [:admin], - query_params: [ - :q, - { name: :since, - description: 'Filter the shows by date of their last broadcast.', - format: :date } - ]) - - private - - def fetch_entries - if params[:since] || sort_with_order.first == 'last_broadcast_at' - with_last_broadcast(super) - else - super - end - end - - def with_last_broadcast(scope) - scope = scope.left_joins(:broadcasts).group('shows.id') - scope = scope.having('MAX(broadcasts.started_at) > ?', params[:since]) if params[:since] - scope - end - - # Only allow a trusted parameter "white list" through. - def model_params - attrs = nested_param(:data, :attributes) || ActionController::Parameters.new - profile_id = nested_param(:data, :relationships, :profile, :data, :id) - attrs[:profile_id] = profile_id if profile_id - attrs.permit(:name, :details, :profile_id) - end - - def nested_param(*keys) - value = params - keys.each { |key| value = value[key] if value } - value - end - - end -end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb deleted file mode 100644 index 6fc4517..0000000 --- a/app/controllers/v1/users_controller.rb +++ /dev/null @@ -1,43 +0,0 @@ -module V1 - class UsersController < CrudController - - before_action :require_admin, except: [:show, :regenerate_api_key] - before_action :require_self_or_admin, only: [:show, :regenerate_api_key] - - self.permitted_attrs = [:username, :first_name, :last_name, :groups] - - self.search_columns = %w(username first_name last_name) - - crud_swagger_paths(route_prefix: '/v1', - data_class: 'V1::User', - tags: [:admin], - query_params: [:q]) - - swagger_path('/v1/users/{id}/api_key') do - operation :put do - key :description, 'Regenerates the api key of the given user.' - key :tags, [:user, :admin] - - parameter_id('user', 'regenerate the api key for') - response_entity('V1::User') - - security_infos([:admin]) - end - end - - def regenerate_api_key - entry.regenerate_api_key! - render json: entry, serializer: model_serializer - end - - private - - def require_self_or_admin - require_authentication - if current_user && !current_user.admin? && current_user != entry - render json: { errors: 'Forbidden' }, status: :forbidden - end - end - - end -end diff --git a/app/serializers/v1/archive_format_serializer.rb b/app/serializers/admin/archive_format_serializer.rb similarity index 92% rename from app/serializers/v1/archive_format_serializer.rb rename to app/serializers/admin/archive_format_serializer.rb index 2d911a0..12b04ec 100644 --- a/app/serializers/v1/archive_format_serializer.rb +++ b/app/serializers/admin/archive_format_serializer.rb @@ -1,4 +1,4 @@ -module V1 +module Admin class ArchiveFormatSerializer < ApplicationSerializer json_api_swagger_schema do @@ -27,7 +27,7 @@ class ArchiveFormatSerializer < ApplicationSerializer attributes :id, :codec, :initial_bitrate, :initial_channels, :max_public_bitrate, :created_at, :updated_at - link(:self) { v1_profile_archive_format_url(object.profile_id, object) } + link(:self) { admin_profile_archive_format_url(object.profile_id, object) } end end diff --git a/app/serializers/v1/audio_encoding_serializer.rb b/app/serializers/admin/audio_encoding_serializer.rb similarity index 98% rename from app/serializers/v1/audio_encoding_serializer.rb rename to app/serializers/admin/audio_encoding_serializer.rb index 331ced5..a6a5981 100644 --- a/app/serializers/v1/audio_encoding_serializer.rb +++ b/app/serializers/admin/audio_encoding_serializer.rb @@ -1,4 +1,4 @@ -module V1 +module Admin class AudioEncodingSerializer < ApplicationSerializer json_api_swagger_schema do diff --git a/app/serializers/v1/downgrade_action_serializer.rb b/app/serializers/admin/downgrade_action_serializer.rb similarity index 93% rename from app/serializers/v1/downgrade_action_serializer.rb rename to app/serializers/admin/downgrade_action_serializer.rb index e957cf7..96e5ee4 100644 --- a/app/serializers/v1/downgrade_action_serializer.rb +++ b/app/serializers/admin/downgrade_action_serializer.rb @@ -1,4 +1,4 @@ -module V1 +module Admin class DowngradeActionSerializer < ApplicationSerializer json_api_swagger_schema do @@ -21,7 +21,7 @@ class DowngradeActionSerializer < ApplicationSerializer attributes :id, :months, :bitrate, :channels link(:self) do - v1_profile_archive_format_downgrade_action_url( + admin_profile_archive_format_downgrade_action_url( object.profile, object.archive_format_id, object diff --git a/app/serializers/v1/playback_format_serializer.rb b/app/serializers/admin/playback_format_serializer.rb similarity index 94% rename from app/serializers/v1/playback_format_serializer.rb rename to app/serializers/admin/playback_format_serializer.rb index 55c258b..c114064 100644 --- a/app/serializers/v1/playback_format_serializer.rb +++ b/app/serializers/admin/playback_format_serializer.rb @@ -1,4 +1,4 @@ -module V1 +module Admin class PlaybackFormatSerializer < ApplicationSerializer json_api_swagger_schema do @@ -26,7 +26,7 @@ class PlaybackFormatSerializer < ApplicationSerializer attributes :id, :name, :description, :codec, :bitrate, :channels, :created_at, :updated_at - link(:self) { v1_playback_format_url(object) } + link(:self) { admin_playback_format_url(object) } end end diff --git a/app/serializers/v1/profile_serializer.rb b/app/serializers/admin/profile_serializer.rb similarity index 91% rename from app/serializers/v1/profile_serializer.rb rename to app/serializers/admin/profile_serializer.rb index c094477..0ba3473 100644 --- a/app/serializers/v1/profile_serializer.rb +++ b/app/serializers/admin/profile_serializer.rb @@ -1,4 +1,4 @@ -module V1 +module Admin class ProfileSerializer < ApplicationSerializer json_api_swagger_schema do @@ -16,7 +16,7 @@ class ProfileSerializer < ApplicationSerializer attributes :id, :name, :description, :default, :created_at, :updated_at - link(:self) { v1_profile_url(object) } + link(:self) { admin_profile_url(object) } end end diff --git a/app/serializers/v1/show_serializer.rb b/app/serializers/admin/show_serializer.rb similarity index 82% rename from app/serializers/v1/show_serializer.rb rename to app/serializers/admin/show_serializer.rb index 77f048f..af5c8f1 100644 --- a/app/serializers/v1/show_serializer.rb +++ b/app/serializers/admin/show_serializer.rb @@ -1,4 +1,4 @@ -module V1 +module Admin class ShowSerializer < ApplicationSerializer json_api_swagger_schema do @@ -21,9 +21,9 @@ class ShowSerializer < ApplicationSerializer attributes :id, :name, :details - belongs_to :profile, serializer: V1::ProfileSerializer, if: :admin? + belongs_to :profile, serializer: ProfileSerializer - link(:self) { v1_show_url(object) } + link(:self) { admin_show_url(object) } end end diff --git a/app/serializers/v1/user_serializer.rb b/app/serializers/admin/user_serializer.rb similarity index 74% rename from app/serializers/v1/user_serializer.rb rename to app/serializers/admin/user_serializer.rb index 2e3e386..1f7242b 100644 --- a/app/serializers/v1/user_serializer.rb +++ b/app/serializers/admin/user_serializer.rb @@ -1,4 +1,4 @@ -module V1 +module Admin class UserSerializer < ApplicationSerializer json_api_swagger_schema do @@ -7,8 +7,6 @@ class UserSerializer < ApplicationSerializer property :first_name, type: :string property :last_name, type: :string property :groups, type: :array, items: { type: :string } - property :api_token, type: :string, readOnly: true - property :api_key_expires_at, type: :string, format: 'date-time', readOnly: true property :admin, type: :boolean, readOnly: true property :created_at, type: :string, format: 'date-time', readOnly: true property :updated_at, type: :string, format: 'date-time', readOnly: true @@ -19,11 +17,11 @@ class UserSerializer < ApplicationSerializer end attributes :id, :username, :first_name, :last_name, :groups, - :api_token, :api_key_expires_at, :created_at, :updated_at + :created_at, :updated_at attribute :admin?, key: :admin - link(:self) { v1_user_url(object) } + link(:self) { admin_user_url(object) } def groups object.group_list diff --git a/app/serializers/audio_file_serializer.rb b/app/serializers/audio_file_serializer.rb new file mode 100644 index 0000000..b294ba9 --- /dev/null +++ b/app/serializers/audio_file_serializer.rb @@ -0,0 +1,33 @@ +class AudioFileSerializer < ApplicationSerializer + + json_api_swagger_schema do + property :attributes do + property :codec, type: :string + property :bitrates, type: :integer + property :channels, type: :integer + property :url, type: :string + property :playback_format, type: :string + end + property :links do + property :self, type: :string, format: 'url', readOnly: true + end + end + + attributes :codec, :bitrate, :channels, :playback_format, :url + + # duplication required as we are in a different scope inside the link block. + link(:self) { audio_file_url(AudioPath.new(object).url_params) } + + def url + audio_file_url(audio_path.url_params) + end + + def playback_format + audio_path.playback_format + end + + def audio_path + @audio_path ||= AudioPath.new(object) + end + +end diff --git a/app/serializers/broadcast_serializer.rb b/app/serializers/broadcast_serializer.rb new file mode 100644 index 0000000..ec00be8 --- /dev/null +++ b/app/serializers/broadcast_serializer.rb @@ -0,0 +1,25 @@ +class BroadcastSerializer < ApplicationSerializer + + json_api_swagger_schema do + property :attributes do + property :label, type: :string + property :started_at, type: :string, format: 'date-time' + property :finished_at, type: :string, format: 'date-time' + property :people, type: :string + property :details, type: :string + end + property :relationships do + property :show do + property :data do + property :id, type: :integer + property :type, type: :string + end + end + end + end + + attributes :id, :label, :started_at, :finished_at, :people, :details + + belongs_to :show, serializer: ShowSerializer + +end diff --git a/app/serializers/show_serializer.rb b/app/serializers/show_serializer.rb new file mode 100644 index 0000000..261e00f --- /dev/null +++ b/app/serializers/show_serializer.rb @@ -0,0 +1,17 @@ +class ShowSerializer < ApplicationSerializer + + json_api_swagger_schema do + property :attributes do + property :name, type: :string + property :details, type: :string + end + property :links do + property :self, type: :string, format: 'url', readOnly: true + end + end + + attributes :id, :name, :details + + link(:self) { show_url(object) } + +end diff --git a/app/serializers/unprocessable_entity_serializer.rb b/app/serializers/unprocessable_entity_serializer.rb new file mode 100644 index 0000000..664e433 --- /dev/null +++ b/app/serializers/unprocessable_entity_serializer.rb @@ -0,0 +1,12 @@ +class UnprocessableEntitySerializer + + include Swagger::Blocks + + swagger_schema('UnprocessableEntity') do + property :source do + property :pointer, type: :string + end + property :details, type: :string + end + +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 0000000..f6a1724 --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,27 @@ +class UserSerializer < ApplicationSerializer + + json_api_swagger_schema do + property :attributes do + property :username, type: :string + property :first_name, type: :string + property :last_name, type: :string + property :groups, type: :array, items: { type: :string } + property :api_token, type: :string, readOnly: true + property :api_key_expires_at, type: :string, format: 'date-time', readOnly: true + property :admin, type: :boolean, readOnly: true + end + property :links do + property :self, type: :string, format: 'url', readOnly: true + end + end + + attributes :id, :username, :first_name, :last_name, :groups, + :api_token, :api_key_expires_at + + attribute :admin?, key: :admin + + def groups + object.group_list + end + +end diff --git a/app/serializers/v1/audio_file_serializer.rb b/app/serializers/v1/audio_file_serializer.rb deleted file mode 100644 index b2189cf..0000000 --- a/app/serializers/v1/audio_file_serializer.rb +++ /dev/null @@ -1,35 +0,0 @@ -module V1 - class AudioFileSerializer < ApplicationSerializer - - json_api_swagger_schema do - property :attributes do - property :codec, type: :string - property :bitrates, type: :integer - property :channels, type: :integer - property :url, type: :string - property :playback_format, type: :string - end - property :links do - property :self, type: :string, format: 'url', readOnly: true - end - end - - attributes :codec, :bitrate, :channels, :playback_format, :url - - # duplication required as we are in a different scope inside the link block. - link(:self) { v1_audio_file_url(AudioPath.new(object).url_params) } - - def url - v1_audio_file_url(audio_path.url_params) - end - - def playback_format - audio_path.playback_format - end - - def audio_path - @audio_path ||= AudioPath.new(object) - end - - end -end diff --git a/app/serializers/v1/broadcast_serializer.rb b/app/serializers/v1/broadcast_serializer.rb deleted file mode 100644 index af16286..0000000 --- a/app/serializers/v1/broadcast_serializer.rb +++ /dev/null @@ -1,27 +0,0 @@ -module V1 - class BroadcastSerializer < ApplicationSerializer - - json_api_swagger_schema do - property :attributes do - property :label, type: :string - property :started_at, type: :string, format: 'date-time' - property :finished_at, type: :string, format: 'date-time' - property :people, type: :string - property :details, type: :string - end - property :relationships do - property :show do - property :data do - property :id, type: :integer - property :type, type: :string - end - end - end - end - - attributes :id, :label, :started_at, :finished_at, :people, :details - - belongs_to :show, serializer: V1::ShowSerializer - - end -end diff --git a/app/serializers/v1/unprocessable_entity_serializer.rb b/app/serializers/v1/unprocessable_entity_serializer.rb deleted file mode 100644 index b4f2ec4..0000000 --- a/app/serializers/v1/unprocessable_entity_serializer.rb +++ /dev/null @@ -1,14 +0,0 @@ -module V1 - class UnprocessableEntitySerializer - - include Swagger::Blocks - - swagger_schema('V1::UnprocessableEntity') do - property :source do - property :pointer, type: :string - end - property :details, type: :string - end - - end -end diff --git a/config/routes.rb b/config/routes.rb index e3ed4d6..2a1dda7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,33 +1,30 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html - root to: 'v1/apidocs#index' + root to: 'apidocs#index' - namespace :v1 do - root to: 'apidocs#index' + resources :shows, only: [:index, :show] - resources :shows - - resources :broadcasts, only: [] do - resources :audio_files, only: :index - end + resources :broadcasts, only: [] do + resources :audio_files, only: :index + end - constraints(year: /\d{4}/, month: /\d{2}/, day: /\d{2}/, - hour: /\d{2}/, min: /\d{2}/, sec: /\d{2}/) do - get '(/shows/:show_id)/broadcasts(/:year(/:month(/:day(/:hour(:min(:sec))))))', - to: 'broadcasts#index', - as: :broadcasts + constraints(year: /\d{4}/, month: /\d{2}/, day: /\d{2}/, + hour: /\d{2}/, min: /\d{2}/, sec: /\d{2}/) do + get '(/shows/:show_id)/broadcasts(/:year(/:month(/:day(/:hour(:min(:sec))))))', + to: 'broadcasts#index', + as: :broadcasts - get 'audio_files/:year/:month/:day/:hour:min(:sec)_:playback_format.:format', - to: 'audio_files#show', - as: :audio_file - end + get 'audio_files/:year/:month/:day/:hour:min(:sec)_:playback_format.:format', + to: 'audio_files#show', + as: :audio_file + end - match 'login', via: [:get, :post], to: 'login#login' + match 'login', via: [:get, :post], to: 'login#login' + put 'login/api_key', to: 'login#regenerate_api_key' - resources :users do - put :api_key, to: 'users#regenerate_api_key', on: :member - end + namespace :admin do + resources :users resources :audio_encodings, only: :index @@ -39,6 +36,7 @@ resources :playback_formats + resources :shows end end diff --git a/doc/api.md b/doc/api.md index fe33792..de4117f 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,10 +1,10 @@ # RAAR API -Raar provides a simple REST API to access all shows, broadcasts and audio files. Additionally, a secured area allows management of profiles, archive and playback formats. All request and response payload follows the [JSON API](http://jsonapi.org) format. +Raar provides a simple REST API to access all shows, broadcasts and audio files. Additionally, a secured admin area allows management of profiles, archive and playback formats. All request and response payload follows the [JSON API](http://jsonapi.org) format. The API is documented with [Swagger](http://swagger.io). The current definition is available under the root path of the running server or as a generated artifact in [swagger.json](swagger.json). A nice visualization is available via [Swagger UI](http://petstore.swagger.io/?baseUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fradiorabe%2Fraar%2Fmaster%2Fdoc%2Fswagger.json). (This link may not work due to limitations of swagger-ui. Just copy the URL of the raw [swagger.json](https://raw.githubusercontent.com/radiorabe/raar/master/doc/swagger.json) file into the input field on swagger-ui and click 'Explore'.) ## Authentication -Authentication works over an API token passed as HTTP authorization token, as described in `swagger.json`. The token may be passed in a HTTP header (`Authorization: Token token="abc"`) or as a query parameter (`?api_token=abc`). If Free IPA is configured, a successfull `POST` request to `v1/login` with `username` and `password` will return the user object containing this token. +Authentication works over an API token passed as HTTP authorization token, as described in `swagger.json`. The token may be passed in a HTTP header (`Authorization: Token token="abc"`) or as a query parameter (`?api_token=abc`). If Free IPA is configured, a successfull `POST` request to `/login` with `username` and `password` will return the user object containing this token. diff --git a/doc/deployment.md b/doc/deployment.md index bd8387a..3dd3223 100644 --- a/doc/deployment.md +++ b/doc/deployment.md @@ -85,7 +85,7 @@ systemctl enable --now raar-downgrade.timer ## Free IPA -In order for the authentication to work with username and password, Free IPA may be configured to capture `POST` requests to `v1/login`. The form parameters `username` and `password` are provided. The application expects `REMOTE_USER`, `REMOTE_USER_GROUPS`, `REMOTE_USER_FIRST_NAME`, `REMOTE_USER_LAST_NAME` or `EXTERNAL_AUTH_ERROR` headers to be set. If the `REMOTE_USER` is set, a user object with the generated API token is returned. +In order for the authentication to work with username and password, Free IPA may be configured to capture `POST` requests to `/login`. The form parameters `username` and `password` are provided. The application expects `REMOTE_USER`, `REMOTE_USER_GROUPS`, `REMOTE_USER_FIRST_NAME`, `REMOTE_USER_LAST_NAME` or `EXTERNAL_AUTH_ERROR` headers to be set. If the `REMOTE_USER` is set, a user object with the generated API token is returned. If no Free IPA is configured, authentication is still possible by API token. The users must be created and the tokens must be distributed manually in this case. diff --git a/doc/swagger.json b/doc/swagger.json index 58d753c..1457c56 100644 --- a/doc/swagger.json +++ b/doc/swagger.json @@ -34,7 +34,7 @@ "errors": { "type": "array", "items": { - "$ref": "#/definitions/V1::UnprocessableEntity" + "$ref": "#/definitions/UnprocessableEntity" } } } @@ -72,20 +72,20 @@ } }, "paths": { - "/v1/profiles/{profile_id}/archive_formats": { + "/broadcasts/{broadcast_id}/audio_files": { "get": { - "description": "Returns a list of archive formats.", + "description": "Returns a list of available audio files for a given broadcast.", "tags": [ - "archive_format", - "admin" + "audio_file", + "public" ], "parameters": [ { - "name": "profile_id", - "description": "ID of the profile this archive format belongs to.", - "type": "integer", + "name": "broadcast_id", "in": "path", - "required": true + "description": "Id of the broadcast to list the audio files for.", + "required": true, + "type": "integer" }, { "$ref": "#/parameters/page_number" @@ -105,104 +105,206 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::ArchiveFormat" + "$ref": "#/definitions/AudioFile" } } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] - }, - "post": { - "description": "Creates a new archive format.", + } + } + }, + "/audio_files/{year}/{month}/{day}/{hour}{minute}{second}_{playback_format}.{format}": { + "get": { + "description": "Returns an audio file in the requested format.", + "produces": [ + "audio/flac", + "audio/mpeg" + ], "tags": [ - "archive_format", - "admin" + "audio_file", + "public" ], "parameters": [ { - "name": "profile_id", - "description": "ID of the profile this archive format belongs to.", - "type": "integer", + "name": "year", "in": "path", - "required": true + "description": "Four-digit year to get the audio file for.", + "required": true, + "type": "integer" }, { - "name": "body", - "in": "body", - "description": "Attributes defining the archive format to create.", + "name": "month", + "in": "path", + "description": "Two-digit month to get the audio file for.", + "required": true, + "type": "integer" + }, + { + "name": "day", + "in": "path", + "description": "Two-digit day to get the audio file for.", + "required": true, + "type": "integer" + }, + { + "name": "hour", + "in": "path", + "description": "Two-digit hour to get the audio file for.", + "required": true, + "type": "integer" + }, + { + "name": "minute", + "in": "path", + "description": "Two-digit minute to get the audio file for.", + "required": true, + "type": "integer" + }, + { + "name": "second", + "in": "path", + "description": "Optional two-digit second to get the audio file for.", + "required": true, + "type": "integer" + }, + { + "name": "playback_format", + "in": "path", + "description": "Name of the playback format to get the audio file for. Use 'best' to get the best available quality.", "required": true, + "type": "string" + }, + { + "name": "format", + "in": "path", + "description": "File extension of the audio encoding to get the audio file for.", + "required": true, + "type": "string" + }, + { + "name": "download", + "in": "query", + "description": "Logged-in users may pass this flag to get the file with Content-Disposition attachment.", + "required": false, + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "successfull operation", "schema": { - "properties": { - "data": { - "$ref": "#/definitions/V1::ArchiveFormat" - } - } + "type": "file" } } + } + } + }, + "/broadcasts": { + "get": { + "description": "Searches and returns a list of broadcasts.", + "tags": [ + "broadcast", + "public" + ], + "parameters": [ + { + "$ref": "#/parameters/q" + }, + { + "$ref": "#/parameters/page_number" + }, + { + "$ref": "#/parameters/page_size" + }, + { + "$ref": "#/parameters/sort" + } ], "responses": { - "201": { + "200": { "description": "successfull operation", "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::ArchiveFormat" + "type": "array", + "items": { + "$ref": "#/definitions/Broadcast" + } + }, + "included": { + "type": "array", + "items": { + "$ref": "#/definitions/Show" + } } } } - }, - "422": { - "$ref": "#/responses/unprocessable_entity" - } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] } - ] + } } }, - "/v1/profiles/{profile_id}/archive_formats/{id}": { + "/broadcasts/{year}/{month}/{day}/{hour}{minute}{second}": { "get": { - "description": "Returns a single archive format.", + "description": "Returns a list of broadcasts at the given date/time span.", "tags": [ - "archive_format", - "admin" + "broadcast", + "public" ], "parameters": [ { - "name": "profile_id", - "description": "ID of the profile this archive format belongs to.", - "type": "integer", + "name": "year", "in": "path", - "required": true + "description": "The four-digit year to get the broadcasts for.", + "required": true, + "type": "integer" }, { - "name": "id", + "name": "month", "in": "path", - "description": "ID of the archive format to fetch.", + "description": "Optional two-digit month to get the broadcasts for. Requires all preceeding parameters.", + "required": true, + "type": "integer" + }, + { + "name": "day", + "in": "path", + "description": "Optional two-digit day to get the broadcasts for. Requires all preceeding parameters.", "required": true, "type": "integer" + }, + { + "name": "hour", + "in": "path", + "description": "Optional two-digit hour to get the broadcasts for. Requires all preceeding parameters.", + "required": true, + "type": "integer" + }, + { + "name": "minute", + "in": "path", + "description": "Optional two-digit minute to get the broadcasts for. Requires all preceeding parameters.", + "required": true, + "type": "integer" + }, + { + "name": "second", + "in": "path", + "description": "Optional two-digit second to get the broadcasts for. Requires all preceeding parameters.", + "required": true, + "type": "integer" + }, + { + "$ref": "#/parameters/q" + }, + { + "$ref": "#/parameters/page_number" + }, + { + "$ref": "#/parameters/page_size" + }, + { + "$ref": "#/parameters/sort" } ], "responses": { @@ -211,59 +313,79 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::ArchiveFormat" + "type": "array", + "items": { + "$ref": "#/definitions/Broadcast" + } + }, + "included": { + "type": "array", + "items": { + "$ref": "#/definitions/Show" + } } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] - }, - "patch": { - "description": "Updates an existing archive format.", + } + } + }, + "/shows/{show_id}/broadcasts": { + "get": { + "description": "Returns a list of broadcasts of the given show.", "tags": [ - "archive_format", - "admin" + "broadcast", + "public" ], "parameters": [ { - "name": "profile_id", - "description": "ID of the profile this archive format belongs to.", - "type": "integer", - "in": "path", - "required": true - }, - { - "name": "id", + "name": "show_id", "in": "path", - "description": "ID of the archive format to update.", + "description": "ID of the show to list the broadcasts for", "required": true, "type": "integer" }, { - "name": "body", - "in": "body", - "description": "Attributes defining the archive format to update.", - "required": true, + "$ref": "#/parameters/q" + }, + { + "$ref": "#/parameters/page_number" + }, + { + "$ref": "#/parameters/page_size" + }, + { + "$ref": "#/parameters/sort" + } + ], + "responses": { + "200": { + "description": "successfull operation", "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::ArchiveFormat" + "type": "array", + "items": { + "$ref": "#/definitions/Broadcast" + } + }, + "included": { + "type": "array", + "items": { + "$ref": "#/definitions/Show" + } } } } } + } + } + }, + "/login": { + "get": { + "description": "Get the user object of the currently logged in user.", + "tags": [ + "user" ], "responses": { "200": { @@ -271,78 +393,62 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::ArchiveFormat" + "$ref": "#/definitions/User" } } } }, - "422": { - "$ref": "#/responses/unprocessable_entity" - } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] + "401": { + "description": "not authorized" } - ] + } }, - "delete": { - "description": "Deletes an existing archive format.", + "post": { + "description": "Login with username and password. Returns the user object including the api_token for further requests.", "tags": [ - "archive_format", - "admin" + "user" + ], + "consumes": [ + "application/x-www-form-urlencoded" ], "parameters": [ { - "name": "profile_id", - "description": "ID of the profile this archive format belongs to.", - "type": "integer", - "in": "path", - "required": true + "name": "username", + "in": "formData", + "description": "The username of the user to login.", + "required": true, + "type": "string" }, { - "name": "id", - "in": "path", - "description": "ID of the archive format to delete.", + "name": "password", + "in": "formData", + "description": "The password of the user to login.", "required": true, - "type": "integer" + "type": "string" } ], "responses": { - "204": { - "description": "successfull operation" - }, - "422": { - "$ref": "#/responses/unprocessable_entity" - } - }, - "security": [ - { - "http_token": [ - - ] + "200": { + "description": "successfull operation", + "schema": { + "properties": { + "data": { + "$ref": "#/definitions/User" + } + } + } }, - { - "api_token": [ - - ] + "401": { + "description": "not authorized" } - ] + } } }, - "/v1/audio_encodings": { - "get": { - "description": "Returns a list of available audio encodings.", + "/login/api_key": { + "put": { + "description": "Regenerates the api key of the current user.", "tags": [ - "audio_encoding", - "admin" + "user" ], "responses": { "200": { @@ -350,43 +456,27 @@ "schema": { "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/V1::AudioEncoding" - } + "$ref": "#/definitions/User" } } } - } - }, - "security": [ - { - "http_token": [ - - ] }, - { - "api_token": [ - - ] + "401": { + "description": "not authorized" } - ] + } } }, - "/v1/broadcasts/{broadcast_id}/audio_files": { + "/shows": { "get": { - "description": "Returns a list of available audio files for a given broadcast.", + "description": "Searches and returns a list of shows.", "tags": [ - "audio_file", + "show", "public" ], "parameters": [ { - "name": "broadcast_id", - "in": "path", - "description": "Id of the broadcast to list the audio files for.", - "required": true, - "type": "integer" + "$ref": "#/parameters/q" }, { "$ref": "#/parameters/page_number" @@ -396,6 +486,14 @@ }, { "$ref": "#/parameters/sort" + }, + { + "name": "since", + "description": "Filter the shows by date of their last broadcast.", + "format": "date", + "in": "query", + "required": false, + "type": "string" } ], "responses": { @@ -406,7 +504,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::AudioFile" + "$ref": "#/definitions/Show" } } } @@ -415,102 +513,50 @@ } } }, - "/v1/audio_files/{year}/{month}/{day}/{hour}{minute}{second}_{playback_format}.{format}": { + "/shows/{id}": { "get": { - "description": "Returns an audio file in the requested format.", - "produces": [ - "audio/flac", - "audio/mpeg" - ], + "description": "Returns a single show.", "tags": [ - "audio_file", + "show", "public" ], "parameters": [ { - "name": "year", - "in": "path", - "description": "Four-digit year to get the audio file for.", - "required": true, - "type": "integer" - }, - { - "name": "month", - "in": "path", - "description": "Two-digit month to get the audio file for.", - "required": true, - "type": "integer" - }, - { - "name": "day", - "in": "path", - "description": "Two-digit day to get the audio file for.", - "required": true, - "type": "integer" - }, - { - "name": "hour", - "in": "path", - "description": "Two-digit hour to get the audio file for.", - "required": true, - "type": "integer" - }, - { - "name": "minute", - "in": "path", - "description": "Two-digit minute to get the audio file for.", - "required": true, - "type": "integer" - }, - { - "name": "second", + "name": "id", "in": "path", - "description": "Optional two-digit second to get the audio file for.", + "description": "ID of the show to fetch.", "required": true, "type": "integer" - }, - { - "name": "playback_format", - "in": "path", - "description": "Name of the playback format to get the audio file for. Use 'best' to get the best available quality.", - "required": true, - "type": "string" - }, - { - "name": "format", - "in": "path", - "description": "File extension of the audio encoding to get the audio file for.", - "required": true, - "type": "string" - }, - { - "name": "download", - "in": "query", - "description": "Logged-in users may pass this flag to get the file with Content-Disposition attachment.", - "required": false, - "type": "boolean" } ], "responses": { "200": { "description": "successfull operation", "schema": { - "type": "file" + "properties": { + "data": { + "$ref": "#/definitions/Show" + } + } } } } } }, - "/v1/broadcasts": { + "/admin/profiles/{profile_id}/archive_formats": { "get": { - "description": "Searches and returns a list of broadcasts.", + "description": "Returns a list of archive formats.", "tags": [ - "broadcast", - "public" + "archive_format", + "admin" ], "parameters": [ { - "$ref": "#/parameters/q" + "name": "profile_id", + "description": "ID of the profile this archive format belongs to.", + "type": "integer", + "in": "path", + "required": true }, { "$ref": "#/parameters/page_number" @@ -530,134 +576,184 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::Broadcast" - } - }, - "included": { - "type": "array", - "items": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::ArchiveFormat" } } } } } } - } - }, - "/v1/broadcasts/{year}/{month}/{day}/{hour}{minute}{second}": { - "get": { - "description": "Returns a list of broadcasts at the given date/time span.", + }, + "post": { + "description": "Creates a new archive format.", "tags": [ - "broadcast", - "public" + "archive_format", + "admin" ], "parameters": [ { - "name": "year", + "name": "profile_id", + "description": "ID of the profile this archive format belongs to.", + "type": "integer", "in": "path", - "description": "The four-digit year to get the broadcasts for.", - "required": true, - "type": "integer" + "required": true }, { - "name": "month", - "in": "path", - "description": "Optional two-digit month to get the broadcasts for. Requires all preceeding parameters.", + "name": "body", + "in": "body", + "description": "Attributes defining the archive format to create.", "required": true, - "type": "integer" + "schema": { + "properties": { + "data": { + "$ref": "#/definitions/Admin::ArchiveFormat" + } + } + } + } + ], + "responses": { + "201": { + "description": "successfull operation", + "schema": { + "properties": { + "data": { + "$ref": "#/definitions/Admin::ArchiveFormat" + } + } + } }, + "422": { + "$ref": "#/responses/unprocessable_entity" + } + } + } + }, + "/admin/profiles/{profile_id}/archive_formats/{id}": { + "get": { + "description": "Returns a single archive format.", + "tags": [ + "archive_format", + "admin" + ], + "parameters": [ { - "name": "day", + "name": "profile_id", + "description": "ID of the profile this archive format belongs to.", + "type": "integer", "in": "path", - "description": "Optional two-digit day to get the broadcasts for. Requires all preceeding parameters.", - "required": true, - "type": "integer" + "required": true }, { - "name": "hour", + "name": "id", "in": "path", - "description": "Optional two-digit hour to get the broadcasts for. Requires all preceeding parameters.", + "description": "ID of the archive format to fetch.", "required": true, "type": "integer" - }, + } + ], + "responses": { + "200": { + "description": "successfull operation", + "schema": { + "properties": { + "data": { + "$ref": "#/definitions/Admin::ArchiveFormat" + } + } + } + } + } + }, + "patch": { + "description": "Updates an existing archive format.", + "tags": [ + "archive_format", + "admin" + ], + "parameters": [ { - "name": "minute", + "name": "profile_id", + "description": "ID of the profile this archive format belongs to.", + "type": "integer", "in": "path", - "description": "Optional two-digit minute to get the broadcasts for. Requires all preceeding parameters.", - "required": true, - "type": "integer" + "required": true }, { - "name": "second", + "name": "id", "in": "path", - "description": "Optional two-digit second to get the broadcasts for. Requires all preceeding parameters.", + "description": "ID of the archive format to update.", "required": true, "type": "integer" }, { - "$ref": "#/parameters/q" - }, - { - "$ref": "#/parameters/page_number" - }, - { - "$ref": "#/parameters/page_size" - }, - { - "$ref": "#/parameters/sort" + "name": "body", + "in": "body", + "description": "Attributes defining the archive format to update.", + "required": true, + "schema": { + "properties": { + "data": { + "$ref": "#/definitions/Admin::ArchiveFormat" + } + } + } } ], "responses": { "200": { "description": "successfull operation", "schema": { - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/V1::Broadcast" - } - }, - "included": { - "type": "array", - "items": { - "$ref": "#/definitions/V1::Show" - } + "properties": { + "data": { + "$ref": "#/definitions/Admin::ArchiveFormat" } } } + }, + "422": { + "$ref": "#/responses/unprocessable_entity" } } - } - }, - "/v1/shows/{show_id}/broadcasts": { - "get": { - "description": "Returns a list of broadcasts of the given show.", + }, + "delete": { + "description": "Deletes an existing archive format.", "tags": [ - "broadcast", - "public" + "archive_format", + "admin" ], "parameters": [ { - "name": "show_id", + "name": "profile_id", + "description": "ID of the profile this archive format belongs to.", + "type": "integer", "in": "path", - "description": "ID of the show to list the broadcasts for", - "required": true, - "type": "integer" - }, - { - "$ref": "#/parameters/q" - }, - { - "$ref": "#/parameters/page_number" + "required": true }, { - "$ref": "#/parameters/page_size" + "name": "id", + "in": "path", + "description": "ID of the archive format to delete.", + "required": true, + "type": "integer" + } + ], + "responses": { + "204": { + "description": "successfull operation" }, - { - "$ref": "#/parameters/sort" + "422": { + "$ref": "#/responses/unprocessable_entity" } + } + } + }, + "/admin/audio_encodings": { + "get": { + "description": "Returns a list of available audio encodings.", + "tags": [ + "audio_encoding", + "admin" ], "responses": { "200": { @@ -667,22 +763,28 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::Broadcast" - } - }, - "included": { - "type": "array", - "items": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::AudioEncoding" } } } } } - } + }, + "security": [ + { + "http_token": [ + + ] + }, + { + "api_token": [ + + ] + } + ] } }, - "/v1/profiles/{profile_id}/archive_formats/{archive_format_id}/downgrade_actions": { + "/admin/profiles/{profile_id}/archive_formats/{archive_format_id}/downgrade_actions": { "get": { "description": "Returns a list of downgrade actions.", "tags": [ @@ -722,25 +824,13 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::DowngradeAction" + "$ref": "#/definitions/Admin::DowngradeAction" } } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "post": { "description": "Creates a new downgrade action.", @@ -771,7 +861,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::DowngradeAction" + "$ref": "#/definitions/Admin::DowngradeAction" } } } @@ -783,7 +873,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::DowngradeAction" + "$ref": "#/definitions/Admin::DowngradeAction" } } } @@ -791,22 +881,10 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/profiles/{profile_id}/archive_formats/{archive_format_id}/downgrade_actions/{id}": { + "/admin/profiles/{profile_id}/archive_formats/{archive_format_id}/downgrade_actions/{id}": { "get": { "description": "Returns a single downgrade action.", "tags": [ @@ -842,24 +920,12 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::DowngradeAction" + "$ref": "#/definitions/Admin::DowngradeAction" } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "patch": { "description": "Updates an existing downgrade action.", @@ -897,7 +963,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::DowngradeAction" + "$ref": "#/definitions/Admin::DowngradeAction" } } } @@ -909,7 +975,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::DowngradeAction" + "$ref": "#/definitions/Admin::DowngradeAction" } } } @@ -917,19 +983,7 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "delete": { "description": "Deletes an existing downgrade action.", @@ -967,85 +1021,10 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] - } - }, - "/v1/login": { - "get": { - "description": "Get the user object of the currently logged in user.", - "tags": [ - "user" - ], - "responses": { - "200": { - "description": "successfull operation", - "schema": { - "properties": { - "data": { - "$ref": "#/definitions/V1::User" - } - } - } - }, - "401": { - "description": "not authorized" - } - } - }, - "post": { - "description": "Login with username and password. Returns the user object including the api_token for further requests.", - "tags": [ - "user" - ], - "consumes": [ - "application/x-www-form-urlencoded" - ], - "parameters": [ - { - "name": "username", - "in": "formData", - "description": "The username of the user to login.", - "required": true, - "type": "string" - }, - { - "name": "password", - "in": "formData", - "description": "The password of the user to login.", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "successfull operation", - "schema": { - "properties": { - "data": { - "$ref": "#/definitions/V1::User" - } - } - } - }, - "401": { - "description": "not authorized" - } } } }, - "/v1/playback_formats": { + "/admin/playback_formats": { "get": { "description": "Returns a list of playback formats.", "tags": [ @@ -1074,25 +1053,13 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::PlaybackFormat" + "$ref": "#/definitions/Admin::PlaybackFormat" } } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "post": { "description": "Creates a new playback format.", @@ -1109,7 +1076,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::PlaybackFormat" + "$ref": "#/definitions/Admin::PlaybackFormat" } } } @@ -1121,7 +1088,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::PlaybackFormat" + "$ref": "#/definitions/Admin::PlaybackFormat" } } } @@ -1129,22 +1096,10 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/playback_formats/{id}": { + "/admin/playback_formats/{id}": { "get": { "description": "Returns a single playback format.", "tags": [ @@ -1166,24 +1121,12 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::PlaybackFormat" + "$ref": "#/definitions/Admin::PlaybackFormat" } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "patch": { "description": "Updates an existing playback format.", @@ -1207,7 +1150,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::PlaybackFormat" + "$ref": "#/definitions/Admin::PlaybackFormat" } } } @@ -1218,28 +1161,16 @@ "description": "successfull operation", "schema": { "properties": { - "data": { - "$ref": "#/definitions/V1::PlaybackFormat" - } - } - } - }, - "422": { - "$ref": "#/responses/unprocessable_entity" - } - }, - "security": [ - { - "http_token": [ - - ] + "data": { + "$ref": "#/definitions/Admin::PlaybackFormat" + } + } + } }, - { - "api_token": [ - - ] + "422": { + "$ref": "#/responses/unprocessable_entity" } - ] + } }, "delete": { "description": "Deletes an existing playback format.", @@ -1263,22 +1194,10 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/profiles": { + "/admin/profiles": { "get": { "description": "Returns a list of profiles.", "tags": [ @@ -1307,25 +1226,13 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::Profile" + "$ref": "#/definitions/Admin::Profile" } } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "post": { "description": "Creates a new profile.", @@ -1342,7 +1249,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Profile" + "$ref": "#/definitions/Admin::Profile" } } } @@ -1354,7 +1261,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Profile" + "$ref": "#/definitions/Admin::Profile" } } } @@ -1362,22 +1269,10 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/profiles/{id}": { + "/admin/profiles/{id}": { "get": { "description": "Returns a single profile.", "tags": [ @@ -1399,24 +1294,12 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Profile" + "$ref": "#/definitions/Admin::Profile" } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "patch": { "description": "Updates an existing profile.", @@ -1440,7 +1323,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Profile" + "$ref": "#/definitions/Admin::Profile" } } } @@ -1452,7 +1335,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Profile" + "$ref": "#/definitions/Admin::Profile" } } } @@ -1460,19 +1343,7 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "delete": { "description": "Deletes an existing profile.", @@ -1496,40 +1367,17 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/shows": { + "/admin/shows": { "get": { "description": "Returns a list of shows.", "tags": [ "show", - "public" + "admin" ], "parameters": [ - { - "$ref": "#/parameters/q" - }, - { - "in": "query", - "required": false, - "type": "string", - "name": "since", - "description": "Filter the shows by date of their last broadcast.", - "format": "date" - }, { "$ref": "#/parameters/page_number" }, @@ -1548,7 +1396,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::Show" } } } @@ -1571,7 +1419,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::Show" } } } @@ -1583,7 +1431,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::Show" } } } @@ -1591,27 +1439,15 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/shows/{id}": { + "/admin/shows/{id}": { "get": { "description": "Returns a single show.", "tags": [ "show", - "public" + "admin" ], "parameters": [ { @@ -1628,7 +1464,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::Show" } } } @@ -1657,7 +1493,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::Show" } } } @@ -1669,7 +1505,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::Show" + "$ref": "#/definitions/Admin::Show" } } } @@ -1677,19 +1513,7 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "delete": { "description": "Deletes an existing show.", @@ -1713,22 +1537,10 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/users": { + "/admin/users": { "get": { "description": "Returns a list of users.", "tags": [ @@ -1757,25 +1569,13 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/V1::User" + "$ref": "#/definitions/Admin::User" } } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "post": { "description": "Creates a new user.", @@ -1792,7 +1592,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::User" + "$ref": "#/definitions/Admin::User" } } } @@ -1804,7 +1604,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::User" + "$ref": "#/definitions/Admin::User" } } } @@ -1812,22 +1612,10 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } } }, - "/v1/users/{id}": { + "/admin/users/{id}": { "get": { "description": "Returns a single user.", "tags": [ @@ -1849,24 +1637,12 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::User" + "$ref": "#/definitions/Admin::User" } } } } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "patch": { "description": "Updates an existing user.", @@ -1890,7 +1666,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::User" + "$ref": "#/definitions/Admin::User" } } } @@ -1902,7 +1678,7 @@ "schema": { "properties": { "data": { - "$ref": "#/definitions/V1::User" + "$ref": "#/definitions/Admin::User" } } } @@ -1910,19 +1686,7 @@ "422": { "$ref": "#/responses/unprocessable_entity" } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] + } }, "delete": { "description": "Deletes an existing user.", @@ -1941,71 +1705,17 @@ ], "responses": { "204": { - "description": "successfull operation" - }, - "422": { - "$ref": "#/responses/unprocessable_entity" - } - }, - "security": [ - { - "http_token": [ - - ] - }, - { - "api_token": [ - - ] - } - ] - } - }, - "/v1/users/{id}/api_key": { - "put": { - "description": "Regenerates the api key of the given user.", - "tags": [ - "user", - "admin" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "ID of the user to regenerate the api key for.", - "required": true, - "type": "integer" - } - ], - "responses": { - "200": { - "description": "successfull operation", - "schema": { - "properties": { - "data": { - "$ref": "#/definitions/V1::User" - } - } - } - } - }, - "security": [ - { - "http_token": [ - - ] + "description": "successfull operation" }, - { - "api_token": [ - - ] + "422": { + "$ref": "#/responses/unprocessable_entity" } - ] + } } } }, "definitions": { - "V1::ArchiveFormat": { + "AudioFile": { "properties": { "id": { "type": "integer" @@ -2016,30 +1726,97 @@ "attributes": { "properties": { "codec": { - "type": "string", - "description": "See audio_encodings for possible values. This attribute may only be set on create." + "type": "string" }, - "initial_bitrate": { - "type": "integer", - "description": "See audio_encodings of selected codec for possible values." + "bitrates": { + "type": "integer" }, - "initial_channels": { - "type": "integer", - "description": "See audio_encodings of selected codec for possible values." + "channels": { + "type": "integer" }, - "max_public_bitrate": { - "type": "integer", - "description": "See audio_encodings of selected codec for possible values." + "url": { + "type": "string" }, - "created_at": { + "playback_format": { + "type": "string" + } + } + }, + "links": { + "properties": { + "self": { "type": "string", - "format": "date-time", + "format": "url", "readOnly": true + } + } + } + } + }, + "Broadcast": { + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "attributes": { + "properties": { + "label": { + "type": "string" }, - "updated_at": { + "started_at": { "type": "string", - "format": "date-time", - "readOnly": true + "format": "date-time" + }, + "finished_at": { + "type": "string", + "format": "date-time" + }, + "people": { + "type": "string" + }, + "details": { + "type": "string" + } + } + }, + "relationships": { + "properties": { + "show": { + "properties": { + "data": { + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "Show": { + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "attributes": { + "properties": { + "name": { + "type": "string" + }, + "details": { + "type": "string" } } }, @@ -2054,7 +1831,21 @@ } } }, - "V1::AudioEncoding": { + "UnprocessableEntity": { + "properties": { + "source": { + "properties": { + "pointer": { + "type": "string" + } + } + }, + "details": { + "type": "string" + } + } + }, + "User": { "properties": { "id": { "type": "integer" @@ -2064,34 +1855,48 @@ }, "attributes": { "properties": { - "codec": { + "username": { "type": "string" }, - "file_extension": { + "first_name": { "type": "string" }, - "mime_type": { + "last_name": { "type": "string" }, - "bitrates": { + "groups": { "type": "array", "items": { - "type": "integer" - }, - "description": "Possible bitrates in kbps." + "type": "string" + } }, - "channels": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "Possible number of channels." + "api_token": { + "type": "string", + "readOnly": true + }, + "api_key_expires_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "admin": { + "type": "boolean", + "readOnly": true + } + } + }, + "links": { + "properties": { + "self": { + "type": "string", + "format": "url", + "readOnly": true } } } } }, - "V1::AudioFile": { + "Admin::ArchiveFormat": { "properties": { "id": { "type": "integer" @@ -2102,19 +1907,30 @@ "attributes": { "properties": { "codec": { - "type": "string" + "type": "string", + "description": "See audio_encodings for possible values. This attribute may only be set on create." }, - "bitrates": { - "type": "integer" + "initial_bitrate": { + "type": "integer", + "description": "See audio_encodings of selected codec for possible values." }, - "channels": { - "type": "integer" + "initial_channels": { + "type": "integer", + "description": "See audio_encodings of selected codec for possible values." }, - "url": { - "type": "string" + "max_public_bitrate": { + "type": "integer", + "description": "See audio_encodings of selected codec for possible values." }, - "playback_format": { - "type": "string" + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true } } }, @@ -2129,7 +1945,7 @@ } } }, - "V1::Broadcast": { + "Admin::AudioEncoding": { "properties": { "id": { "type": "integer" @@ -2139,46 +1955,34 @@ }, "attributes": { "properties": { - "label": { + "codec": { "type": "string" }, - "started_at": { - "type": "string", - "format": "date-time" - }, - "finished_at": { - "type": "string", - "format": "date-time" - }, - "people": { + "file_extension": { "type": "string" }, - "details": { + "mime_type": { "type": "string" - } - } - }, - "relationships": { - "properties": { - "show": { - "properties": { - "data": { - "properties": { - "id": { - "type": "integer" - }, - "type": { - "type": "string" - } - } - } - } + }, + "bitrates": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Possible bitrates in kbps." + }, + "channels": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Possible number of channels." } } } } }, - "V1::DowngradeAction": { + "Admin::DowngradeAction": { "properties": { "id": { "type": "integer" @@ -2212,7 +2016,7 @@ } } }, - "V1::PlaybackFormat": { + "Admin::PlaybackFormat": { "properties": { "id": { "type": "integer" @@ -2264,7 +2068,7 @@ } } }, - "V1::Profile": { + "Admin::Profile": { "properties": { "id": { "type": "integer" @@ -2306,7 +2110,7 @@ } } }, - "V1::Show": { + "Admin::Show": { "properties": { "id": { "type": "integer" @@ -2353,7 +2157,7 @@ } } }, - "V1::User": { + "Admin::User": { "properties": { "id": { "type": "integer" @@ -2378,15 +2182,6 @@ "type": "string" } }, - "api_token": { - "type": "string", - "readOnly": true - }, - "api_key_expires_at": { - "type": "string", - "format": "date-time", - "readOnly": true - }, "admin": { "type": "boolean", "readOnly": true @@ -2413,20 +2208,6 @@ } } } - }, - "V1::UnprocessableEntity": { - "properties": { - "source": { - "properties": { - "pointer": { - "type": "string" - } - } - }, - "details": { - "type": "string" - } - } } }, "host": "localhost:3000" diff --git a/test/controllers/v1/archive_formats_controller_test.rb b/test/controllers/admin/archive_formats_controller_test.rb similarity index 99% rename from test/controllers/v1/archive_formats_controller_test.rb rename to test/controllers/admin/archive_formats_controller_test.rb index 7ebd393..557c4c0 100644 --- a/test/controllers/v1/archive_formats_controller_test.rb +++ b/test/controllers/admin/archive_formats_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -module V1 +module Admin class ArchiveFormatsControllerTest < ActionController::TestCase setup :login_as_admin diff --git a/test/controllers/v1/audio_encodings_controller_test.rb b/test/controllers/admin/audio_encodings_controller_test.rb similarity index 97% rename from test/controllers/v1/audio_encodings_controller_test.rb rename to test/controllers/admin/audio_encodings_controller_test.rb index 38f93e7..0397992 100644 --- a/test/controllers/v1/audio_encodings_controller_test.rb +++ b/test/controllers/admin/audio_encodings_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -module V1 +module Admin class AudioEncodingsControllerTest < ActionController::TestCase setup :login_as_admin diff --git a/test/controllers/v1/downgrade_actions_controller_test.rb b/test/controllers/admin/downgrade_actions_controller_test.rb similarity index 99% rename from test/controllers/v1/downgrade_actions_controller_test.rb rename to test/controllers/admin/downgrade_actions_controller_test.rb index fec2d20..c0b38cf 100644 --- a/test/controllers/v1/downgrade_actions_controller_test.rb +++ b/test/controllers/admin/downgrade_actions_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -module V1 +module Admin class DowngradeActionsControllerTest < ActionController::TestCase setup :login_as_admin diff --git a/test/controllers/v1/playback_formats_controller_test.rb b/test/controllers/admin/playback_formats_controller_test.rb similarity index 99% rename from test/controllers/v1/playback_formats_controller_test.rb rename to test/controllers/admin/playback_formats_controller_test.rb index 8982bda..0c08e94 100644 --- a/test/controllers/v1/playback_formats_controller_test.rb +++ b/test/controllers/admin/playback_formats_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -module V1 +module Admin class PlaybackFormatsControllerTest < ActionController::TestCase setup :login_as_admin diff --git a/test/controllers/v1/profiles_controller_test.rb b/test/controllers/admin/profiles_controller_test.rb similarity index 99% rename from test/controllers/v1/profiles_controller_test.rb rename to test/controllers/admin/profiles_controller_test.rb index d7be48b..8dd3686 100644 --- a/test/controllers/v1/profiles_controller_test.rb +++ b/test/controllers/admin/profiles_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -module V1 +module Admin class ProfilesControllerTest < ActionController::TestCase setup :login_as_admin diff --git a/test/controllers/v1/shows_controller_test.rb b/test/controllers/admin/shows_controller_test.rb similarity index 74% rename from test/controllers/v1/shows_controller_test.rb rename to test/controllers/admin/shows_controller_test.rb index 1e31244..b51f09d 100644 --- a/test/controllers/v1/shows_controller_test.rb +++ b/test/controllers/admin/shows_controller_test.rb @@ -1,59 +1,26 @@ require 'test_helper' -module V1 +module Admin class ShowsControllerTest < ActionController::TestCase setup :login_as_admin test 'GET index returns list of all shows' do - login(nil) get :index assert_equal ['Geschäch9schlimmers', 'Info', 'Klangbecken'], json_attrs(:name) end test 'GET index with query params returns list of matching shows' do - login(nil) get :index, params: { q: 'e' } assert_equal ['Geschäch9schlimmers', 'Klangbecken'], json_attrs(:name) end - test 'GET index with since param returns list of matching shows' do - login(nil) - get :index, params: { since: '2013-06-01' } - assert_equal ['Geschäch9schlimmers'], json_attrs(:name) - end - - test 'GET index with long-ago since param returns all shows' do - login(nil) - get :index, params: { since: '2013-05-20', page: { number: 1, size: 2 } } - assert_equal ['Geschäch9schlimmers', 'Info'], json_attrs(:name) - end - - test 'GET index with sort by last_broadcast_at returns ordered list' do - login(nil) - get :index, params: { sort: '-last_broadcast_at' } - assert_equal ['Geschäch9schlimmers', 'Klangbecken', 'Info'], json_attrs(:name) - end - - test 'GET index with since param and sort by last_broadcast_at returns ordered list' do - login(nil) - get :index, params: { since: '2013-06-01', sort: '-last_broadcast_at' } - assert_equal ['Geschäch9schlimmers'], json_attrs(:name) - end - - test 'GET show returns with profile as admin' do + test 'GET show returns with profile' do get :show, params: { id: shows(:info).id } assert_equal 'Info', json['data']['attributes']['name'] assert_equal profiles(:important).id.to_s, json['data']['relationships']['profile']['data']['id'] end - test 'GET show returns no profile as regular user' do - login(nil) - get :show, params: { id: shows(:info).id } - assert_equal 'Info', json['data']['attributes']['name'] - assert_nil json['data']['relationships'] - end - test 'GET create returns unauthorized if not logged in' do login(nil) post :create diff --git a/test/controllers/v1/users_controller_test.rb b/test/controllers/admin/users_controller_test.rb similarity index 80% rename from test/controllers/v1/users_controller_test.rb rename to test/controllers/admin/users_controller_test.rb index 8e1e6a5..c43a56f 100644 --- a/test/controllers/v1/users_controller_test.rb +++ b/test/controllers/admin/users_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -module V1 +module Admin class UsersControllerTest < ActionController::TestCase setup :login_as_admin @@ -15,14 +15,8 @@ class UsersControllerTest < ActionController::TestCase assert_equal 'admin', json['data']['attributes']['username'] end - test 'GET show returns himself as regular user' do - login - get :show, params: { id: users(:speedee).id } - assert_equal 'speedee', json['data']['attributes']['username'] - end - - test 'GET show returns forbidden for other user' do - login + test 'GET show returns forbidden for regular user' do + login('speedee') get :show, params: { id: users(:admin).id } assert_response 403 end @@ -81,15 +75,6 @@ class UsersControllerTest < ActionController::TestCase assert_equal 'speedee', users(:speedee).reload.username end - test 'PUT api_key regenerates api key' do - user = users(:speedee) - key = user.api_key - put :regenerate_api_key, params: { id: user.id } - assert_response 200 - assert_not_equal key, user.reload.api_key - assert_equal user.api_token, json['data']['attributes']['api_token'] - end - test 'DELETE destroy removes existing user' do assert_difference('User.count', -1) do delete :destroy, params: { id: users(:speedee).id } diff --git a/test/controllers/apidocs_controller_test.rb b/test/controllers/apidocs_controller_test.rb new file mode 100644 index 0000000..3d56a16 --- /dev/null +++ b/test/controllers/apidocs_controller_test.rb @@ -0,0 +1,11 @@ +require 'test_helper' + +class ApidocsControllerTest < ActionController::TestCase + + test 'GET index returns json' do + get :index + assert_equal 22, json['paths'].size + assert_equal 12, json['definitions'].size + end + +end diff --git a/test/controllers/audio_files_controller_test.rb b/test/controllers/audio_files_controller_test.rb new file mode 100644 index 0000000..b71eb6d --- /dev/null +++ b/test/controllers/audio_files_controller_test.rb @@ -0,0 +1,262 @@ +require 'test_helper' + +class AudioFilesControllerTest < ActionController::TestCase + + setup :touch_path + teardown :remove_path + + test 'GET index returns complete list for broadcast for logged in user' do + login + get :index, params: { broadcast_id: broadcasts(:info_april).id } + + assert_equal [320, 192, 96], json_attrs(:bitrate) + assert_equal %w(best high low), json_attrs('playback_format') + assert_equal ['http://example.com/audio_files/2013/04/10/110000_best.mp3', + 'http://example.com/audio_files/2013/04/10/110000_high.mp3', + 'http://example.com/audio_files/2013/04/10/110000_low.mp3'], + json_attrs(:url) + json_links = json['data'].collect { |s| s['links']['self'] } + assert_equal json_attrs(:url), json_links + end + + test 'GET index returns public list for broadcast for guest user' do + get :index, params: { broadcast_id: broadcasts(:info_april).id } + + assert_equal [192, 96], json_attrs(:bitrate) + assert_equal %w(high low), json_attrs('playback_format') + assert_equal ['http://example.com/audio_files/2013/04/10/110000_high.mp3', + 'http://example.com/audio_files/2013/04/10/110000_low.mp3'], + json_attrs(:url) + end + + test 'GET index without max_public_bitrate returns complete list for broadcast for guest user' do + archive_formats(:important_mp3).update!(max_public_bitrate: nil) + + get :index, params: { broadcast_id: broadcasts(:info_april).id } + + assert_equal [320, 192, 96], json_attrs(:bitrate) + assert_equal %w(best high low), json_attrs('playback_format') + assert_equal ['http://example.com/audio_files/2013/04/10/110000_best.mp3', + 'http://example.com/audio_files/2013/04/10/110000_high.mp3', + 'http://example.com/audio_files/2013/04/10/110000_low.mp3'], + json_attrs(:url) + end + + test 'GET show for non-public file returns 401' do + get :show, + params: { + year: '2013', + month: '04', + day: '10', + hour: '11', + min: '00', + sec: '00', + playback_format: 'best', + format: 'mp3' } + + assert_response 401 + end + + test 'GET show for public file returns audio file' do + @path = audio_files(:info_april_high).absolute_path + touch_path + get :show, + params: { + year: '2013', + month: '04', + day: '10', + hour: '11', + min: '00', + sec: '00', + playback_format: 'high', + format: 'mp3' } + + assert_response 200 + assert_equal AudioEncoding::Mp3.mime_type, response.headers['Content-Type'] + assert_match 'inline', response.headers['Content-Disposition'] + end + + test 'GET show for public file with download flag returns 401' do + @path = audio_files(:info_april_high).absolute_path + touch_path + get :show, + params: { + year: '2013', + month: '04', + day: '10', + hour: '11', + min: '00', + sec: '00', + playback_format: 'high', + format: 'mp3', + download: 'true' } + + assert_response 401 + end + + test 'GET show for file with no max_public_bitrate set returns audio file' do + @path = audio_files(:info_april_high).absolute_path + touch_path + archive_formats(:important_mp3).update!(max_public_bitrate: nil) + get :show, + params: { + year: '2013', + month: '04', + day: '10', + hour: '11', + min: '00', + sec: '00', + playback_format: 'high', + format: 'mp3' } + + assert_response 200 + assert_equal AudioEncoding::Mp3.mime_type, response.headers['Content-Type'] + end + + test 'GET show logged in at start time returns audio file' do + login + get :show, + params: { + year: '2013', + month: '05', + day: '20', + hour: '20', + min: '00', + sec: '00', + playback_format: 'high', + format: 'mp3' } + + assert_response 200 + assert_equal AudioEncoding::Mp3.mime_type, response.headers['Content-Type'] + end + + test 'GET show logged in in the middle of broadcast returns audio file' do + login + get :show, + params: { + year: '2013', + month: '05', + day: '20', + hour: '20', + min: '43', + playback_format: 'high', + format: 'mp3' } + + assert_response 200 + end + + test 'GET show logged in with best quality returns audio file' do + login + get :show, + params: { + year: '2013', + month: '05', + day: '20', + hour: '20', + min: '43', + playback_format: 'best', + format: 'mp3' } + + assert_response 200 + assert_match 'inline', response.headers['Content-Disposition'] + end + + test 'GET show logged in with best quality and download flag returns audio file' do + login + get :show, + params: { + year: '2013', + month: '05', + day: '20', + hour: '20', + min: '43', + playback_format: 'best', + format: 'mp3', + download: true } + + assert_response 200 + assert_match 'attachment', response.headers['Content-Disposition'] + end + + test 'GET show with invalid format returns 404' do + assert_raise(ActionController::UnknownFormat) do + get :show, + params: { + year: '2013', + month: '05', + day: '20', + hour: '20', + min: '43', + playback_format: 'high', + format: 'wav' } + end + end + + test 'GET show with invalid playback format returns 404' do + assert_raise(ActiveRecord::RecordNotFound) do + get :show, + params: { + year: '2013', + month: '05', + day: '20', + hour: '20', + min: '43', + playback_format: 'another', + format: 'mp3' } + end + end + + test 'GET show after broadcast returns 404' do + @path = AudioFilesController::NOT_FOUND_PATH + touch_path + get :show, + params: { + year: '2013', + month: '05', + day: '20', + hour: '23', + min: '00', + sec: '00', + playback_format: 'high', + format: 'mp3' } + + assert_response 404 + end + + test 'GET show in the future returns 404' do + @path = AudioFilesController::THE_FUTURE_PATH + touch_path + get :show, + params: { + year: '2099', + month: '05', + day: '20', + hour: '23', + min: '00', + sec: '00', + playback_format: 'high', + format: 'mp3' } + + assert_response 404 + end + + private + + def file + audio_files(:g9s_mai_high) + end + + def path + @path ||= file.absolute_path + end + + def touch_path + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.touch(path) + end + + def remove_path + FileUtils.rm(path) + end + +end diff --git a/test/controllers/broadcasts_controller_test.rb b/test/controllers/broadcasts_controller_test.rb new file mode 100644 index 0000000..1794e76 --- /dev/null +++ b/test/controllers/broadcasts_controller_test.rb @@ -0,0 +1,107 @@ +require 'test_helper' + +class BroadcastsControllerTest < ActionController::TestCase + + test 'GET index returns list of all broadcasts of the given show' do + get :index, params: { show_id: shows(:info).id } + assert_equal ['Info April', 'Info Mai'], json_attrs(:label) + end + + test 'GET index returns list of all broadcasts of the given show, respecting descending sort order' do + get :index, params: { show_id: shows(:info).id, sort: '-started_at' } + assert_equal ['Info Mai', 'Info April'], json_attrs(:label) + end + + test 'GET index returns list of all broadcasts of the given show, respecting ascending sort order' do + get :index, params: { show_id: shows(:info).id, sort: 'label' } + assert_equal ['Info April', 'Info Mai'], json_attrs(:label) + end + + test 'GET index returns bad request if sort is invalid' do + get :index, params: { show_id: shows(:info).id, sort: 'show' } + assert_equal 400, response.status + end + + test 'GET index returns bad request if sort contains multiple values' do + get :index, params: { show_id: shows(:info).id, sort: '-started_at,label' } + assert_equal 400, response.status + end + + test 'GET index with search param returns filtered list' do + broadcasts(:klangbecken_mai1).update(label: 'Klangecken Mai') + get :index, params: { show_id: shows(:info).id, q: 'Mai' } + assert_equal ['Info Mai'], json_attrs(:label) + end + + test 'GET index without show with search param returns filtered list' do + broadcasts(:klangbecken_mai1).update(label: 'Klangbecken Mai') + get :index, params: { q: 'Mai' } + assert_equal ['Info Mai', 'Klangbecken Mai'], json_attrs(:label) + end + + test 'GET index with day time range returns filtered list' do + get :index, params: { year: 2013, month: 5, day: 20 } + assert_equal ['Info Mai', 'Klangbecken', 'G9S Shizzle Edition', 'Klangbecken'], + json_attrs(:label) + end + + test 'GET index with hour time range returns filtered list' do + get :index, params: { year: 2013, month: 5, day: 20, hour: 11 } + assert_equal ['Info Mai', 'Klangbecken'], + json_attrs(:label) + end + + test 'GET index with minute time range returns filtered list' do + get :index, params: { year: 2013, month: 5, day: 20, hour: 21, minute: 0 } + assert_equal ['G9S Shizzle Edition'], + json_attrs(:label) + end + + test 'GET index with show_id and time parts resolves params correctly' do + assert_routing({ path: 'shows/42/broadcasts/2013/05/20', method: :get }, + { controller: 'broadcasts', action: 'index', show_id: '42', + year: '2013', month: '05', day: '20' }) + end + + test 'GET index only with show_id resolves params correctly' do + assert_routing({ path: 'shows/42/broadcasts', method: :get }, + { controller: 'broadcasts', action: 'index', show_id: '42' }) + end + + test 'GET index with only year resolves params correctly' do + assert_routing({ path: 'broadcasts/2013', method: :get }, + { controller: 'broadcasts', action: 'index', + year: '2013' }) + end + + test 'GET index with time parts up to month resolves params correctly' do + assert_routing({ path: 'broadcasts/2013/05', method: :get }, + { controller: 'broadcasts', action: 'index', + year: '2013', month: '05' }) + end + + test 'GET index with time parts up to day resolves params correctly' do + assert_routing({ path: 'broadcasts/2013/05/20', method: :get }, + { controller: 'broadcasts', action: 'index', + year: '2013', month: '05', day: '20' }) + end + + test 'GET index with time parts up to hour resolves params correctly' do + assert_routing({ path: 'broadcasts/2013/05/20/20', method: :get }, + { controller: 'broadcasts', action: 'index', + year: '2013', month: '05', day: '20', hour: '20' }) + end + + test 'GET index with time parts up to minute resolves params correctly' do + assert_routing({ path: 'broadcasts/2013/05/20/2015', method: :get }, + { controller: 'broadcasts', action: 'index', + year: '2013', month: '05', day: '20', hour: '20', min: '15' }) + end + + test 'GET index with time parts up to seconds resolves params correctly' do + assert_routing({ path: 'broadcasts/2013/05/20/201534', method: :get }, + { controller: 'broadcasts', action: 'index', + year: '2013', month: '05', day: '20', hour: '20', min: '15', sec: '34' }) + end + +end diff --git a/test/controllers/login_controller_test.rb b/test/controllers/login_controller_test.rb new file mode 100644 index 0000000..a9f7c2c --- /dev/null +++ b/test/controllers/login_controller_test.rb @@ -0,0 +1,70 @@ +require 'test_helper' + +class LoginControllerTest < ActionController::TestCase + + test 'GET login with REMOTE_USER returns user object' do + request.env['REMOTE_USER'] = 'speedee' + get :login + assert_response 200 + assert_equal 'speedee', json['data']['attributes']['username'] + assert_match /\A#{users(:speedee).id}\$[A-Za-z0-9]{24}\z/, + json['data']['attributes']['api_token'] + end + + test 'GET login with api_token returns user object' do + get :login, + params: { api_token: users(:speedee).api_token } + assert_response 200 + assert_equal 'speedee', json['data']['attributes']['username'] + end + + test 'POST login with REMOTE_USER returns user object' do + request.env['REMOTE_USER'] = 'speedee' + post :login, + params: { username: 'speedee', password: 'foo' } + assert_response 200 + assert_equal 'speedee', json['data']['attributes']['username'] + assert_match /\A#{users(:speedee).id}\$[A-Za-z0-9]{24}\z/, + json['data']['attributes']['api_token'] + end + + test 'POST login without REMOTE_USER returns error' do + post :login, + params: { username: 'speedee', password: 'foo' } + assert_response 401 + assert_match /Not authenticated/, response.body + end + + test 'POST login with EXTERNAL_AUTH_ERROR returns error' do + request.env['EXTERNAL_AUTH_ERROR'] = 'invalid password' + post :login, + params: { username: 'speedee', password: 'foo' } + assert_response 401 + assert_match /invalid password/, response.body + end + + test 'PUT api_key regenerates api key with REMOTE_USER' do + request.env['REMOTE_USER'] = 'speedee' + user = users(:speedee) + key = user.api_key + put :regenerate_api_key + assert_response 200 + assert_not_equal key, user.reload.api_key + assert_equal user.api_token, json['data']['attributes']['api_token'] + end + + test 'PUT api_key regenerates api key with old api_token' do + user = users(:speedee) + key = user.api_key + put :regenerate_api_key, params: { api_token: user.api_token } + assert_response 200 + assert_not_equal key, user.reload.api_key + assert_equal user.api_token, json['data']['attributes']['api_token'] + end + + test 'PUT api_key responds unauthorized without authentication' do + put :regenerate_api_key + assert_response 401 + end + +end diff --git a/test/controllers/shows_controller_test.rb b/test/controllers/shows_controller_test.rb new file mode 100644 index 0000000..de1145d --- /dev/null +++ b/test/controllers/shows_controller_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' + +class ShowsControllerTest < ActionController::TestCase + + test 'GET index returns list of all shows' do + get :index + assert_equal ['Geschäch9schlimmers', 'Info', 'Klangbecken'], json_attrs(:name) + end + + test 'GET index with query params returns list of matching shows' do + get :index, params: { q: 'e' } + assert_equal ['Geschäch9schlimmers', 'Klangbecken'], json_attrs(:name) + end + + test 'GET index with since param returns list of matching shows' do + get :index, params: { since: '2013-06-01' } + assert_equal ['Geschäch9schlimmers'], json_attrs(:name) + end + + test 'GET index with long-ago since param returns all shows' do + get :index, params: { since: '2013-05-20', page: { number: 1, size: 2 } } + assert_equal ['Geschäch9schlimmers', 'Info'], json_attrs(:name) + end + + test 'GET index with sort by last_broadcast_at returns ordered list' do + get :index, params: { sort: '-last_broadcast_at' } + assert_equal ['Geschäch9schlimmers', 'Klangbecken', 'Info'], json_attrs(:name) + end + + test 'GET index with since param and sort by last_broadcast_at returns ordered list' do + get :index, params: { since: '2013-06-01', sort: '-last_broadcast_at' } + assert_equal ['Geschäch9schlimmers'], json_attrs(:name) + end + + test 'GET show returns no profile' do + get :show, params: { id: shows(:info).id } + assert_equal 'Info', json['data']['attributes']['name'] + assert_nil json['data']['relationships'] + end + +end diff --git a/test/controllers/v1/apidocs_controller_test.rb b/test/controllers/v1/apidocs_controller_test.rb deleted file mode 100644 index ca4fdec..0000000 --- a/test/controllers/v1/apidocs_controller_test.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'test_helper' - -module V1 - class ApidocsControllerTest < ActionController::TestCase - - test 'GET index returns json' do - get :index - assert_equal 20, json['paths'].size - assert_equal 10, json['definitions'].size - end - - end -end diff --git a/test/controllers/v1/audio_files_controller_test.rb b/test/controllers/v1/audio_files_controller_test.rb deleted file mode 100644 index 5830d36..0000000 --- a/test/controllers/v1/audio_files_controller_test.rb +++ /dev/null @@ -1,264 +0,0 @@ -require 'test_helper' - -module V1 - class AudioFilesControllerTest < ActionController::TestCase - - setup :touch_path - teardown :remove_path - - test 'GET index returns complete list for broadcast for logged in user' do - login - get :index, params: { broadcast_id: broadcasts(:info_april).id } - - assert_equal [320, 192, 96], json_attrs(:bitrate) - assert_equal %w(best high low), json_attrs('playback_format') - assert_equal ['http://example.com/v1/audio_files/2013/04/10/110000_best.mp3', - 'http://example.com/v1/audio_files/2013/04/10/110000_high.mp3', - 'http://example.com/v1/audio_files/2013/04/10/110000_low.mp3'], - json_attrs(:url) - json_links = json['data'].collect { |s| s['links']['self'] } - assert_equal json_attrs(:url), json_links - end - - test 'GET index returns public list for broadcast for guest user' do - get :index, params: { broadcast_id: broadcasts(:info_april).id } - - assert_equal [192, 96], json_attrs(:bitrate) - assert_equal %w(high low), json_attrs('playback_format') - assert_equal ['http://example.com/v1/audio_files/2013/04/10/110000_high.mp3', - 'http://example.com/v1/audio_files/2013/04/10/110000_low.mp3'], - json_attrs(:url) - end - - test 'GET index without max_public_bitrate returns complete list for broadcast for guest user' do - archive_formats(:important_mp3).update!(max_public_bitrate: nil) - - get :index, params: { broadcast_id: broadcasts(:info_april).id } - - assert_equal [320, 192, 96], json_attrs(:bitrate) - assert_equal %w(best high low), json_attrs('playback_format') - assert_equal ['http://example.com/v1/audio_files/2013/04/10/110000_best.mp3', - 'http://example.com/v1/audio_files/2013/04/10/110000_high.mp3', - 'http://example.com/v1/audio_files/2013/04/10/110000_low.mp3'], - json_attrs(:url) - end - - test 'GET show for non-public file returns 401' do - get :show, - params: { - year: '2013', - month: '04', - day: '10', - hour: '11', - min: '00', - sec: '00', - playback_format: 'best', - format: 'mp3' } - - assert_response 401 - end - - test 'GET show for public file returns audio file' do - @path = audio_files(:info_april_high).absolute_path - touch_path - get :show, - params: { - year: '2013', - month: '04', - day: '10', - hour: '11', - min: '00', - sec: '00', - playback_format: 'high', - format: 'mp3' } - - assert_response 200 - assert_equal AudioEncoding::Mp3.mime_type, response.headers['Content-Type'] - assert_match 'inline', response.headers['Content-Disposition'] - end - - test 'GET show for public file with download flag returns 401' do - @path = audio_files(:info_april_high).absolute_path - touch_path - get :show, - params: { - year: '2013', - month: '04', - day: '10', - hour: '11', - min: '00', - sec: '00', - playback_format: 'high', - format: 'mp3', - download: 'true' } - - assert_response 401 - end - - test 'GET show for file with no max_public_bitrate set returns audio file' do - @path = audio_files(:info_april_high).absolute_path - touch_path - archive_formats(:important_mp3).update!(max_public_bitrate: nil) - get :show, - params: { - year: '2013', - month: '04', - day: '10', - hour: '11', - min: '00', - sec: '00', - playback_format: 'high', - format: 'mp3' } - - assert_response 200 - assert_equal AudioEncoding::Mp3.mime_type, response.headers['Content-Type'] - end - - test 'GET show logged in at start time returns audio file' do - login - get :show, - params: { - year: '2013', - month: '05', - day: '20', - hour: '20', - min: '00', - sec: '00', - playback_format: 'high', - format: 'mp3' } - - assert_response 200 - assert_equal AudioEncoding::Mp3.mime_type, response.headers['Content-Type'] - end - - test 'GET show logged in in the middle of broadcast returns audio file' do - login - get :show, - params: { - year: '2013', - month: '05', - day: '20', - hour: '20', - min: '43', - playback_format: 'high', - format: 'mp3' } - - assert_response 200 - end - - test 'GET show logged in with best quality returns audio file' do - login - get :show, - params: { - year: '2013', - month: '05', - day: '20', - hour: '20', - min: '43', - playback_format: 'best', - format: 'mp3' } - - assert_response 200 - assert_match 'inline', response.headers['Content-Disposition'] - end - - test 'GET show logged in with best quality and download flag returns audio file' do - login - get :show, - params: { - year: '2013', - month: '05', - day: '20', - hour: '20', - min: '43', - playback_format: 'best', - format: 'mp3', - download: true } - - assert_response 200 - assert_match 'attachment', response.headers['Content-Disposition'] - end - - test 'GET show with invalid format returns 404' do - assert_raise(ActionController::UnknownFormat) do - get :show, - params: { - year: '2013', - month: '05', - day: '20', - hour: '20', - min: '43', - playback_format: 'high', - format: 'wav' } - end - end - - test 'GET show with invalid playback format returns 404' do - assert_raise(ActiveRecord::RecordNotFound) do - get :show, - params: { - year: '2013', - month: '05', - day: '20', - hour: '20', - min: '43', - playback_format: 'another', - format: 'mp3' } - end - end - - test 'GET show after broadcast returns 404' do - @path = V1::AudioFilesController::NOT_FOUND_PATH - touch_path - get :show, - params: { - year: '2013', - month: '05', - day: '20', - hour: '23', - min: '00', - sec: '00', - playback_format: 'high', - format: 'mp3' } - - assert_response 404 - end - - test 'GET show in the future returns 404' do - @path = V1::AudioFilesController::THE_FUTURE_PATH - touch_path - get :show, - params: { - year: '2099', - month: '05', - day: '20', - hour: '23', - min: '00', - sec: '00', - playback_format: 'high', - format: 'mp3' } - - assert_response 404 - end - - private - - def file - audio_files(:g9s_mai_high) - end - - def path - @path ||= file.absolute_path - end - - def touch_path - FileUtils.mkdir_p(File.dirname(path)) - FileUtils.touch(path) - end - - def remove_path - FileUtils.rm(path) - end - - end -end diff --git a/test/controllers/v1/broadcasts_controller_test.rb b/test/controllers/v1/broadcasts_controller_test.rb deleted file mode 100644 index 3cd4ddd..0000000 --- a/test/controllers/v1/broadcasts_controller_test.rb +++ /dev/null @@ -1,109 +0,0 @@ -require 'test_helper' - -module V1 - class BroadcastsControllerTest < ActionController::TestCase - - test 'GET index returns list of all broadcasts of the given show' do - get :index, params: { show_id: shows(:info).id } - assert_equal ['Info April', 'Info Mai'], json_attrs(:label) - end - - test 'GET index returns list of all broadcasts of the given show, respecting descending sort order' do - get :index, params: { show_id: shows(:info).id, sort: '-started_at' } - assert_equal ['Info Mai', 'Info April'], json_attrs(:label) - end - - test 'GET index returns list of all broadcasts of the given show, respecting ascending sort order' do - get :index, params: { show_id: shows(:info).id, sort: 'label' } - assert_equal ['Info April', 'Info Mai'], json_attrs(:label) - end - - test 'GET index returns bad request if sort is invalid' do - get :index, params: { show_id: shows(:info).id, sort: 'show' } - assert_equal 400, response.status - end - - test 'GET index returns bad request if sort contains multiple values' do - get :index, params: { show_id: shows(:info).id, sort: '-started_at,label' } - assert_equal 400, response.status - end - - test 'GET index with search param returns filtered list' do - broadcasts(:klangbecken_mai1).update(label: 'Klangecken Mai') - get :index, params: { show_id: shows(:info).id, q: 'Mai' } - assert_equal ['Info Mai'], json_attrs(:label) - end - - test 'GET index without show with search param returns filtered list' do - broadcasts(:klangbecken_mai1).update(label: 'Klangbecken Mai') - get :index, params: { q: 'Mai' } - assert_equal ['Info Mai', 'Klangbecken Mai'], json_attrs(:label) - end - - test 'GET index with day time range returns filtered list' do - get :index, params: { year: 2013, month: 5, day: 20 } - assert_equal ['Info Mai', 'Klangbecken', 'G9S Shizzle Edition', 'Klangbecken'], - json_attrs(:label) - end - - test 'GET index with hour time range returns filtered list' do - get :index, params: { year: 2013, month: 5, day: 20, hour: 11 } - assert_equal ['Info Mai', 'Klangbecken'], - json_attrs(:label) - end - - test 'GET index with minute time range returns filtered list' do - get :index, params: { year: 2013, month: 5, day: 20, hour: 21, minute: 0 } - assert_equal ['G9S Shizzle Edition'], - json_attrs(:label) - end - - test 'GET index with show_id and time parts resolves params correctly' do - assert_routing({ path: 'v1/shows/42/broadcasts/2013/05/20', method: :get }, - { controller: 'v1/broadcasts', action: 'index', show_id: '42', - year: '2013', month: '05', day: '20' }) - end - - test 'GET index only with show_id resolves params correctly' do - assert_routing({ path: 'v1/shows/42/broadcasts', method: :get }, - { controller: 'v1/broadcasts', action: 'index', show_id: '42' }) - end - - test 'GET index with only year resolves params correctly' do - assert_routing({ path: 'v1/broadcasts/2013', method: :get }, - { controller: 'v1/broadcasts', action: 'index', - year: '2013' }) - end - - test 'GET index with time parts up to month resolves params correctly' do - assert_routing({ path: 'v1/broadcasts/2013/05', method: :get }, - { controller: 'v1/broadcasts', action: 'index', - year: '2013', month: '05' }) - end - - test 'GET index with time parts up to day resolves params correctly' do - assert_routing({ path: 'v1/broadcasts/2013/05/20', method: :get }, - { controller: 'v1/broadcasts', action: 'index', - year: '2013', month: '05', day: '20' }) - end - - test 'GET index with time parts up to hour resolves params correctly' do - assert_routing({ path: 'v1/broadcasts/2013/05/20/20', method: :get }, - { controller: 'v1/broadcasts', action: 'index', - year: '2013', month: '05', day: '20', hour: '20' }) - end - - test 'GET index with time parts up to minute resolves params correctly' do - assert_routing({ path: 'v1/broadcasts/2013/05/20/2015', method: :get }, - { controller: 'v1/broadcasts', action: 'index', - year: '2013', month: '05', day: '20', hour: '20', min: '15' }) - end - - test 'GET index with time parts up to seconds resolves params correctly' do - assert_routing({ path: 'v1/broadcasts/2013/05/20/201534', method: :get }, - { controller: 'v1/broadcasts', action: 'index', - year: '2013', month: '05', day: '20', hour: '20', min: '15', sec: '34' }) - end - - end -end diff --git a/test/controllers/v1/login_controller_test.rb b/test/controllers/v1/login_controller_test.rb deleted file mode 100644 index f87f663..0000000 --- a/test/controllers/v1/login_controller_test.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'test_helper' - -module V1 - class LoginControllerTest < ActionController::TestCase - - test 'GET login with REMOTE_USER returns user object' do - request.env['REMOTE_USER'] = 'speedee' - get :login - assert_response 200 - assert_equal 'speedee', json['data']['attributes']['username'] - assert_match /\A#{users(:speedee).id}\$[A-Za-z0-9]{24}\z/, - json['data']['attributes']['api_token'] - end - - test 'GET login with api_token returns user object' do - get :login, - params: { api_token: users(:speedee).api_token } - assert_response 200 - assert_equal 'speedee', json['data']['attributes']['username'] - end - - test 'POST login with REMOTE_USER returns user object' do - request.env['REMOTE_USER'] = 'speedee' - post :login, - params: { username: 'speedee', password: 'foo' } - assert_response 200 - assert_equal 'speedee', json['data']['attributes']['username'] - assert_match /\A#{users(:speedee).id}\$[A-Za-z0-9]{24}\z/, - json['data']['attributes']['api_token'] - end - - test 'POST login without REMOTE_USER returns error' do - post :login, - params: { username: 'speedee', password: 'foo' } - assert_response 401 - assert_match /Not authenticated/, response.body - end - - test 'POST login with EXTERNAL_AUTH_ERROR returns error' do - request.env['EXTERNAL_AUTH_ERROR'] = 'invalid password' - post :login, - params: { username: 'speedee', password: 'foo' } - assert_response 401 - assert_match /invalid password/, response.body - end - - end -end diff --git a/test/integration/api/authorization_test.rb b/test/integration/api/authorization_test.rb index 1e9b9e0..1a3c519 100644 --- a/test/integration/api/authorization_test.rb +++ b/test/integration/api/authorization_test.rb @@ -2,76 +2,56 @@ class AuthorizationTest < ActionDispatch::IntegrationTest - test 'POST create user as REMOTE USER with json body api creates REMOTE_USER and adds passed user' do - assert_difference('User.count', 2) do - post '/v1/users', - params: { - data: { - attributes: { - username: 'foo', - first_name: 'Pit', - last_name: 'Foo' } } }.to_json, - headers: { - 'CONTENT_TYPE' => 'application/vnd.api+json', - 'ACCEPT' => 'application/vnd.api+json' }, - env: { - 'REMOTE_USER' => 'frosch', - 'REMOTE_USER_GROUPS' => 'admin' } - assert_response 201 - assert_equal 'application/vnd.api+json; charset=utf-8', response.headers['Content-Type'] + setup { touch_audio_file } + + test 'GET show audio file as REMOTE USER is allowed and creates REMOTE_USER' do + assert_difference('User.count', 1) do + get audio_path, + env: { + 'REMOTE_USER' => 'frosch', + 'REMOTE_USER_GROUPS' => 'admin' } + assert_response 200 end - assert_equal 'foo', json['data']['attributes']['username'] end - test 'POST create user with HTTP TOKEN with json api body adds new user' do - assert_difference('User.count', 1) do + test 'GET show audio file with HTTP TOKEN is allowed' do + assert_no_difference('User.count') do auth = ActionController::HttpAuthentication::Token.encode_credentials(users(:admin).api_token) - post '/v1/users', - params: { - data: { - attributes: { - username: 'foo', - first_name: 'Pit', - last_name: 'Foo' } } }.to_json, - headers: { - 'CONTENT_TYPE' => 'application/vnd.api+json', - 'HTTP_AUTHORIZATION' => auth } - assert_response 201 - assert_equal 'application/vnd.api+json; charset=utf-8', response.headers['Content-Type'] + get audio_path, + headers: { + 'HTTP_AUTHORIZATION' => auth } + assert_response 200 end - assert_equal 'foo', json['data']['attributes']['username'] end - test 'POST create user with api_key param with json body adds new user' do - assert_difference('User.count', 1) do - post '/v1/users', - params: { - api_token: users(:admin).api_token, - data: { - attributes: { - username: 'foo', - first_name: 'Pit', - last_name: 'Foo' } } }.to_json, - headers: { - 'CONTENT_TYPE' => 'application/json' } - assert_response 201 + test 'GET show audio file with api_key param is allowed' do + assert_no_difference('User.count') do + get "#{audio_path}?api_token=#{users(:admin).api_token}" + assert_response 200 end - assert_equal 'foo', json['data']['attributes']['username'] end - test 'POST create user without authorization fails' do + test 'GET show audio file without authorization fails' do assert_no_difference('User.count') do - post '/v1/users', - params: { - data: { - attributes: { - username: 'foo', - first_name: 'Pit', - last_name: 'Foo' } } }.to_json, - headers: { - 'CONTENT_TYPE' => 'application/vnd.api+json' } + get audio_path assert_response 401 end end + private + + def touch_audio_file + path = audio_file.absolute_path + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.touch(path) + end + + def audio_path + audio_file_path(AudioPath.new(audio_file).url_params) + end + + def audio_file + audio_files(:info_april_best) + end + end diff --git a/test/integration/api/media_type_test.rb b/test/integration/api/media_type_test.rb new file mode 100644 index 0000000..682b222 --- /dev/null +++ b/test/integration/api/media_type_test.rb @@ -0,0 +1,46 @@ +require 'test_helper' + +class MediaTypeTest < ActionDispatch::IntegrationTest + + test 'POST create user with vnd.api+json body creates user' do + assert_difference('User.count', 1) do + post '/admin/users', + params: { + data: { + attributes: { + username: 'foo', + first_name: 'Pit', + last_name: 'Foo' } } }.to_json, + headers: { + 'CONTENT_TYPE' => 'application/vnd.api+json', + 'ACCEPT' => 'application/vnd.api+json' }, + env: { + 'REMOTE_USER' => users(:admin).username, + 'REMOTE_USER_GROUPS' => 'admin' } + assert_response 201 + assert_equal 'application/vnd.api+json; charset=utf-8', response.headers['Content-Type'] + end + assert_equal 'foo', json['data']['attributes']['username'] + end + + test 'POST create user with json body creates new user' do + assert_difference('User.count', 1) do + post '/admin/users', + params: { + api_token: users(:admin).api_token, + data: { + attributes: { + username: 'foo', + first_name: 'Pit', + last_name: 'Foo' } } }.to_json, + headers: { + 'CONTENT_TYPE' => 'application/json' }, + env: { + 'REMOTE_USER' => users(:admin).username, + 'REMOTE_USER_GROUPS' => 'admin' } + assert_response 201 + end + assert_equal 'foo', json['data']['attributes']['username'] + end + +end