mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-16 23:22:53 +00:00
Moving to github
This commit is contained in:
38
app/controllers/api/v1/base_controller.rb
Normal file
38
app/controllers/api/v1/base_controller.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
module Api
|
||||
module V1
|
||||
class BaseController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
before_action :authenticate_api_token
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
|
||||
|
||||
private
|
||||
|
||||
def authenticate_api_token
|
||||
token = request.headers["Authorization"]&.split(" ")&.last
|
||||
@api_token = ApiToken.active.find_by(token: token)
|
||||
|
||||
if @api_token
|
||||
@api_token.touch_last_used!
|
||||
@current_user = @api_token.user
|
||||
set_rls_user_id(@current_user.id)
|
||||
else
|
||||
render json: { error: "Invalid or missing API token" }, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user
|
||||
end
|
||||
|
||||
def not_found(exception)
|
||||
render json: { error: exception.message }, status: :not_found
|
||||
end
|
||||
|
||||
def unprocessable_entity(exception)
|
||||
render json: { errors: exception.record.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/controllers/api/v1/collections_controller.rb
Normal file
49
app/controllers/api/v1/collections_controller.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module Api
|
||||
module V1
|
||||
class CollectionsController < BaseController
|
||||
before_action :set_collection, only: [ :show, :update, :destroy ]
|
||||
|
||||
def index
|
||||
@collections = current_user.collections.includes(:games).order(:name)
|
||||
render json: @collections, include: :games
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @collection, include: :games
|
||||
end
|
||||
|
||||
def create
|
||||
@collection = current_user.collections.build(collection_params)
|
||||
|
||||
if @collection.save
|
||||
render json: @collection, status: :created
|
||||
else
|
||||
render json: { errors: @collection.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @collection.update(collection_params)
|
||||
render json: @collection
|
||||
else
|
||||
render json: { errors: @collection.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@collection.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_collection
|
||||
@collection = current_user.collections.find(params[:id])
|
||||
end
|
||||
|
||||
def collection_params
|
||||
params.require(:collection).permit(:name, :description, :parent_collection_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
95
app/controllers/api/v1/games_controller.rb
Normal file
95
app/controllers/api/v1/games_controller.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
module Api
|
||||
module V1
|
||||
class GamesController < BaseController
|
||||
before_action :set_game, only: [ :show, :update, :destroy ]
|
||||
|
||||
def index
|
||||
@games = current_user.games.includes(:platform, :genres, :collections)
|
||||
|
||||
# Filtering
|
||||
@games = @games.by_platform(params[:platform_id]) if params[:platform_id].present?
|
||||
@games = @games.by_genre(params[:genre_id]) if params[:genre_id].present?
|
||||
@games = @games.where(format: params[:format]) if params[:format].present?
|
||||
@games = @games.where(completion_status: params[:completion_status]) if params[:completion_status].present?
|
||||
@games = @games.search(params[:search]) if params[:search].present?
|
||||
|
||||
# Sorting
|
||||
@games = case params[:sort]
|
||||
when "alphabetical" then @games.alphabetical
|
||||
when "recent" then @games.recent
|
||||
when "rated" then @games.rated
|
||||
else @games.recent
|
||||
end
|
||||
|
||||
# Pagination
|
||||
page = params[:page] || 1
|
||||
per_page = params[:per_page] || 25
|
||||
|
||||
@games = @games.page(page).per(per_page)
|
||||
|
||||
render json: @games, include: [ :platform, :genres, :collections ]
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @game, include: [ :platform, :genres, :collections ]
|
||||
end
|
||||
|
||||
def create
|
||||
@game = current_user.games.build(game_params)
|
||||
|
||||
if @game.save
|
||||
render json: @game, status: :created, include: [ :platform, :genres ]
|
||||
else
|
||||
render json: { errors: @game.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @game.update(game_params)
|
||||
render json: @game, include: [ :platform, :genres, :collections ]
|
||||
else
|
||||
render json: { errors: @game.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@game.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def bulk
|
||||
results = { created: [], failed: [] }
|
||||
games_data = params[:games] || []
|
||||
|
||||
games_data.each do |game_data|
|
||||
game = current_user.games.build(game_data.permit!)
|
||||
if game.save
|
||||
results[:created] << game
|
||||
else
|
||||
results[:failed] << { data: game_data, errors: game.errors.full_messages }
|
||||
end
|
||||
end
|
||||
|
||||
render json: {
|
||||
created: results[:created].count,
|
||||
failed: results[:failed].count,
|
||||
details: results
|
||||
}, status: :created
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_game
|
||||
@game = current_user.games.find(params[:id])
|
||||
end
|
||||
|
||||
def game_params
|
||||
params.require(:game).permit(
|
||||
:title, :platform_id, :format, :date_added, :completion_status,
|
||||
:user_rating, :notes, :condition, :price_paid, :location,
|
||||
:digital_store, :custom_entry, :igdb_id, genre_ids: []
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/controllers/api/v1/genres_controller.rb
Normal file
17
app/controllers/api/v1/genres_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module Api
|
||||
module V1
|
||||
class GenresController < BaseController
|
||||
skip_before_action :authenticate_api_token, only: [ :index, :show ]
|
||||
|
||||
def index
|
||||
@genres = Genre.order(:name)
|
||||
render json: @genres
|
||||
end
|
||||
|
||||
def show
|
||||
@genre = Genre.find(params[:id])
|
||||
render json: @genre
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/controllers/api/v1/platforms_controller.rb
Normal file
17
app/controllers/api/v1/platforms_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module Api
|
||||
module V1
|
||||
class PlatformsController < BaseController
|
||||
skip_before_action :authenticate_api_token, only: [ :index, :show ]
|
||||
|
||||
def index
|
||||
@platforms = Platform.order(:name)
|
||||
render json: @platforms
|
||||
end
|
||||
|
||||
def show
|
||||
@platform = Platform.find(params[:id])
|
||||
render json: @platform
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/controllers/api_tokens_controller.rb
Normal file
31
app/controllers/api_tokens_controller.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
class ApiTokensController < ApplicationController
|
||||
before_action :require_authentication
|
||||
|
||||
def index
|
||||
@api_tokens = current_user.api_tokens.order(created_at: :desc)
|
||||
@api_token = ApiToken.new
|
||||
end
|
||||
|
||||
def create
|
||||
@api_token = current_user.api_tokens.build(api_token_params)
|
||||
|
||||
if @api_token.save
|
||||
redirect_to settings_api_tokens_path, notice: "API token created successfully. Make sure to copy it now!"
|
||||
else
|
||||
@api_tokens = current_user.api_tokens.order(created_at: :desc)
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@api_token = current_user.api_tokens.find(params[:id])
|
||||
@api_token.destroy
|
||||
redirect_to settings_api_tokens_path, notice: "API token was deleted."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_token_params
|
||||
params.require(:api_token).permit(:name, :expires_at)
|
||||
end
|
||||
end
|
||||
9
app/controllers/application_controller.rb
Normal file
9
app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include Authentication
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
|
||||
# Changes to the importmap will invalidate the etag for HTML responses
|
||||
stale_when_importmap_changes
|
||||
end
|
||||
65
app/controllers/collections_controller.rb
Normal file
65
app/controllers/collections_controller.rb
Normal file
@@ -0,0 +1,65 @@
|
||||
class CollectionsController < ApplicationController
|
||||
before_action :require_authentication
|
||||
before_action :set_collection, only: [ :show, :edit, :update, :destroy, :games ]
|
||||
|
||||
def index
|
||||
@root_collections = current_user.collections.root_collections.order(:name)
|
||||
end
|
||||
|
||||
def show
|
||||
@games = @collection.games.includes(:platform, :genres).page(params[:page]).per(25)
|
||||
end
|
||||
|
||||
def new
|
||||
@collection = current_user.collections.build
|
||||
@collections = current_user.collections.root_collections.order(:name)
|
||||
end
|
||||
|
||||
def create
|
||||
@collection = current_user.collections.build(collection_params)
|
||||
|
||||
if @collection.save
|
||||
redirect_to @collection, notice: "Collection was successfully created."
|
||||
else
|
||||
@collections = current_user.collections.root_collections.order(:name)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@collections = current_user.collections.root_collections.where.not(id: @collection.id).order(:name)
|
||||
end
|
||||
|
||||
def update
|
||||
if @collection.update(collection_params)
|
||||
redirect_to @collection, notice: "Collection was successfully updated."
|
||||
else
|
||||
@collections = current_user.collections.root_collections.where.not(id: @collection.id).order(:name)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@collection.destroy
|
||||
redirect_to collections_path, notice: "Collection was successfully deleted."
|
||||
end
|
||||
|
||||
def games
|
||||
# Same as show, but maybe with different view
|
||||
@games = @collection.games.includes(:platform, :genres).page(params[:page]).per(25)
|
||||
render :show
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_collection
|
||||
@collection = current_user.collections.find(params[:id])
|
||||
end
|
||||
|
||||
def collection_params
|
||||
permitted = params.require(:collection).permit(:name, :description, :parent_collection_id)
|
||||
# Convert empty string to nil for parent_collection_id
|
||||
permitted[:parent_collection_id] = nil if permitted[:parent_collection_id].blank?
|
||||
permitted
|
||||
end
|
||||
end
|
||||
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
66
app/controllers/concerns/authentication.rb
Normal file
66
app/controllers/concerns/authentication.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_current_user
|
||||
helper_method :current_user, :user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
|
||||
end
|
||||
|
||||
def user_signed_in?
|
||||
current_user.present?
|
||||
end
|
||||
|
||||
def require_authentication
|
||||
unless user_signed_in?
|
||||
redirect_to login_path, alert: "You must be signed in to access this page."
|
||||
end
|
||||
end
|
||||
|
||||
def require_no_authentication
|
||||
if user_signed_in?
|
||||
redirect_to root_path, notice: "You are already signed in."
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(user)
|
||||
reset_session
|
||||
session[:user_id] = user.id
|
||||
set_rls_user_id(user.id)
|
||||
end
|
||||
|
||||
def sign_out
|
||||
reset_session
|
||||
@current_user = nil
|
||||
clear_rls_user_id
|
||||
end
|
||||
|
||||
def set_current_user
|
||||
if current_user
|
||||
set_rls_user_id(current_user.id)
|
||||
else
|
||||
clear_rls_user_id
|
||||
end
|
||||
end
|
||||
|
||||
def set_rls_user_id(user_id)
|
||||
return unless ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
||||
ActiveRecord::Base.connection.execute("SET LOCAL app.current_user_id = #{ActiveRecord::Base.connection.quote(user_id)}")
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
Rails.logger.warn("Failed to set RLS user_id: #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
def clear_rls_user_id
|
||||
return unless ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
||||
ActiveRecord::Base.connection.execute("RESET app.current_user_id")
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
Rails.logger.warn("Failed to clear RLS user_id: #{e.message}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
26
app/controllers/dashboard_controller.rb
Normal file
26
app/controllers/dashboard_controller.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
class DashboardController < ApplicationController
|
||||
before_action :require_authentication
|
||||
|
||||
def index
|
||||
@recently_added_games = current_user.games.recent.limit(5)
|
||||
@currently_playing_games = current_user.games.currently_playing.limit(5)
|
||||
@total_games = current_user.games.count
|
||||
@physical_games = current_user.games.physical_games.count
|
||||
@digital_games = current_user.games.digital_games.count
|
||||
@completed_games = current_user.games.completed.count
|
||||
@backlog_games = current_user.games.backlog.count
|
||||
@total_spent = current_user.games.sum(:price_paid) || 0
|
||||
|
||||
@games_by_platform = current_user.games.joins(:platform)
|
||||
.group("platforms.name")
|
||||
.count
|
||||
.sort_by { |_, count| -count }
|
||||
.first(5)
|
||||
|
||||
@games_by_genre = current_user.games.joins(:genres)
|
||||
.group("genres.name")
|
||||
.count
|
||||
.sort_by { |_, count| -count }
|
||||
.first(5)
|
||||
end
|
||||
end
|
||||
384
app/controllers/games_controller.rb
Normal file
384
app/controllers/games_controller.rb
Normal file
@@ -0,0 +1,384 @@
|
||||
class GamesController < ApplicationController
|
||||
before_action :require_authentication
|
||||
before_action :set_game, only: [ :show, :edit, :update, :destroy ]
|
||||
|
||||
def index
|
||||
@games = current_user.games.includes(:platform, :genres, :collections)
|
||||
|
||||
# Filtering
|
||||
@games = @games.by_platform(params[:platform_id]) if params[:platform_id].present?
|
||||
@games = @games.by_genre(params[:genre_id]) if params[:genre_id].present?
|
||||
@games = @games.where(format: params[:format]) if params[:format].present?
|
||||
@games = @games.where(completion_status: params[:completion_status]) if params[:completion_status].present?
|
||||
@games = @games.search(params[:search]) if params[:search].present?
|
||||
|
||||
# Sorting
|
||||
@games = case params[:sort]
|
||||
when "alphabetical" then @games.alphabetical
|
||||
when "recent" then @games.recent
|
||||
when "rated" then @games.rated
|
||||
else @games.alphabetical
|
||||
end
|
||||
|
||||
@games = @games.page(params[:page]).per(25)
|
||||
|
||||
# For filters
|
||||
@platforms = Platform.order(:name)
|
||||
@genres = Genre.order(:name)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@game = current_user.games.build
|
||||
@platforms = Platform.order(:name)
|
||||
@genres = Genre.order(:name)
|
||||
@collections = current_user.collections.order(:name)
|
||||
end
|
||||
|
||||
def create
|
||||
@game = current_user.games.build(game_params)
|
||||
|
||||
if @game.save
|
||||
# If game was created with IGDB ID, sync the metadata
|
||||
sync_igdb_metadata_after_create if @game.igdb_id.present?
|
||||
|
||||
redirect_to @game, notice: "Game was successfully created."
|
||||
else
|
||||
@platforms = Platform.order(:name)
|
||||
@genres = Genre.order(:name)
|
||||
@collections = current_user.collections.order(:name)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@platforms = Platform.order(:name)
|
||||
@genres = Genre.order(:name)
|
||||
@collections = current_user.collections.order(:name)
|
||||
end
|
||||
|
||||
def update
|
||||
if @game.update(game_params)
|
||||
redirect_to @game, notice: "Game was successfully updated."
|
||||
else
|
||||
@platforms = Platform.order(:name)
|
||||
@genres = Genre.order(:name)
|
||||
@collections = current_user.collections.order(:name)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@game.destroy
|
||||
redirect_to games_path, notice: "Game was successfully deleted."
|
||||
end
|
||||
|
||||
def import
|
||||
@platforms = Platform.order(:name)
|
||||
@genres = Genre.order(:name)
|
||||
@collections = current_user.collections.order(:name)
|
||||
end
|
||||
|
||||
def bulk_edit
|
||||
@game_ids = params[:game_ids] || []
|
||||
|
||||
if @game_ids.empty?
|
||||
redirect_to games_path, alert: "Please select at least one game to edit."
|
||||
return
|
||||
end
|
||||
|
||||
@games = current_user.games.where(id: @game_ids)
|
||||
@platforms = Platform.order(:name)
|
||||
@genres = Genre.order(:name)
|
||||
@collections = current_user.collections.order(:name)
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
@game_ids = params[:game_ids] || []
|
||||
|
||||
if @game_ids.empty?
|
||||
redirect_to games_path, alert: "No games selected."
|
||||
return
|
||||
end
|
||||
|
||||
@games = current_user.games.where(id: @game_ids)
|
||||
updated_count = 0
|
||||
|
||||
@games.each do |game|
|
||||
updates = {}
|
||||
|
||||
# Only update fields that have values provided
|
||||
updates[:completion_status] = params[:completion_status] if params[:completion_status].present?
|
||||
updates[:location] = params[:location] if params[:location].present?
|
||||
updates[:condition] = params[:condition] if params[:condition].present?
|
||||
|
||||
# Handle collection assignment
|
||||
if params[:collection_action].present?
|
||||
case params[:collection_action]
|
||||
when "add"
|
||||
if params[:collection_ids].present?
|
||||
game.collection_ids = (game.collection_ids + params[:collection_ids].map(&:to_i)).uniq
|
||||
end
|
||||
when "remove"
|
||||
if params[:collection_ids].present?
|
||||
game.collection_ids = game.collection_ids - params[:collection_ids].map(&:to_i)
|
||||
end
|
||||
when "replace"
|
||||
game.collection_ids = params[:collection_ids] if params[:collection_ids].present?
|
||||
end
|
||||
end
|
||||
|
||||
# Handle genre assignment
|
||||
if params[:genre_action].present?
|
||||
case params[:genre_action]
|
||||
when "add"
|
||||
if params[:genre_ids].present?
|
||||
game.genre_ids = (game.genre_ids + params[:genre_ids].map(&:to_i)).uniq
|
||||
end
|
||||
when "remove"
|
||||
if params[:genre_ids].present?
|
||||
game.genre_ids = game.genre_ids - params[:genre_ids].map(&:to_i)
|
||||
end
|
||||
when "replace"
|
||||
game.genre_ids = params[:genre_ids] if params[:genre_ids].present?
|
||||
end
|
||||
end
|
||||
|
||||
if game.update(updates)
|
||||
updated_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to games_path, notice: "Successfully updated #{updated_count} game(s)."
|
||||
end
|
||||
|
||||
def bulk_create
|
||||
require "csv"
|
||||
|
||||
results = { created: 0, failed: 0, errors: [] }
|
||||
|
||||
if params[:csv_file].present?
|
||||
csv_text = params[:csv_file].read
|
||||
csv = CSV.parse(csv_text, headers: true)
|
||||
|
||||
csv.each_with_index do |row, index|
|
||||
platform = Platform.find_by(name: row["platform"]) || Platform.find_by(abbreviation: row["platform"])
|
||||
|
||||
unless platform
|
||||
results[:failed] += 1
|
||||
results[:errors] << "Row #{index + 2}: Platform '#{row['platform']}' not found"
|
||||
next
|
||||
end
|
||||
|
||||
game = current_user.games.build(
|
||||
title: row["title"],
|
||||
platform: platform,
|
||||
format: row["format"]&.downcase || "physical",
|
||||
date_added: row["date_added"] || Date.current,
|
||||
completion_status: row["completion_status"]&.downcase,
|
||||
user_rating: row["user_rating"],
|
||||
condition: row["condition"]&.downcase,
|
||||
price_paid: row["price_paid"],
|
||||
location: row["location"],
|
||||
digital_store: row["digital_store"],
|
||||
notes: row["notes"]
|
||||
)
|
||||
|
||||
# Handle genres
|
||||
if row["genres"].present?
|
||||
genre_names = row["genres"].split("|").map(&:strip)
|
||||
genres = Genre.where(name: genre_names)
|
||||
game.genres = genres
|
||||
end
|
||||
|
||||
if game.save
|
||||
results[:created] += 1
|
||||
else
|
||||
results[:failed] += 1
|
||||
results[:errors] << "Row #{index + 2}: #{game.errors.full_messages.join(", ")}"
|
||||
end
|
||||
end
|
||||
|
||||
flash[:notice] = "Created #{results[:created]} games. Failed: #{results[:failed]}"
|
||||
flash[:alert] = results[:errors].join("<br>").html_safe if results[:errors].any?
|
||||
redirect_to games_path
|
||||
else
|
||||
redirect_to import_games_path, alert: "Please select a CSV file to upload."
|
||||
end
|
||||
end
|
||||
|
||||
def search_igdb
|
||||
query = params[:q].to_s.strip
|
||||
platform_id = params[:platform_id]
|
||||
|
||||
if query.length < 2
|
||||
render json: []
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
service = IgdbService.new
|
||||
platform = platform_id.present? ? Platform.find_by(id: platform_id) : nil
|
||||
|
||||
# Search IGDB (limit to 10 results for autocomplete)
|
||||
results = service.search_game(query, platform, 10)
|
||||
|
||||
# Format results for autocomplete
|
||||
formatted_results = results.map do |result|
|
||||
# Map IGDB genres to our local genre IDs
|
||||
genre_ids = map_igdb_genres_to_ids(result[:genres] || [])
|
||||
|
||||
{
|
||||
igdb_id: result[:igdb_id],
|
||||
name: result[:name],
|
||||
platform: result[:platform_name],
|
||||
year: result[:release_year],
|
||||
cover_url: result[:cover_url],
|
||||
summary: result[:summary],
|
||||
genres: result[:genres],
|
||||
genre_ids: genre_ids,
|
||||
confidence: result[:confidence_score]
|
||||
}
|
||||
end
|
||||
|
||||
render json: formatted_results
|
||||
rescue => e
|
||||
Rails.logger.error("IGDB search error: #{e.message}")
|
||||
render json: [], status: :internal_server_error
|
||||
end
|
||||
end
|
||||
|
||||
def search_locations
|
||||
query = params[:q].to_s.strip
|
||||
|
||||
# Get unique locations from user's games that match the query
|
||||
locations = current_user.games
|
||||
.where.not(location: [nil, ""])
|
||||
.where("location ILIKE ?", "%#{query}%")
|
||||
.select(:location)
|
||||
.distinct
|
||||
.order(:location)
|
||||
.limit(10)
|
||||
.pluck(:location)
|
||||
|
||||
render json: locations
|
||||
end
|
||||
|
||||
def search_stores
|
||||
query = params[:q].to_s.strip
|
||||
|
||||
# Get unique digital stores from user's games that match the query
|
||||
stores = current_user.games
|
||||
.where.not(digital_store: [nil, ""])
|
||||
.where("digital_store ILIKE ?", "%#{query}%")
|
||||
.select(:digital_store)
|
||||
.distinct
|
||||
.order(:digital_store)
|
||||
.limit(10)
|
||||
.pluck(:digital_store)
|
||||
|
||||
render json: stores
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_game
|
||||
@game = current_user.games.includes(:igdb_game).find(params[:id])
|
||||
end
|
||||
|
||||
def sync_igdb_metadata_after_create
|
||||
# Fetch full game data from IGDB
|
||||
service = IgdbService.new
|
||||
igdb_data = service.get_game(@game.igdb_id)
|
||||
|
||||
return unless igdb_data
|
||||
|
||||
# Create or update IgdbGame record
|
||||
igdb_game = IgdbGame.find_or_create_by!(igdb_id: @game.igdb_id) do |ig|
|
||||
ig.name = igdb_data["name"]
|
||||
ig.slug = igdb_data["slug"]
|
||||
ig.summary = igdb_data["summary"]
|
||||
ig.first_release_date = igdb_data["first_release_date"] ? Time.at(igdb_data["first_release_date"]).to_date : nil
|
||||
|
||||
# Extract cover URL
|
||||
cover_url = igdb_data.dig("cover", "url")&.split("/")&.last&.sub(".jpg", "")
|
||||
ig.cover_url = cover_url
|
||||
|
||||
ig.last_synced_at = Time.current
|
||||
end
|
||||
|
||||
igdb_game.increment_match_count!
|
||||
|
||||
# Update game with IGDB metadata
|
||||
@game.update(
|
||||
igdb_matched_at: Time.current,
|
||||
igdb_match_status: "matched",
|
||||
igdb_match_confidence: 100.0
|
||||
)
|
||||
|
||||
# Map and assign genres
|
||||
if igdb_data["genres"].present?
|
||||
genre_names = igdb_data["genres"].map { |g| g["name"] }
|
||||
assign_igdb_genres_to_game(genre_names)
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error("Failed to sync IGDB metadata: #{e.message}")
|
||||
end
|
||||
|
||||
def map_igdb_genres_to_ids(genre_names)
|
||||
return [] if genre_names.blank?
|
||||
|
||||
# Genre mapping (same as in IgdbMatchSuggestion)
|
||||
genre_mappings = {
|
||||
"Role-playing (RPG)" => "RPG",
|
||||
"Fighting" => "Fighting",
|
||||
"Shooter" => "Shooter",
|
||||
"Platform" => "Platformer",
|
||||
"Puzzle" => "Puzzle",
|
||||
"Racing" => "Racing",
|
||||
"Real Time Strategy (RTS)" => "Strategy",
|
||||
"Simulator" => "Simulation",
|
||||
"Sport" => "Sports",
|
||||
"Strategy" => "Strategy",
|
||||
"Adventure" => "Adventure",
|
||||
"Indie" => "Indie",
|
||||
"Arcade" => "Arcade",
|
||||
"Hack and slash/Beat 'em up" => "Action"
|
||||
}
|
||||
|
||||
genre_ids = []
|
||||
genre_names.each do |igdb_genre_name|
|
||||
# Try exact match
|
||||
local_genre = Genre.find_by("LOWER(name) = ?", igdb_genre_name.downcase)
|
||||
|
||||
# Try mapped name
|
||||
if local_genre.nil? && genre_mappings[igdb_genre_name]
|
||||
mapped_name = genre_mappings[igdb_genre_name]
|
||||
local_genre = Genre.find_by("LOWER(name) = ?", mapped_name.downcase)
|
||||
end
|
||||
|
||||
genre_ids << local_genre.id if local_genre
|
||||
end
|
||||
|
||||
genre_ids
|
||||
end
|
||||
|
||||
def assign_igdb_genres_to_game(genre_names)
|
||||
genre_ids = map_igdb_genres_to_ids(genre_names)
|
||||
|
||||
genre_ids.each do |genre_id|
|
||||
genre = Genre.find(genre_id)
|
||||
@game.genres << genre unless @game.genres.include?(genre)
|
||||
end
|
||||
end
|
||||
|
||||
def game_params
|
||||
params.require(:game).permit(
|
||||
:title, :platform_id, :format, :date_added, :completion_status,
|
||||
:user_rating, :notes, :condition, :price_paid, :location,
|
||||
:digital_store, :custom_entry, :igdb_id, genre_ids: [], collection_ids: []
|
||||
)
|
||||
end
|
||||
end
|
||||
69
app/controllers/igdb_matches_controller.rb
Normal file
69
app/controllers/igdb_matches_controller.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
class IgdbMatchesController < ApplicationController
|
||||
before_action :require_authentication
|
||||
before_action :set_suggestion, only: [ :approve, :reject ]
|
||||
|
||||
def index
|
||||
# Only show suggestions for games that haven't been matched yet
|
||||
@pending_suggestions = current_user.igdb_match_suggestions
|
||||
.pending_review
|
||||
.joins(:game)
|
||||
.where(games: { igdb_id: nil })
|
||||
.includes(game: :platform)
|
||||
.group_by(&:game)
|
||||
|
||||
@matched_games = current_user.games.igdb_matched.count
|
||||
@unmatched_games = current_user.games.igdb_unmatched.count
|
||||
@pending_review_count = current_user.igdb_match_suggestions
|
||||
.status_pending
|
||||
.joins(:game)
|
||||
.where(games: { igdb_id: nil })
|
||||
.count
|
||||
end
|
||||
|
||||
def approve
|
||||
if @suggestion.approve!
|
||||
redirect_to igdb_matches_path, notice: "Match approved! #{@suggestion.game.title} linked to IGDB."
|
||||
else
|
||||
redirect_to igdb_matches_path, alert: "Failed to approve match."
|
||||
end
|
||||
end
|
||||
|
||||
def reject
|
||||
if @suggestion.reject!
|
||||
redirect_to igdb_matches_path, notice: "Match rejected."
|
||||
else
|
||||
redirect_to igdb_matches_path, alert: "Failed to reject match."
|
||||
end
|
||||
end
|
||||
|
||||
def sync_now
|
||||
# Auto-enable sync if not already enabled
|
||||
unless current_user.igdb_sync_enabled?
|
||||
current_user.update(igdb_sync_enabled: true)
|
||||
end
|
||||
|
||||
# Check if games need syncing
|
||||
unmatched_count = current_user.games.igdb_unmatched.where(igdb_match_status: [nil, "failed"]).count
|
||||
|
||||
if unmatched_count == 0
|
||||
redirect_to igdb_matches_path, alert: "All games are already matched or being processed!"
|
||||
return
|
||||
end
|
||||
|
||||
# Try to run job immediately for faster feedback
|
||||
begin
|
||||
IgdbSyncJob.perform_later
|
||||
|
||||
redirect_to igdb_matches_path, notice: "IGDB sync started! Processing #{unmatched_count} games. This may take a few minutes - the page will auto-refresh."
|
||||
rescue => e
|
||||
Rails.logger.error("Failed to start IGDB sync: #{e.message}")
|
||||
redirect_to igdb_matches_path, alert: "Failed to start sync. Make sure background jobs are running."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_suggestion
|
||||
@suggestion = current_user.igdb_match_suggestions.find(params[:id])
|
||||
end
|
||||
end
|
||||
59
app/controllers/items_controller.rb
Normal file
59
app/controllers/items_controller.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
class ItemsController < ApplicationController
|
||||
before_action :require_authentication
|
||||
before_action :set_item, only: [ :show, :edit, :update, :destroy ]
|
||||
|
||||
def index
|
||||
@items = current_user.items.includes(:platform).order(date_added: :desc).page(params[:page]).per(25)
|
||||
@platforms = Platform.order(:name)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@item = current_user.items.build
|
||||
@platforms = Platform.order(:name)
|
||||
end
|
||||
|
||||
def create
|
||||
@item = current_user.items.build(item_params)
|
||||
|
||||
if @item.save
|
||||
redirect_to @item, notice: "Item was successfully created."
|
||||
else
|
||||
@platforms = Platform.order(:name)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@platforms = Platform.order(:name)
|
||||
end
|
||||
|
||||
def update
|
||||
if @item.update(item_params)
|
||||
redirect_to @item, notice: "Item was successfully updated."
|
||||
else
|
||||
@platforms = Platform.order(:name)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@item.destroy
|
||||
redirect_to items_path, notice: "Item was successfully deleted."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_item
|
||||
@item = current_user.items.find(params[:id])
|
||||
end
|
||||
|
||||
def item_params
|
||||
params.require(:item).permit(
|
||||
:name, :item_type, :platform_id, :condition, :price_paid,
|
||||
:location, :date_added, :notes, :igdb_id
|
||||
)
|
||||
end
|
||||
end
|
||||
11
app/controllers/pages_controller.rb
Normal file
11
app/controllers/pages_controller.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class PagesController < ApplicationController
|
||||
def home
|
||||
if user_signed_in?
|
||||
redirect_to dashboard_path
|
||||
end
|
||||
end
|
||||
|
||||
def api_docs
|
||||
# API documentation page - publicly accessible
|
||||
end
|
||||
end
|
||||
46
app/controllers/password_resets_controller.rb
Normal file
46
app/controllers/password_resets_controller.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
class PasswordResetsController < ApplicationController
|
||||
before_action :require_no_authentication, only: [ :new, :create, :edit, :update ]
|
||||
before_action :set_user_by_token, only: [ :edit, :update ]
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
user = User.find_by(email: params[:email].downcase)
|
||||
|
||||
if user
|
||||
user.generate_password_reset_token
|
||||
PasswordResetMailer.reset_password(user).deliver_later
|
||||
end
|
||||
|
||||
# Always show success message to prevent email enumeration
|
||||
redirect_to login_path, notice: "If an account exists with that email, you will receive password reset instructions."
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(password_params)
|
||||
@user.update_columns(password_reset_token: nil, password_reset_sent_at: nil)
|
||||
sign_in(@user)
|
||||
redirect_to dashboard_path, notice: "Your password has been reset successfully."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user_by_token
|
||||
@user = User.find_by(password_reset_token: params[:id])
|
||||
|
||||
unless @user && !@user.password_reset_expired?
|
||||
redirect_to new_password_reset_path, alert: "Password reset link is invalid or has expired."
|
||||
end
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
end
|
||||
end
|
||||
24
app/controllers/profiles_controller.rb
Normal file
24
app/controllers/profiles_controller.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
class ProfilesController < ApplicationController
|
||||
def show
|
||||
@user = User.find_by!(username: params[:username])
|
||||
|
||||
unless @user.profile_public? || @user == current_user
|
||||
redirect_to root_path, alert: "This profile is private."
|
||||
return
|
||||
end
|
||||
|
||||
@total_games = @user.games.count
|
||||
@physical_games = @user.games.physical_games.count
|
||||
@digital_games = @user.games.digital_games.count
|
||||
@completed_games = @user.games.completed.count
|
||||
|
||||
@games_by_platform = @user.games.joins(:platform)
|
||||
.group("platforms.name")
|
||||
.count
|
||||
.sort_by { |_, count| -count }
|
||||
.first(10)
|
||||
|
||||
@public_collections = @user.collections.root_collections.order(:name)
|
||||
@recent_games = @user.games.includes(:platform).recent.limit(10)
|
||||
end
|
||||
end
|
||||
24
app/controllers/sessions_controller.rb
Normal file
24
app/controllers/sessions_controller.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
class SessionsController < ApplicationController
|
||||
before_action :require_no_authentication, only: [ :new, :create ]
|
||||
before_action :require_authentication, only: [ :destroy ]
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
user = User.find_by(email: params[:email].downcase)
|
||||
|
||||
if user && user.authenticate(params[:password])
|
||||
sign_in(user)
|
||||
redirect_to dashboard_path, notice: "Welcome back, #{user.username}!"
|
||||
else
|
||||
flash.now[:alert] = "Invalid email or password"
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
sign_out
|
||||
redirect_to root_path, notice: "You have been signed out."
|
||||
end
|
||||
end
|
||||
46
app/controllers/users_controller.rb
Normal file
46
app/controllers/users_controller.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
class UsersController < ApplicationController
|
||||
before_action :require_no_authentication, only: [ :new, :create ]
|
||||
before_action :require_authentication, only: [ :edit, :update, :settings ]
|
||||
before_action :set_user, only: [ :edit, :update ]
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
|
||||
if @user.save
|
||||
sign_in(@user)
|
||||
redirect_to dashboard_path, notice: "Welcome to TurboVault, #{@user.username}!"
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(user_params)
|
||||
redirect_to settings_path, notice: "Your profile has been updated."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def settings
|
||||
@user = current_user
|
||||
@api_tokens = current_user.api_tokens.order(created_at: :desc)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :username, :password, :password_confirmation, :bio, :profile_public, :igdb_sync_enabled, :theme)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user