diff --git a/lib/admin_auth.rb b/lib/admin_auth.rb new file mode 100644 index 0000000..360fd7f --- /dev/null +++ b/lib/admin_auth.rb @@ -0,0 +1,24 @@ +require 'rack/auth/basic' + +# Rack middleware that gates everything under /admin/* behind HTTP Basic. +# Credentials come from ENV at boot; missing vars fail closed (401 always). +class AdminAuth + def initialize(app) + @app = app + @user = ENV['ADMIN_USER'] + @pass = ENV['ADMIN_PASSWORD'] + end + + def call(env) + return @app.call(env) unless env['PATH_INFO'].to_s.start_with?('/admin') + + auth = Rack::Auth::Basic::Request.new(env) + if @user && @pass && auth.provided? && auth.basic? && auth.credentials == [@user, @pass] + @app.call(env) + else + [401, + { 'content-type' => 'text/plain', 'www-authenticate' => 'Basic realm="asxp.io admin"' }, + ["Unauthorized\n"]] + end + end +end diff --git a/lib/s3.rb b/lib/s3.rb new file mode 100644 index 0000000..8647c24 --- /dev/null +++ b/lib/s3.rb @@ -0,0 +1,50 @@ +require 'aws-sdk-s3' + +module S3 + module_function + + def client + @client ||= Aws::S3::Client.new( + region: ENV.fetch('S3_REGION', 'us-east-1'), + endpoint: ENV.fetch('S3_ENDPOINT'), + access_key_id: ENV.fetch('S3_ACCESS_KEY'), + secret_access_key: ENV.fetch('S3_SECRET_KEY'), + force_path_style: true + ) + end + + # Separate client whose endpoint is the public hostname; used only to mint + # presigned URLs that a browser can dereference. Bytes never travel through + # this client. + def presign_client + @presign_client ||= Aws::S3::Client.new( + region: ENV.fetch('S3_REGION', 'us-east-1'), + endpoint: ENV.fetch('S3_PUBLIC_ENDPOINT'), + access_key_id: ENV.fetch('S3_ACCESS_KEY'), + secret_access_key: ENV.fetch('S3_SECRET_KEY'), + force_path_style: true + ) + end + + def bucket + ENV.fetch('S3_BUCKET') + end + + def put(key, bytes, content_type: 'application/pdf') + client.put_object( + bucket: bucket, + key: key, + body: bytes, + content_type: content_type + ) + key + end + + def presigned_url(key, expires_in: 300, filename: nil) + params = { bucket: bucket, key: key } + if filename + params[:response_content_disposition] = %(attachment; filename="#{filename}") + end + Aws::S3::Presigner.new(client: presign_client).presigned_url(:get_object, **params, expires_in: expires_in) + end +end