diff --git a/.env.example b/.env.example index 2c113aa380..69edf47282 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,9 @@ SENDGRID_USERNAME='apikey' SENDGRID_PASSWORD= SMTP_DOMAIN='saeloun.com' SMTP_PORT=587 + +# AWS S3 Credentials +AWS_ACCESS_KEY_ID: "" +AWS_SECRET_ACCESS_ID: "" +AWS_S3_BUCKET_NAME: "" +AWS_REGION: "" diff --git a/Gemfile b/Gemfile index ee0f4ca20b..c2551d8347 100644 --- a/Gemfile +++ b/Gemfile @@ -78,6 +78,9 @@ gem "letter_opener_web" # currency list and conversion gem "money" +# aws storage account +gem "aws-sdk-s3", require: false + group :development, :test do # See https://edgeguides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", ">= 1.0.0", platforms: %i[mri mingw x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index d907db0849..6379447b32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,6 +84,22 @@ GEM activerecord (>= 2.3.0) rake (>= 0.8.7) ast (2.4.2) + aws-eventstream (1.2.0) + aws-partitions (1.547.0) + aws-sdk-core (3.125.2) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.53.0) + aws-sdk-core (~> 3, >= 3.125.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.111.1) + aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.4.0) + aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) @@ -143,6 +159,7 @@ GEM jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) + jmespath (1.5.0) launchy (2.5.0) addressable (~> 2.7) letter_opener (1.7.0) @@ -355,6 +372,7 @@ PLATFORMS DEPENDENCIES annotate + aws-sdk-s3 bootsnap (>= 1.4.4) capybara (>= 3.26) countries diff --git a/public/avtar.svg b/app/assets/images/avatar.svg similarity index 100% rename from public/avtar.svg rename to app/assets/images/avatar.svg diff --git a/app/assets/images/cancel_button.svg b/app/assets/images/cancel_button.svg new file mode 100644 index 0000000000..13f38a362a --- /dev/null +++ b/app/assets/images/cancel_button.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/delete_image_button.svg b/app/assets/images/delete_image_button.svg new file mode 100644 index 0000000000..d4d3f68b55 --- /dev/null +++ b/app/assets/images/delete_image_button.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/assets/images/edit_image_button.svg b/app/assets/images/edit_image_button.svg new file mode 100644 index 0000000000..618a175d37 --- /dev/null +++ b/app/assets/images/edit_image_button.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/logout_icon.svg b/app/assets/images/logout_icon.svg new file mode 100644 index 0000000000..5e6ea61ae6 --- /dev/null +++ b/app/assets/images/logout_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/plus_icon.svg b/app/assets/images/plus_icon.svg new file mode 100644 index 0000000000..15859ba1a4 --- /dev/null +++ b/app/assets/images/plus_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/save_button.svg b/app/assets/images/save_button.svg new file mode 100644 index 0000000000..544a83b039 --- /dev/null +++ b/app/assets/images/save_button.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/setting_icon.svg b/app/assets/images/setting_icon.svg new file mode 100644 index 0000000000..305ab3d57d --- /dev/null +++ b/app/assets/images/setting_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 066bb6cb1a..ad37c7a22d 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -3,8 +3,27 @@ class Users::RegistrationsController < Devise::RegistrationsController before_action :configure_permitted_parameters + def purge_avatar + user = User.find(params[:id]) + user.avatar.destroy + redirect_to profile_path + end + protected def configure_permitted_parameters - devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :email, :password, :password_confirmation]) + devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :email, :password, :password_confirmation, :avatar]) + devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name, :email, :password, :password_confirmation, :avatar]) + end + + def update_resource(resource, params) + if params[:current_password].blank? + resource.update_without_password(params.except(:current_password)) + else + resource.update_with_password(params) + end + end + + def after_update_path_for(resource) + profile_path end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 15b06f0f67..8777218e2d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,11 @@ # frozen_string_literal: true module ApplicationHelper + def user_avatar(user) + if user.avatar.attached? + user.avatar + else + image_url "avatar.svg" + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index ea5cf651dc..ceef3dd7b0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -51,6 +51,8 @@ class User < ApplicationRecord after_create :assign_default_role + has_one_attached :avatar + private def assign_default_role self.add_role(:owner) if self.roles.blank? diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 38d95b85a8..833d27e327 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -1,43 +1,174 @@ -

Edit <%= resource_name.to_s.humanize %>

+<%= render "partial/navbar" %> -<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> - <%= render "devise/shared/error_messages", resource: resource %> +
+
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> -
- <%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> -
+
+
+

+ Settings +

