Compare commits
2 Commits
1655334922
...
311ecafb74
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
311ecafb74 | ||
|
|
6d74e7aff1 |
159
Gemfile.lock
159
Gemfile.lock
@@ -1,29 +1,29 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actioncable (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionmailbox (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activestorage (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionmailer (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
actionview (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionpack (8.0.3)
|
||||
actionview (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -31,35 +31,35 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actiontext (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activestorage (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionview (8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activejob (8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activerecord (8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activemodel (8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
activerecord (8.0.3)
|
||||
activemodel (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activestorage (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.2.1)
|
||||
activesupport (8.0.3)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
@@ -113,14 +113,14 @@ GEM
|
||||
addressable (~> 2.8)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.4.0)
|
||||
erb (5.0.2)
|
||||
erb (5.0.3)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.3.0)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
fugit (1.11.2)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@@ -138,7 +138,7 @@ GEM
|
||||
activesupport (>= 7.0.0)
|
||||
jsbundling-rails (1.3.1)
|
||||
railties (>= 6.0.0)
|
||||
json (2.13.2)
|
||||
json (2.15.0)
|
||||
kamal (2.7.0)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
@@ -161,7 +161,7 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.3)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.5)
|
||||
@@ -176,7 +176,7 @@ GEM
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
msgpack (1.8.0)
|
||||
net-imap (0.5.10)
|
||||
net-imap (0.5.12)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -217,11 +217,11 @@ GEM
|
||||
phlex (~> 2.3.0)
|
||||
railties (>= 7.1, < 9)
|
||||
zeitwerk (~> 2.7)
|
||||
pp (0.6.2)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.5.1)
|
||||
propshaft (1.2.1)
|
||||
propshaft (1.3.1)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
@@ -229,11 +229,11 @@ GEM
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puma (7.0.3)
|
||||
puma (7.0.4)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.1)
|
||||
rack (3.2.2)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
@@ -241,20 +241,20 @@ GEM
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.2.1)
|
||||
actioncable (= 8.0.2.1)
|
||||
actionmailbox (= 8.0.2.1)
|
||||
actionmailer (= 8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actiontext (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
rails (8.0.3)
|
||||
actioncable (= 8.0.3)
|
||||
actionmailbox (= 8.0.3)
|
||||
actionmailer (= 8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
actiontext (= 8.0.3)
|
||||
actionview (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activemodel (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activestorage (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2.1)
|
||||
railties (= 8.0.3)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -262,24 +262,26 @@ GEM
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
railties (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.2)
|
||||
rdoc (6.15.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.2)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.4.4)
|
||||
rubocop (1.80.2)
|
||||
rubocop (1.81.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -287,17 +289,17 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.46.0, < 2.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.46.0)
|
||||
rubocop-ast (1.47.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.26.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rails (2.33.3)
|
||||
rubocop-rails (2.33.4)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
@@ -308,7 +310,7 @@ GEM
|
||||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
rubyzip (3.1.0)
|
||||
rubyzip (3.1.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.35.0)
|
||||
base64 (~> 0.2)
|
||||
@@ -333,13 +335,13 @@ GEM
|
||||
railties (>= 7.1)
|
||||
thor (>= 1.3.1)
|
||||
sqids (0.2.2)
|
||||
sqlite3 (2.7.3-aarch64-linux-gnu)
|
||||
sqlite3 (2.7.3-aarch64-linux-musl)
|
||||
sqlite3 (2.7.3-arm-linux-gnu)
|
||||
sqlite3 (2.7.3-arm-linux-musl)
|
||||
sqlite3 (2.7.3-arm64-darwin)
|
||||
sqlite3 (2.7.3-x86_64-linux-gnu)
|
||||
sqlite3 (2.7.3-x86_64-linux-musl)
|
||||
sqlite3 (2.7.4-aarch64-linux-gnu)
|
||||
sqlite3 (2.7.4-aarch64-linux-musl)
|
||||
sqlite3 (2.7.4-arm-linux-gnu)
|
||||
sqlite3 (2.7.4-arm-linux-musl)
|
||||
sqlite3 (2.7.4-arm64-darwin)
|
||||
sqlite3 (2.7.4-x86_64-linux-gnu)
|
||||
sqlite3 (2.7.4-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
base64
|
||||
logger
|
||||
@@ -365,7 +367,8 @@ GEM
|
||||
thruster (0.1.15-arm64-darwin)
|
||||
thruster (0.1.15-x86_64-linux)
|
||||
timeout (0.4.3)
|
||||
turbo-rails (2.0.16)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.17)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
@@ -373,7 +376,7 @@ GEM
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.0.3)
|
||||
uri (1.0.4)
|
||||
useragent (0.16.11)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
|
||||
@@ -16,6 +16,18 @@ class Components::Libraries::EditView < Components::Base
|
||||
end
|
||||
|
||||
div(class: "bg-white rounded-lg shadow-md p-6") do
|
||||
# Display errors if any
|
||||
if @library.errors.any?
|
||||
div(class: "mb-6 bg-red-50 border border-red-200 rounded-lg p-4") do
|
||||
h3(class: "text-red-800 font-semibold mb-2") { "Error#{@library.errors.count > 1 ? 's' : ''}" }
|
||||
ul(class: "list-disc list-inside text-red-700 text-sm") do
|
||||
@library.errors.full_messages.each do |message|
|
||||
li { message }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
form(action: library_path(@library), method: "post") do
|
||||
input(type: "hidden", name: "_method", value: "patch")
|
||||
input(type: "hidden", name: "authenticity_token", value: form_authenticity_token)
|
||||
|
||||
@@ -101,32 +101,47 @@ class Components::User::ProfileView < Components::Base
|
||||
|
||||
# API Configuration Section
|
||||
div(class: "px-6 py-6 border-t border-gray-200") do
|
||||
h3(class: "text-lg font-medium text-gray-900 mb-6") { "API Configuration" }
|
||||
|
||||
div(class: "space-y-4") do
|
||||
h3(class: "text-lg font-medium text-gray-900 mb-6") { "TBDB Integration" }
|
||||
|
||||
div(class: "space-y-6") do
|
||||
# OAuth Connection Status
|
||||
div do
|
||||
dt(class: "text-sm font-medium text-gray-700 mb-2") { "TheBookDB API Token" }
|
||||
dd(class: "text-xs text-gray-500 mb-3") { "Personal API token for accessing TheBookDB.info service. Falls back to application default if not set." }
|
||||
|
||||
if @user.has_thebookdb_api_token?
|
||||
div(class: "text-sm text-gray-900 font-mono bg-gray-50 px-3 py-2 rounded border") do
|
||||
token = @user.thebookdb_api_token
|
||||
masked_token = token[0..7] + "..." + token[-4..-1]
|
||||
masked_token
|
||||
dt(class: "text-sm font-medium text-gray-700 mb-2") { "OAuth Connection" }
|
||||
dd(class: "text-xs text-gray-500 mb-3") { "Secure OAuth connection to TheBookDB.info for enhanced product data access." }
|
||||
|
||||
if @user.has_oauth_connection?
|
||||
div(class: "flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded") do
|
||||
div do
|
||||
div(class: "text-sm font-medium text-green-800") { "Connected to TBDB" }
|
||||
if @user.oauth_token_expired?
|
||||
div(class: "text-xs text-amber-600 mt-1") { "Token expired - will refresh automatically" }
|
||||
else
|
||||
div(class: "text-xs text-green-600 mt-1") { "Active connection" }
|
||||
end
|
||||
end
|
||||
a(
|
||||
href: auth_tbdb_disconnect_path,
|
||||
data: {
|
||||
turbo_method: "delete",
|
||||
turbo_confirm: "Are you sure you want to disconnect from TBDB?"
|
||||
},
|
||||
class: "text-sm text-red-600 hover:text-red-700 font-medium"
|
||||
) { "Disconnect" }
|
||||
end
|
||||
div(class: "text-xs text-green-600 mt-1") { "Using personal token" }
|
||||
else
|
||||
if ENV["TBDB_API_TOKEN"].present?
|
||||
div(class: "text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded border") do
|
||||
"Using application default"
|
||||
end
|
||||
else
|
||||
div(class: "text-sm text-red-600 bg-red-50 px-3 py-2 rounded border border-red-200") do
|
||||
"No token configured"
|
||||
div(class: "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded") do
|
||||
div do
|
||||
div(class: "text-sm font-medium text-gray-700") { "Not Connected" }
|
||||
div(class: "text-xs text-gray-500 mt-1") { "Connect for seamless API access" }
|
||||
end
|
||||
a(
|
||||
href: auth_tbdb_path,
|
||||
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
) { "Connect to TBDB" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class LibrariesController < ApplicationController
|
||||
private
|
||||
|
||||
def library_params
|
||||
params.require(:library).permit(:name, :description, :bulk_barcodes)
|
||||
params.expect(library: [:name, :description, :bulk_barcodes])
|
||||
end
|
||||
|
||||
def process_bulk_barcodes(library, bulk_barcodes_text)
|
||||
@@ -114,8 +114,7 @@ class LibrariesController < ApplicationController
|
||||
# Create library item
|
||||
LibraryItem.create!(
|
||||
library: library,
|
||||
product: product,
|
||||
user: Current.user
|
||||
product: product
|
||||
)
|
||||
|
||||
# Create scan record for the user
|
||||
@@ -141,14 +140,6 @@ class LibrariesController < ApplicationController
|
||||
end
|
||||
|
||||
def find_or_create_product(gtin)
|
||||
product = Product.find_by(gtin: gtin)
|
||||
return product if product
|
||||
|
||||
# Create new product with minimal data
|
||||
Product.create!(
|
||||
gtin: gtin,
|
||||
title: "Unknown Product (#{gtin})",
|
||||
product_type: 'other'
|
||||
)
|
||||
Product.findd(gtin, title: "Unknown Product (#{gtin})", product_type: 'other')
|
||||
end
|
||||
end
|
||||
|
||||
69
app/controllers/oauth_controller.rb
Normal file
69
app/controllers/oauth_controller.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
class OauthController < ApplicationController
|
||||
before_action :require_authentication
|
||||
|
||||
def tbdb
|
||||
oauth_service = TbdbOauthService.new(Current.user)
|
||||
|
||||
begin
|
||||
authorization_url = oauth_service.authorization_url
|
||||
redirect_to authorization_url, allow_other_host: true
|
||||
rescue TbdbOauthService::OAuthError => e
|
||||
Rails.logger.error "OAuth initiation failed: #{e.message}"
|
||||
redirect_to profile_path, alert: "Failed to connect to TBDB: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def tbdb_callback
|
||||
code = params[:code]
|
||||
state = params[:state]
|
||||
error = params[:error]
|
||||
error_hint = params[:error_hint]
|
||||
|
||||
if error.present?
|
||||
# Handle case where OAuth client is invalid/not found on TBDB
|
||||
if error == "invalid_client" && error_hint == "client_not_found"
|
||||
Rails.logger.info "OAuth client not found on TBDB, clearing credentials and re-registering"
|
||||
|
||||
oauth_service = TbdbOauthService.new(Current.user)
|
||||
|
||||
begin
|
||||
# Clear the invalid credentials
|
||||
oauth_service.clear_client_credentials
|
||||
|
||||
# Redirect back to initiate OAuth flow, which will re-register
|
||||
redirect_to auth_tbdb_path, notice: "Re-registering with TBDB..."
|
||||
return
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to handle client re-registration: #{e.message}"
|
||||
redirect_to profile_path, alert: "OAuth client not found. Please try connecting again."
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
Rails.logger.error "OAuth callback error: #{error} (hint: #{error_hint})"
|
||||
redirect_to profile_path, alert: "TBDB authorization failed: #{params[:error_description] || error}"
|
||||
return
|
||||
end
|
||||
|
||||
if code.blank?
|
||||
redirect_to profile_path, alert: "No authorization code received from TBDB"
|
||||
return
|
||||
end
|
||||
|
||||
oauth_service = TbdbOauthService.new(Current.user)
|
||||
|
||||
begin
|
||||
oauth_service.exchange_code_for_token(code, state)
|
||||
redirect_to profile_path, notice: "Successfully connected to TBDB!"
|
||||
rescue TbdbOauthService::OAuthError => e
|
||||
Rails.logger.error "OAuth token exchange failed: #{e.message}"
|
||||
redirect_to profile_path, alert: "Failed to complete TBDB connection: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def tbdb_disconnect
|
||||
oauth_service = TbdbOauthService.new(Current.user)
|
||||
oauth_service.revoke_tokens
|
||||
redirect_to profile_path, notice: "Disconnected from TBDB"
|
||||
end
|
||||
end
|
||||
@@ -35,4 +35,29 @@ class User < ApplicationRecord
|
||||
def effective_thebookdb_api_token
|
||||
has_thebookdb_api_token? ? thebookdb_api_token : ENV["TBDB_API_TOKEN"]
|
||||
end
|
||||
|
||||
# OAuth management
|
||||
def has_oauth_connection?
|
||||
oauth_client_id.present? && oauth_access_token.present?
|
||||
end
|
||||
|
||||
def oauth_token_expired?
|
||||
oauth_expires_at.nil? || oauth_expires_at <= Time.current
|
||||
end
|
||||
|
||||
def oauth_token_valid?
|
||||
has_oauth_connection? && !oauth_token_expired?
|
||||
end
|
||||
|
||||
def effective_tbdb_token
|
||||
oauth_token_valid? ? oauth_access_token : effective_thebookdb_api_token
|
||||
end
|
||||
|
||||
def clear_oauth_connection
|
||||
update!(
|
||||
oauth_access_token: nil,
|
||||
oauth_refresh_token: nil,
|
||||
oauth_expires_at: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ class ProductEnrichmentService
|
||||
# Update basic product info if missing or improve existing
|
||||
attributes[:title] = tbdb_data["title"] if tbdb_data["title"].present? && (product.title.blank? || product.title.start_with?("Unknown "))
|
||||
attributes[:subtitle] = tbdb_data["subtitle"] if tbdb_data["subtitle"].present?
|
||||
attributes[:author] = tbdb_data["authors"]&.first&.dig("name") if product.author.blank?
|
||||
attributes[:author] = tbdb_data["author"] if product.author.blank?
|
||||
attributes[:publisher] = tbdb_data["publisher"] if product.publisher.blank?
|
||||
attributes[:description] = tbdb_data["description"] if product.description.blank?
|
||||
attributes[:pages] = tbdb_data["pages"] if product.pages.blank?
|
||||
|
||||
@@ -28,14 +28,23 @@ module ShelfLife
|
||||
|
||||
def get_or_create_client(token, user)
|
||||
# Create a cache key based on user ID or 'system' for ENV token
|
||||
# Include OAuth status in cache key to avoid conflicts
|
||||
cache_key = if user&.id
|
||||
"tbdb_client:#{user.id}"
|
||||
oauth_status = user.has_oauth_connection? ? "oauth" : "api"
|
||||
"tbdb_client:#{user.id}:#{oauth_status}"
|
||||
else
|
||||
"tbdb_client:system"
|
||||
end
|
||||
|
||||
Rails.cache.fetch(cache_key, expires_in: 25.minutes) do
|
||||
Tbdb::Client.new(api_token: token)
|
||||
if user&.has_oauth_connection?
|
||||
Tbdb::Client.new(
|
||||
oauth_token: user.oauth_access_token,
|
||||
user: user
|
||||
)
|
||||
else
|
||||
Tbdb::Client.new(api_token: token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -13,16 +13,21 @@ module Tbdb
|
||||
# Use production TBDB API by default (will move to api.tbdb.info soon)
|
||||
DEFAULT_BASE_URI = ENV.fetch("TBDB_API_URI", "https://api.thebookdb.info").freeze
|
||||
|
||||
attr_reader :api_token, :jwt_token, :jwt_expires_at, :base_uri, :last_request_time
|
||||
attr_reader :api_token, :oauth_token, :user, :jwt_token, :jwt_expires_at, :base_uri, :last_request_time
|
||||
|
||||
def initialize(api_token: ENV["TBDB_API_TOKEN"], base_uri: DEFAULT_BASE_URI)
|
||||
def initialize(api_token: nil, oauth_token: nil, user: nil, base_uri: DEFAULT_BASE_URI)
|
||||
@api_token = api_token
|
||||
@oauth_token = oauth_token
|
||||
@user = user
|
||||
@base_uri = URI(base_uri)
|
||||
@jwt_token = nil
|
||||
@jwt_expires_at = nil
|
||||
@last_request_time = nil
|
||||
|
||||
validate_api_token!
|
||||
# Determine the token to use
|
||||
@effective_token = determine_effective_token
|
||||
|
||||
validate_token!
|
||||
ensure_valid_jwt
|
||||
end
|
||||
|
||||
@@ -54,9 +59,36 @@ module Tbdb
|
||||
|
||||
private
|
||||
|
||||
def validate_api_token!
|
||||
if @api_token.nil? || @api_token.empty?
|
||||
raise ArgumentError, "TBDB_API_TOKEN environment variable is required"
|
||||
def determine_effective_token
|
||||
# Priority: OAuth token > API token > ENV token
|
||||
if @oauth_token.present?
|
||||
refresh_oauth_token_if_needed
|
||||
@oauth_token
|
||||
elsif @api_token.present?
|
||||
@api_token
|
||||
else
|
||||
ENV["TBDB_API_TOKEN"]
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_oauth_token_if_needed
|
||||
return unless @user&.oauth_token_expired?
|
||||
|
||||
Rails.logger.debug "OAuth token expired, attempting refresh..."
|
||||
oauth_service = TbdbOauthService.new(@user)
|
||||
|
||||
if oauth_service.refresh_access_token
|
||||
@oauth_token = @user.oauth_access_token
|
||||
Rails.logger.debug "OAuth token refreshed successfully"
|
||||
else
|
||||
Rails.logger.warn "OAuth token refresh failed, falling back to API token"
|
||||
@oauth_token = nil
|
||||
end
|
||||
end
|
||||
|
||||
def validate_token!
|
||||
if @effective_token.nil? || @effective_token.empty?
|
||||
raise ArgumentError, "No valid token available (OAuth, API, or ENV)"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +109,7 @@ module Tbdb
|
||||
uri = URI.join(@base_uri.to_s.chomp("/") + "/", "api/tokens/exchange")
|
||||
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
request["Authorization"] = "Bearer #{@api_token}"
|
||||
request["Authorization"] = "Bearer #{@effective_token}"
|
||||
request["Content-Type"] = "application/json"
|
||||
request["Accept"] = "application/json"
|
||||
request["User-Agent"] = user_agent
|
||||
|
||||
@@ -62,6 +62,11 @@ Rails.application.routes.draw do
|
||||
get "/profile/change_password", to: "user#change_password", as: :change_password
|
||||
patch "/profile/update_password", to: "user#update_password"
|
||||
|
||||
# OAuth routes
|
||||
get "/auth/tbdb", to: "oauth#tbdb", as: :auth_tbdb
|
||||
get "/auth/tbdb/callback", to: "oauth#tbdb_callback", as: :auth_tbdb_callback
|
||||
delete "/auth/tbdb/disconnect", to: "oauth#tbdb_disconnect", as: :auth_tbdb_disconnect
|
||||
|
||||
# API routes
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
|
||||
@@ -7,6 +7,12 @@ class CreateUsers < ActiveRecord::Migration[8.0]
|
||||
t.boolean :admin, default: false, null: false
|
||||
t.json :user_settings, default: {}
|
||||
|
||||
# OAuth fields for TBDB integration
|
||||
t.string :oauth_client_id
|
||||
t.string :oauth_client_secret
|
||||
t.string :oauth_access_token
|
||||
t.string :oauth_refresh_token
|
||||
t.datetime :oauth_expires_at
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ class CreateScans < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :scans do |t|
|
||||
t.references :product, null: false, foreign_key: true
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.datetime :scanned_at, null: false
|
||||
|
||||
t.timestamps
|
||||
@@ -1,5 +0,0 @@
|
||||
class AddUserToScans < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_reference :scans, :user, null: false, foreign_key: true
|
||||
end
|
||||
end
|
||||
5
db/schema.rb
generated
5
db/schema.rb
generated
@@ -164,6 +164,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_01_054402) do
|
||||
t.string "name"
|
||||
t.boolean "admin", default: false, null: false
|
||||
t.json "user_settings", default: {}
|
||||
t.string "oauth_client_id"
|
||||
t.string "oauth_client_secret"
|
||||
t.string "oauth_access_token"
|
||||
t.string "oauth_refresh_token"
|
||||
t.datetime "oauth_expires_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["email_address"], name: "index_users_on_email_address", unique: true
|
||||
|
||||
Reference in New Issue
Block a user