diff --git a/app/controllers/asset_sync_controller.rb b/app/controllers/asset_sync_controller.rb new file mode 100644 index 000000000..75b9a7ad9 --- /dev/null +++ b/app/controllers/asset_sync_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class AssetSyncController < ApplicationController + skip_before_action :authenticate_user!, only: :update + skip_before_action :verify_authenticity_token, only: :update + before_action :authenticate_asset_sync_token!, only: :update + + def show + asset = Asset.find_by(params[:asset_id]) + + head :forbidden unless asset && can_manage_asset?(asset) + + asset_sync_token = current_user.asset_sync_tokens.find_or_create_by(asset_id: params[:asset_id]) + + unless asset_sync_token.token_valid? + asset_sync_token = current_user.asset_sync_tokens.create(asset_id: params[:asset_id]) + end + + render json: AssetSyncTokenSerializer.new(asset_sync_token).as_json + end + + def update + head(:conflict) and return if @asset_sync_token.conflicts?(request.headers['VersionToken']) + + @asset.file.attach(io: request.body, filename: @asset.file.filename) + @asset.touch + + render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json + end + + # private + + def authenticate_asset_sync_token! + @asset_sync_token = AssetSyncToken.find_by(token: request.headers['Authentication']) + + head(:unauthorized) and return unless @asset_sync_token&.token_valid? + + @asset = @asset_sync_token.asset + @current_user = @asset_sync_token.user + + head :forbidden unless can_manage_asset?(@asset) + end +end diff --git a/app/models/asset.rb b/app/models/asset.rb index 64a9e1235..928710fd2 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -44,6 +44,7 @@ class Asset < ApplicationRecord dependent: :nullify has_many :report_elements, inverse_of: :asset, dependent: :destroy has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy + has_many :asset_sync_tokens, dependent: :destroy scope :sort_assets, lambda { |sort_value = 'new'| sort = case sort_value diff --git a/app/models/asset_sync_token.rb b/app/models/asset_sync_token.rb new file mode 100644 index 000000000..8c1224366 --- /dev/null +++ b/app/models/asset_sync_token.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class AssetSyncToken < ApplicationRecord + belongs_to :user + belongs_to :asset + + after_initialize :generate_token + after_initialize :set_default_expiration + + validates :token, uniqueness: true, presence: true + + def version_token + asset.updated_at.to_i.to_s + end + + def token_valid? + !revoked_at? && expires_at > Time.current + end + + def conflicts?(token) + version_token != token + end + + private + + def generate_token + self.token ||= SecureRandom.urlsafe_base64(32) + end + + def set_default_expiration + self.expires_at ||= Constants::ASSET_SYNC_TOKEN_EXPIRATION.from_now + end +end diff --git a/app/models/user.rb b/app/models/user.rb index d659da492..3458807e2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -318,6 +318,7 @@ class User < ApplicationRecord has_many :access_tokens, class_name: 'Doorkeeper::AccessToken', foreign_key: :resource_owner_id, dependent: :delete_all + has_many :asset_sync_tokens, dependent: :destroy has_many :hidden_repository_cell_reminders, dependent: :destroy diff --git a/app/serializers/asset_sync_token_serializer.rb b/app/serializers/asset_sync_token_serializer.rb new file mode 100644 index 000000000..cccf23a36 --- /dev/null +++ b/app/serializers/asset_sync_token_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AssetSyncTokenSerializer < ActiveModel::Serializer + include Rails.application.routes.url_helpers + + attributes :url, :asset_id, :filename, :token, :asset_id, :version_token, :checksum + + def checksum + object.asset.file.checksum + end + + def url + object.asset.file.url + end + + def filename + object.asset.file.filename + end +end diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index b1b035f19..246b48430 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -416,6 +416,8 @@ class Constants FAST_STATUS_POLLING_INTERVAL = 5000 SLOW_STATUS_POLLING_INTERVAL = 10000 + ASSET_SYNC_TOKEN_EXPIRATION = 1.year + # ) \ / ( # /|\ )\_/( /|\ # * / | \ (/\|/\) / | \ * diff --git a/config/routes.rb b/config/routes.rb index 0aec0fcc9..d97a6e320 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1002,6 +1002,9 @@ Rails.application.routes.draw do end end + get 'asset_sync/:asset_id', to: 'asset_sync#show' + put 'asset_sync', to: 'asset_sync#update' + post 'global_activities', to: 'global_activities#index' constraints WopiSubdomain do diff --git a/db/migrate/20231006141428_create_asset_sync_tokens.rb b/db/migrate/20231006141428_create_asset_sync_tokens.rb new file mode 100644 index 000000000..f9734cd81 --- /dev/null +++ b/db/migrate/20231006141428_create_asset_sync_tokens.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateAssetSyncTokens < ActiveRecord::Migration[7.0] + def change + create_table :asset_sync_tokens do |t| + t.references :user, null: false, foreign_key: true + t.references :asset, null: false, foreign_key: true + t.string :token, index: { unique: true } + t.timestamp :expires_at + t.timestamp :revoked_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 184a209eb..eac28b41c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_03_114337) do +ActiveRecord::Schema[7.0].define(version: 2023_10_06_141428) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gist" enable_extension "pg_trgm" @@ -76,6 +76,19 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_03_114337) do t.datetime "updated_at", null: false end + create_table "asset_sync_tokens", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "asset_id", null: false + t.string "token" + t.datetime "expires_at", precision: nil + t.datetime "revoked_at", precision: nil + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["asset_id"], name: "index_asset_sync_tokens_on_asset_id" + t.index ["token"], name: "index_asset_sync_tokens_on_token", unique: true + t.index ["user_id"], name: "index_asset_sync_tokens_on_user_id" + end + create_table "asset_text_data", force: :cascade do |t| t.text "data", null: false t.bigint "asset_id", null: false @@ -1309,6 +1322,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_03_114337) do add_foreign_key "activities", "my_modules" add_foreign_key "activities", "projects" add_foreign_key "activities", "users", column: "owner_id" + add_foreign_key "asset_sync_tokens", "assets" + add_foreign_key "asset_sync_tokens", "users" add_foreign_key "asset_text_data", "assets" add_foreign_key "assets", "users", column: "created_by_id" add_foreign_key "assets", "users", column: "last_modified_by_id"