+
+
- <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> -
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
- <% end %> +
+ + <%= link_to "CANCEL", :back, class: "text-base font-sans font-medium text-miru-han-purple-1000 bg-transparent hover:text-miru-han-purple-600 cursor-pointer" %> +
-
- <%= f.label :password %> (leave blank if you don't want to change it)
- <%= f.password_field :password, autocomplete: "new-password" %> - <% if @minimum_password_length %> -
- <%= @minimum_password_length %> characters minimum - <% end %> -
+
+ + <%= f.submit "SAVE", class: "text-base font-sans font-medium text-miru-white-1000 bg-transparent cursor-pointer" %> +
+
+
+
+
+

text-xs text-miru-han-purple-1000 hover:text-miru-han-purple-600 tracking-widest leading-7 sm:text-base sm:truncate py-1 cursor-pointer"> + <%= link_to "PROFILE SETTINGS", "/profile" %> +

+
+
-
- <%= f.label :password_confirmation %>
- <%= f.password_field :password_confirmation, autocomplete: "new-password" %> -
+
+
+ <%= render "devise/shared/error_messages", resource: resource %> -
- <%= f.label :current_password %> (we need your current password to confirm your changes)
- <%= f.password_field :current_password, autocomplete: "current-password" %> -
+
+ +
+ <% if resource.avatar.attached? %> +
+
+
+ <%= image_tag resource.avatar, class: "rounded-full" %> +
+
+ <%= link_to image_tag("#{image_url 'delete_image_button.svg'}"), profile_purge_avatar_path(id: resource), method: :delete, class: "font-medium text-lg leading-6 space-y-1 hover:border-2 hover:border-miru-han-purple-1000"%> +
+ <% else %> +
+
+
+ +
+
+
+ <% end %> +
+
-
- <%= f.submit "Update" %> -
-<% end %> +
+
+ +
+
+ <%= f.text_field :first_name, autofocus: true, class: "rounded tracking-wider appearance-none border border-gray-100 block w-full px-3 py-2 bg-miru-white-100 h-8 shadow-sm font-semibold text-xs text-miru-dark-purple-1000 focus:outline-none focus:ring-miru-gray-1000 focus:border-miru-gray-1000 sm:text-base" %> +
+
+ <%= f.text_field :last_name, autofocus: true, class: "rounded tracking-wider appearance-none border border-gray-100 block w-full px-3 py-2 bg-miru-white-100 h-8 shadow-sm font-semibold text-xs text-miru-dark-purple-1000 focus:outline-none focus:ring-miru-gray-1000 focus:border-miru-gray-1000 sm:text-base" %> +
+
+
+
-

Cancel my account

+
+
+ <%= f.label :email, class: "tracking-wider block text-sm font-semibold text-miru-dark-purple-1000" %> +
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", readonly: true, class: "rounded tracking-wider appearance-none border border-gray-100 block w-full px-3 py-2 bg-miru-dark-purple-100 h-8 shadow-sm font-semibold text-xs text-miru-dark-purple-400 focus:outline-none focus:ring-miru-gray-1000 focus:border-miru-gray-1000 sm:text-base" %> +
+
+
-

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

+ <%# if devise_mapping.confirmable? && resource.pending_reconfirmation? %> + + <%# end %> + +
+
+
+ CHANGE PASSWORD +
+
+
+
+
+
+ <%= f.label :current_password, class: "tracking-wider block text-sm font-semibold text-miru-dark-purple-1000" %> +
+ +
+ + +
+
+
+
+ +
+
+ <%= f.label :password, class: "tracking-wider block text-sm font-semibold text-miru-dark-purple-1000" %> + <% if @minimum_password_length %> + ( <%= @minimum_password_length %> characters minimum ) + <% end %> +
+ +
+ + +
+
+
+
+ +
+
+ <%= f.label :password_confirmation, class: "tracking-wider block text-sm font-semibold text-miru-dark-purple-1000" %> +
+ +
+ + +
+
+
+
+
+
+ CANCEL +
+
+
+ + <% end %> + + +
+
+
+
-<%= link_to "Back", :back %> + diff --git a/app/views/partial/_navbar.html.erb b/app/views/partial/_navbar.html.erb index 092a3aa414..60063cc175 100644 --- a/app/views/partial/_navbar.html.erb +++ b/app/views/partial/_navbar.html.erb @@ -5,24 +5,12 @@ -
-
diff --git a/config/environments/production.rb b/config/environments/production.rb index 152eaa6bda..2c8df3c3f2 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -37,7 +37,7 @@ # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local + config.active_storage.service = :amazon # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil diff --git a/config/routes.rb b/config/routes.rb index b0758ccdfe..9bfbf8708b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,11 @@ resources :company, only: [:new, :create] resources :time_tracking, only: [:index], path: "time-tracking" + devise_scope :user do + get "profile", to: "users/registrations#edit" + delete "profile/purge_avatar", to: "users/registrations#purge_avatar" + end + # For opening the email in the web browser in non production environments if ENV["EMAIL_DELIVERY_METHOD"] == "letter_opener_web" mount LetterOpenerWeb::Engine, at: "/sent_emails" diff --git a/config/storage.yml b/config/storage.yml index 4942ab6694..399b42093e 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -7,12 +7,12 @@ local: root: <%= Rails.root.join("storage") %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) -# amazon: -# service: S3 -# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> -# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> -# region: us-east-1 -# bucket: your_own_bucket-<%= Rails.env %> +amazon: + service: S3 + access_key_id: ENV['AWS_ACCESS_KEY_ID'] + secret_access_key: ENV['AWS_SECRET_ACCESS_ID'] + region: ENV['AWS_REGION'] + bucket: ENV['AWS_S3_BUCKET_NAME']-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: diff --git a/db/migrate/20220112072044_create_active_storage_tables.active_storage.rb b/db/migrate/20220112072044_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000000..dfb4fc54c9 --- /dev/null +++ b/db/migrate/20220112072044_create_active_storage_tables.active_storage.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index c5c365e8e0..1f6f5457d9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -14,6 +14,34 @@ # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", precision: 6, null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", precision: 6, null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + create_table "clients", force: :cascade do |t| t.bigint "company_id", null: false t.string "name", null: false @@ -108,6 +136,8 @@ t.index ["user_id"], name: "index_users_roles_on_user_id" end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "clients", "companies" add_foreign_key "projects", "clients" add_foreign_key "timesheet_entries", "projects" diff --git a/public/avatar.svg b/public/avatar.svg new file mode 100644 index 0000000000..31a20ac171 --- /dev/null +++ b/public/avatar.svg @@ -0,0 +1,4 @@ + + + +