From cc7ac1956bc2cc2f60f4a7ed7fb9543a134967e0 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 12:58:44 -0800 Subject: [PATCH] add artifact controllers and routes --- .../api/v1/artifact_skills_controller.rb | 29 ++++ .../api/v1/artifacts_controller.rb | 31 ++++ .../api/v1/collection_artifacts_controller.rb | 147 ++++++++++++++++++ .../api/v1/grid_artifacts_controller.rb | 119 ++++++++++++++ config/routes.rb | 17 ++ 5 files changed, 343 insertions(+) create mode 100644 app/controllers/api/v1/artifact_skills_controller.rb create mode 100644 app/controllers/api/v1/artifacts_controller.rb create mode 100644 app/controllers/api/v1/collection_artifacts_controller.rb create mode 100644 app/controllers/api/v1/grid_artifacts_controller.rb diff --git a/app/controllers/api/v1/artifact_skills_controller.rb b/app/controllers/api/v1/artifact_skills_controller.rb new file mode 100644 index 0000000..93551ae --- /dev/null +++ b/app/controllers/api/v1/artifact_skills_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Api + module V1 + class ArtifactSkillsController < Api::V1::ApiController + # GET /artifact_skills + def index + @skills = ArtifactSkill.all + @skills = @skills.where(skill_group: params[:group]) if params[:group].present? + @skills = @skills.where(polarity: params[:polarity]) if params[:polarity].present? + + render json: ArtifactSkillBlueprint.render(@skills, root: :artifact_skills) + end + + # GET /artifact_skills/for_slot/:slot + # Returns skills valid for a specific slot (1-4) + def for_slot + slot = params[:slot].to_i + + unless (1..4).cover?(slot) + return render json: { error: 'Slot must be between 1 and 4' }, status: :unprocessable_entity + end + + @skills = ArtifactSkill.for_slot(slot) + render json: ArtifactSkillBlueprint.render(@skills, root: :artifact_skills) + end + end + end +end diff --git a/app/controllers/api/v1/artifacts_controller.rb b/app/controllers/api/v1/artifacts_controller.rb new file mode 100644 index 0000000..fc621de --- /dev/null +++ b/app/controllers/api/v1/artifacts_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Api + module V1 + class ArtifactsController < Api::V1::ApiController + before_action :set_artifact, only: [:show] + + # GET /artifacts + def index + @artifacts = Artifact.all + @artifacts = @artifacts.where(rarity: params[:rarity]) if params[:rarity].present? + @artifacts = @artifacts.where(proficiency: params[:proficiency]) if params[:proficiency].present? + + render json: ArtifactBlueprint.render(@artifacts, root: :artifacts) + end + + # GET /artifacts/:id + def show + render json: ArtifactBlueprint.render(@artifact) + end + + private + + def set_artifact + @artifact = Artifact.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_not_found_response('artifact') + end + end + end +end diff --git a/app/controllers/api/v1/collection_artifacts_controller.rb b/app/controllers/api/v1/collection_artifacts_controller.rb new file mode 100644 index 0000000..258f706 --- /dev/null +++ b/app/controllers/api/v1/collection_artifacts_controller.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +module Api + module V1 + class CollectionArtifactsController < ApiController + # Read actions: look up user from params, check privacy + before_action :set_target_user, only: %i[index show] + before_action :check_collection_access, only: %i[index show] + before_action :set_collection_artifact_for_read, only: %i[show] + + # Write actions: require auth, use current_user + before_action :restrict_access, only: %i[create update destroy batch] + before_action :set_collection_artifact_for_write, only: %i[update destroy] + + def index + @collection_artifacts = @target_user.collection_artifacts.includes(:artifact) + + @collection_artifacts = @collection_artifacts.where(artifact_id: params[:artifact_id]) if params[:artifact_id] + @collection_artifacts = @collection_artifacts.where(element: params[:element]) if params[:element] + + @collection_artifacts = @collection_artifacts.paginate(page: params[:page], per_page: params[:limit] || 50) + + render json: Api::V1::CollectionArtifactBlueprint.render( + @collection_artifacts, + root: :artifacts, + meta: pagination_meta(@collection_artifacts) + ) + end + + def show + render json: Api::V1::CollectionArtifactBlueprint.render( + @collection_artifact, + view: :full + ) + end + + def create + @collection_artifact = current_user.collection_artifacts.build(collection_artifact_params) + + if @collection_artifact.save + render json: Api::V1::CollectionArtifactBlueprint.render( + @collection_artifact, + view: :full + ), status: :created + else + render_validation_error_response(@collection_artifact) + end + end + + def update + if @collection_artifact.update(collection_artifact_params) + render json: Api::V1::CollectionArtifactBlueprint.render( + @collection_artifact, + view: :full + ) + else + render_validation_error_response(@collection_artifact) + end + end + + def destroy + @collection_artifact.destroy + head :no_content + end + + # POST /collection/artifacts/batch + # Creates multiple collection artifacts in a single request + def batch + items = batch_artifact_params[:collection_artifacts] || [] + created = [] + errors = [] + + ActiveRecord::Base.transaction do + items.each_with_index do |item_params, index| + collection_artifact = current_user.collection_artifacts.build(item_params) + + if collection_artifact.save + created << collection_artifact + else + errors << { + index: index, + artifact_id: item_params[:artifact_id], + error: collection_artifact.errors.full_messages.join(', ') + } + end + end + end + + status = errors.any? ? :multi_status : :created + + render json: Api::V1::CollectionArtifactBlueprint.render( + created, + root: :artifacts, + meta: { created: created.size, errors: errors } + ), status: status + end + + private + + def set_target_user + @target_user = User.find(params[:user_id]) + rescue ActiveRecord::RecordNotFound + render json: { error: 'User not found' }, status: :not_found + end + + def check_collection_access + return if @target_user.nil? + + return if @target_user.collection_viewable_by?(current_user) + + render json: { error: 'You do not have permission to view this collection' }, status: :forbidden + end + + def set_collection_artifact_for_read + @collection_artifact = @target_user.collection_artifacts.find(params[:id]) + rescue ActiveRecord::RecordNotFound + raise CollectionErrors::CollectionItemNotFound.new('artifact', params[:id]) + end + + def set_collection_artifact_for_write + @collection_artifact = current_user.collection_artifacts.find(params[:id]) + rescue ActiveRecord::RecordNotFound + raise CollectionErrors::CollectionItemNotFound.new('artifact', params[:id]) + end + + def collection_artifact_params + params.require(:collection_artifact).permit( + :artifact_id, :element, :proficiency, :level, :nickname, + skill1: %i[modifier strength level], + skill2: %i[modifier strength level], + skill3: %i[modifier strength level], + skill4: %i[modifier strength level] + ) + end + + def batch_artifact_params + params.permit(collection_artifacts: [ + :artifact_id, :element, :proficiency, :level, :nickname, + { skill1: %i[modifier strength level] }, + { skill2: %i[modifier strength level] }, + { skill3: %i[modifier strength level] }, + { skill4: %i[modifier strength level] } + ]) + end + end + end +end diff --git a/app/controllers/api/v1/grid_artifacts_controller.rb b/app/controllers/api/v1/grid_artifacts_controller.rb new file mode 100644 index 0000000..6fb558c --- /dev/null +++ b/app/controllers/api/v1/grid_artifacts_controller.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module Api + module V1 + class GridArtifactsController < Api::V1::ApiController + before_action :find_grid_artifact, only: %i[update destroy] + before_action :find_party, only: %i[create update destroy] + before_action :find_grid_character, only: %i[create] + before_action :find_artifact, only: %i[create] + before_action :authorize_party_edit!, only: %i[create update destroy] + + # POST /grid_artifacts + def create + # Check if grid_character already has an artifact + if @grid_character.grid_artifact.present? + @grid_character.grid_artifact.destroy + end + + @grid_artifact = GridArtifact.new( + grid_artifact_params.merge( + grid_character_id: @grid_character.id, + artifact_id: @artifact.id + ) + ) + + if @grid_artifact.save + render json: GridArtifactBlueprint.render(@grid_artifact, view: :nested, root: :grid_artifact), status: :created + else + render_validation_error_response(@grid_artifact) + end + end + + # PATCH/PUT /grid_artifacts/:id + def update + if @grid_artifact.update(grid_artifact_params) + render json: GridArtifactBlueprint.render(@grid_artifact, view: :nested, root: :grid_artifact), status: :ok + else + render_validation_error_response(@grid_artifact) + end + end + + # DELETE /grid_artifacts/:id + def destroy + if @grid_artifact.destroy + render json: GridArtifactBlueprint.render(@grid_artifact, view: :destroyed), status: :ok + else + render_unprocessable_entity_response( + Api::V1::GranblueError.new(@grid_artifact.errors.full_messages.join(', ')) + ) + end + end + + private + + def find_grid_artifact + @grid_artifact = GridArtifact.find_by(id: params[:id]) + render_not_found_response('grid_artifact') unless @grid_artifact + end + + def find_party + @party = if @grid_artifact + @grid_artifact.grid_character.party + else + Party.find_by(id: params[:party_id]) + end + render_not_found_response('party') unless @party + end + + def find_grid_character + @grid_character = GridCharacter.find_by(id: params.dig(:grid_artifact, :grid_character_id)) + render_not_found_response('grid_character') unless @grid_character + end + + def find_artifact + artifact_id = params.dig(:grid_artifact, :artifact_id) + @artifact = Artifact.find_by(id: artifact_id) + render_not_found_response('artifact') unless @artifact + end + + def authorize_party_edit! + if @party.user.present? + authorize_user_party + else + authorize_anonymous_party + end + end + + def authorize_user_party + return if current_user.present? && @party.user == current_user + + render_unauthorized_response + end + + def authorize_anonymous_party + provided_edit_key = edit_key.to_s.strip.force_encoding('UTF-8') + party_edit_key = @party.edit_key.to_s.strip.force_encoding('UTF-8') + return if valid_edit_key?(provided_edit_key, party_edit_key) + + render_unauthorized_response + end + + def valid_edit_key?(provided_edit_key, party_edit_key) + provided_edit_key.present? && + provided_edit_key.bytesize == party_edit_key.bytesize && + ActiveSupport::SecurityUtils.secure_compare(provided_edit_key, party_edit_key) + end + + def grid_artifact_params + params.require(:grid_artifact).permit( + :grid_character_id, :artifact_id, :element, :proficiency, :level, + skill1: %i[modifier strength level], + skill2: %i[modifier strength level], + skill3: %i[modifier strength level], + skill4: %i[modifier strength level] + ) + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index db4665e..c1034f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -101,6 +101,17 @@ Rails.application.routes.draw do resources :weapon_series, only: %i[index show create update destroy] + # Artifacts (read-only reference data) + resources :artifacts, only: %i[index show] + resources :artifact_skills, only: %i[index] do + collection do + get 'for_slot/:slot', action: :for_slot, as: :for_slot + end + end + + # Grid artifacts + resources :grid_artifacts, only: %i[create update destroy] + # Grid endpoints - new prefixed versions post 'grid_characters/resolve', to: 'grid_characters#resolve' post 'grid_characters/update_uncap', to: 'grid_characters#update_uncap_level' @@ -134,6 +145,7 @@ Rails.application.routes.draw do resources :characters, only: [:index, :show], controller: '/api/v1/collection_characters' resources :weapons, only: [:index, :show], controller: '/api/v1/collection_weapons' resources :summons, only: [:index, :show], controller: '/api/v1/collection_summons' + resources :artifacts, only: [:index, :show], controller: '/api/v1/collection_artifacts' end end @@ -156,6 +168,11 @@ Rails.application.routes.draw do end resources :job_accessories, controller: '/api/v1/collection_job_accessories', only: [:index, :show, :create, :destroy] + resources :artifacts, only: [:create, :update, :destroy], controller: '/api/v1/collection_artifacts' do + collection do + post :batch + end + end end end