diff --git a/.env.example b/.env.example index bcc1cda4d36..55daaea2f22 100644 --- a/.env.example +++ b/.env.example @@ -59,3 +59,33 @@ UPGRADES_TARGET=release # `release` or `commit` GITHUB_REPO_OWNER=maybe-finance GITHUB_REPO_NAME=maybe GITHUB_REPO_BRANCH=main + +# ====================================================================================================== +# Active Storage Configuration - responsible for storing file uploads +# ====================================================================================================== +# +# * Defaults to disk storage but you can also use Amazon S3, Google Cloud Storage, or Microsoft Azure Storage. +# * Set the appropriate environment variables to use these services. +# * Ensure libvips is installed on your system for image processing - https://github.com/libvips/libvips +# +# Amazon S3 +# ========== +# ACTIVE_STORAGE_SERVICE=amazon +# S3_ACCESS_KEY_ID= +# S3_SECRET_ACCESS_KEY= +# S3_REGION= # defaults to `us-east-1` if not set +# S3_BUCKET= + +# Google Cloud Storage +# ===================== +# Save your JSON keyfile as `gcp-storage-keyfile.json` in the root of the project +# ACTIVE_STORAGE_SERVICE=google +# GCS_PROJECT= +# GCS_BUCKET= + +# Microsoft Azure Storage +# ======================== +# ACTIVE_STORAGE_SERVICE=azure +# AZURE_STORAGE_ACCOUNT_NAME= +# AZURE_STORAGE_ACCESS_KEY= +# AZURE_STORAGE_CONTAINER= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7e4402d48a0..60c70a2b30c 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ # Ignore .devcontainer files compose-dev.yaml + +# Ignore GCP keyfile +gcp-storage-keyfile.json diff --git a/Gemfile b/Gemfile index 718e01429c4..d52c306a814 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,12 @@ gem "sentry-rails" gem "rails-settings-cached" gem "octokit" +# Active Storage +gem "aws-sdk-s3", require: false +gem "azure-storage-blob", "~> 2.0", require: false +gem "google-cloud-storage", "~> 1.11", require: false +gem "image_processing", ">= 1.2" + # Other gem "bcrypt", "~> 3.1.7" gem "inline_svg" @@ -51,6 +57,7 @@ group :development, :test do gem "letter_opener" gem "i18n-tasks" gem "erb_lint" + gem "byebug" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index e753890fc9c..a2f82e91e35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,6 +107,30 @@ GEM addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) + aws-eventstream (1.3.0) + aws-partitions (1.922.0) + aws-sdk-core (3.193.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.80.0) + aws-sdk-core (~> 3, >= 3.193.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.148.0) + aws-sdk-core (~> 3, >= 3.193.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + azure-storage-blob (2.0.3) + azure-storage-common (~> 2.0) + nokogiri (~> 1, >= 1.10.8) + azure-storage-common (2.0.4) + faraday (~> 1.0) + faraday_middleware (~> 1.0, >= 1.0.0.rc1) + net-http-persistent (~> 4.0) + nokogiri (~> 1, >= 1.10.8) base64 (0.2.0) bcrypt (3.1.20) better_html (2.0.2) @@ -123,6 +147,7 @@ GEM brakeman (6.1.2) racc builder (3.2.4) + byebug (11.1.3) capybara (3.40.0) addressable matrix @@ -143,6 +168,9 @@ GEM debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) dotenv (3.1.0) dotenv-rails (3.1.0) dotenv (= 3.1.0) @@ -158,10 +186,11 @@ GEM erubi (1.12.0) et-orbi (1.2.11) tzinfo - faraday (2.9.0) - faraday-net_http (>= 2.0, < 3.2) - faraday-net_http (3.1.0) - net-http + faraday (1.2.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday_middleware (1.2.0) + faraday (~> 1.0) ffi (1.16.3) fugit (1.11.0) et-orbi (~> 1, >= 1.2.11) @@ -175,12 +204,47 @@ GEM fugit (>= 1.1) railties (>= 6.0.0) thor (>= 0.14.1) + google-apis-core (0.14.1) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.20.0) + google-apis-core (>= 0.14.0, < 2.a) + google-apis-storage_v1 (0.37.0) + google-apis-core (>= 0.14.0, < 2.a) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.1.1) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.4.0) + google-cloud-storage (1.51.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (~> 0.13) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (~> 0.37) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + googleauth (1.11.0) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) hashdiff (1.1.0) highline (3.0.1) hotwire-livereload (1.3.2) actioncable (>= 6.0.0) listen (>= 3.0.0) railties (>= 6.0.0) + httpclient (2.8.3) i18n (1.14.4) concurrent-ruby (~> 1.0) i18n-tasks (1.0.13) @@ -194,6 +258,9 @@ GEM rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) importmap-rails (2.0.1) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -205,7 +272,10 @@ GEM irb (1.12.0) rdoc reline (>= 0.4.2) + jmespath (1.6.2) json (2.7.1) + jwt (2.8.1) + base64 language_server-protocol (3.17.0.3) launchy (3.0.0) addressable (~> 2.8) @@ -225,13 +295,16 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + mini_magick (4.12.0) mini_mime (1.1.5) minitest (5.21.2) mocha (2.2.0) ruby2_keywords (>= 0.0.5) msgpack (1.7.2) - net-http (0.4.1) - uri + multi_json (1.15.0) + multipart-post (2.4.0) + net-http-persistent (4.0.2) + connection_pool (~> 2.2) net-imap (0.4.10) date net-protocol @@ -258,6 +331,7 @@ GEM base64 faraday (>= 1, < 3) sawyer (~> 0.9) + os (1.1.4) pagy (8.3.0) parallel (1.24.0) parser (3.3.0.5) @@ -316,6 +390,11 @@ GEM regexp_parser (2.9.0) reline (0.5.3) io-console (~> 0.5) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) rexml (3.2.6) rubocop (1.60.2) json (~> 2.3) @@ -354,6 +433,8 @@ GEM ruby-lsp (>= 0.16.0, < 0.17.0) sorbet-runtime (>= 0.5.9897) ruby-progressbar (1.13.0) + ruby-vips (2.2.1) + ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) sawyer (0.9.2) @@ -370,6 +451,11 @@ GEM sentry-ruby (5.17.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) smart_properties (1.17.0) sorbet-runtime (0.5.11332) stackprof (0.2.26) @@ -392,14 +478,15 @@ GEM unicode-display_width (>= 1.1.1, < 3) thor (1.3.1) timeout (0.4.1) + trailblazer-option (0.1.2) turbo-rails (2.0.5) actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + uber (0.1.0) unicode-display_width (2.5.0) - uri (0.13.0) useragent (0.16.10) vcr (6.2.0) web-console (4.2.1) @@ -429,17 +516,22 @@ PLATFORMS x86_64-linux DEPENDENCIES + aws-sdk-s3 + azure-storage-blob (~> 2.0) bcrypt (~> 3.1.7) bootsnap brakeman + byebug capybara debug dotenv-rails erb_lint faraday good_job + google-cloud-storage (~> 1.11) hotwire-livereload i18n-tasks + image_processing (>= 1.2) importmap-rails inline_svg letter_opener diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 58c6f678b90..83aa94dab53 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -5,6 +5,10 @@ def show def update user_params_with_family = user_params + if params[:user][:delete_profile_image] == "true" + Current.user.profile_image.purge + end + if Current.family && user_params_with_family[:family_attributes] family_attributes = user_params_with_family[:family_attributes].merge({ id: Current.family.id }) user_params_with_family[:family_attributes] = family_attributes @@ -13,7 +17,7 @@ def update if Current.user.update(user_params_with_family) redirect_to settings_profile_path, notice: t(".success") else - render :edit, status: :unprocessable_entity + redirect_to settings_profile_path, alert: t(".file_size_error") end end @@ -29,7 +33,7 @@ def destroy private def user_params - params.require(:user).permit(:first_name, :last_name, + params.require(:user).permit(:first_name, :last_name, :profile_image, family_attributes: [ :name, :id ]) end end diff --git a/app/javascript/controllers/profile_image_preview_controller.js b/app/javascript/controllers/profile_image_preview_controller.js new file mode 100644 index 00000000000..6118634b078 --- /dev/null +++ b/app/javascript/controllers/profile_image_preview_controller.js @@ -0,0 +1,27 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["imagePreview", "fileField", "deleteField", "clearBtn", "template"] + + preview(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + this.imagePreviewTarget.innerHTML = `Preview`; + this.templateTarget.classList.add("hidden"); + this.clearBtnTarget.classList.remove("hidden"); + }; + reader.readAsDataURL(file); + } + } + + clear() { + this.deleteFieldTarget.value = true; + this.fileFieldTarget.value = null; + this.templateTarget.classList.remove("hidden"); + this.imagePreviewTarget.innerHTML = this.templateTarget.innerHTML; + this.clearBtnTarget.classList.add("hidden"); + this.element.submit(); + } +} diff --git a/app/models/user.rb b/app/models/user.rb index b6f2ffb3810..0d553904430 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,6 +11,12 @@ class User < ApplicationRecord enum :role, { member: "member", admin: "admin" }, validate: true + has_one_attached :profile_image do |attachable| + attachable.variant :thumbnail, resize_to_limit: [ 150, 150 ], preprocessed: true + end + + validate :profile_image_size + generates_token_for :password_reset, expires_in: 15.minutes do password_salt&.last(10) end @@ -74,4 +80,10 @@ def last_user_in_family? def deactivated_email email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@") end + + def profile_image_size + if profile_image.attached? && profile_image.byte_size > 5.megabytes + errors.add(:profile_image, "is too large. Maximum size is 5 MB.") + end + end end diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb index 43b0c34263a..3afab0f4211 100644 --- a/app/views/layouts/_sidebar.html.erb +++ b/app/views/layouts/_sidebar.html.erb @@ -4,11 +4,24 @@ <% end %>