From b1cdee0022fd6f0c3bec34896e96f042c05a1928 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 18 Dec 2024 11:34:18 -0600 Subject: [PATCH 01/10] Add user profile screen --- app/controllers/users_controller.rb | 12 +++++++-- app/views/accounts/users/_user.html.erb | 2 +- app/views/users/show.html.erb | 35 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 app/views/users/show.html.erb diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c0c9dbc9a..47e240865 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,8 @@ class UsersController < ApplicationController - require_unauthenticated_access + require_unauthenticated_access only: %i[ new create ] - before_action :set_account_from_join_code + before_action :set_user, only: :show + before_action :set_account_from_join_code, only: %i[ new create ] def new @user = @account.users.build @@ -13,11 +14,18 @@ class UsersController < ApplicationController redirect_to root_path end + def show + end + private def set_account_from_join_code @account = Account.find_by_join_code!(params[:join_code]) end + def set_user + @user = Current.account.users.active.find(params[:id]) + end + def user_params params.expect(user: [ :name, :email_address, :password ]) end diff --git a/app/views/accounts/users/_user.html.erb b/app/views/accounts/users/_user.html.erb index 787d2b37f..2da66fce3 100644 --- a/app/views/accounts/users/_user.html.erb +++ b/app/views/accounts/users/_user.html.erb @@ -2,7 +2,7 @@ <%= avatar_tag user, loading: :lazy, class: "flex-item-no-shrink" %> - <%= user.name %> + <%= link_to user.name, user_path(user) %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb new file mode 100644 index 000000000..eebdb80aa --- /dev/null +++ b/app/views/users/show.html.erb @@ -0,0 +1,35 @@ +<% @page_title = @user.name %> + +<% content_for :header do %> + +<% end %> + +
+
+
+ <%= image_tag user_avatar_path(@user), alt: "Profile avatar for #{@user.name}", class: "avatar", size: 128 %> +
+ <% if !@user.active? %> +
+

<%= @user.name %>

+
<%= mail_to @user.email_address %>
+
+ + <% if Current.user == @user %> +
+ + <%#= render "users/profiles/transfer", user: @user %> + <% end %> + <% else %> +
+

<%= @user.name %>

+
<%= @user.name %> is no longer on this account
+
+ <% end %> +
+
From 3d2f3b805504781eee8a62afa058a1597db9d8ad Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 18 Dec 2024 11:46:07 -0600 Subject: [PATCH 02/10] Remove debug --- app/views/users/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index eebdb80aa..c36fc5a63 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -14,7 +14,7 @@
<%= image_tag user_avatar_path(@user), alt: "Profile avatar for #{@user.name}", class: "avatar", size: 128 %>
- <% if !@user.active? %> + <% if @user.active? %>

<%= @user.name %>

<%= mail_to @user.email_address %>
From eeb269adad3c50af970921ee29cc9f03de123f94 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 18 Dec 2024 12:38:45 -0600 Subject: [PATCH 03/10] Needs display class and modal attribute --- app/assets/stylesheets/utilities.css | 6 ++++++ app/views/accounts/users/_invite.html.erb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 6cf59bfed..761a24b5d 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -251,6 +251,12 @@ } } +/* QR Codes */ +.qr-code { + aspect-ratio: 1; + block-size: 50dvh; +} + /* Accessibility */ .for-screen-reader { block-size: 1px; diff --git a/app/views/accounts/users/_invite.html.erb b/app/views/accounts/users/_invite.html.erb index 48d7b6ba9..f18982dd4 100644 --- a/app/views/accounts/users/_invite.html.erb +++ b/app/views/accounts/users/_invite.html.erb @@ -9,7 +9,7 @@
-
+
<%= tag.button class: "btn", data: { action: "dialog#open" } do %> <%= image_tag "qr-code.svg", aria: { hidden: "true" }, size: 24, class: "colorize--black" %> Show join link QR code From 592c88e63916f281f7b7f684990a415066671a93 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 18 Dec 2024 12:38:53 -0600 Subject: [PATCH 04/10] Add session transfer feature --- app/controllers/qr_codes_controller.rb | 11 ++++++ .../sessions/transfers_controller.rb | 15 ++++++++ app/helpers/forms_helper.rb | 8 ++++ app/helpers/qr_codes_helper.rb | 6 +++ app/models/qr_code_link.rb | 26 +++++++++++++ app/models/user.rb | 2 + app/models/user/transferable.rb | 15 ++++++++ app/views/accounts/users/index.html.erb | 5 --- app/views/sessions/transfers/show.html.erb | 1 + app/views/users/_transfer.html.erb | 37 +++++++++++++++++++ app/views/users/show.html.erb | 37 +++++++++++-------- config/routes.rb | 8 +++- .../sessions/transfers_controller_test.rb | 18 +++++++++ test/models/qr_code_link_test.rb | 11 ++++++ 14 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 app/controllers/qr_codes_controller.rb create mode 100644 app/controllers/sessions/transfers_controller.rb create mode 100644 app/helpers/forms_helper.rb create mode 100644 app/helpers/qr_codes_helper.rb create mode 100644 app/models/qr_code_link.rb create mode 100644 app/models/user/transferable.rb create mode 100644 app/views/sessions/transfers/show.html.erb create mode 100644 app/views/users/_transfer.html.erb create mode 100644 test/controllers/sessions/transfers_controller_test.rb create mode 100644 test/models/qr_code_link_test.rb diff --git a/app/controllers/qr_codes_controller.rb b/app/controllers/qr_codes_controller.rb new file mode 100644 index 000000000..707166930 --- /dev/null +++ b/app/controllers/qr_codes_controller.rb @@ -0,0 +1,11 @@ +class QrCodesController < ApplicationController + allow_unauthenticated_access + + def show + qr_code_link = QrCodeLink.from_signed(params[:id]) + svg = RQRCode::QRCode.new(qr_code_link.url).as_svg(viewbox: true, fill: :white, color: :black) + + expires_in 1.year, public: true + render plain: svg, content_type: "image/svg+xml" + end +end diff --git a/app/controllers/sessions/transfers_controller.rb b/app/controllers/sessions/transfers_controller.rb new file mode 100644 index 000000000..d30c263b6 --- /dev/null +++ b/app/controllers/sessions/transfers_controller.rb @@ -0,0 +1,15 @@ +class Sessions::TransfersController < ApplicationController + require_unauthenticated_access + + def show + end + + def update + if user = User.active.find_by_transfer_id(params[:id]) + start_new_session_for user + redirect_to root_path + else + head :bad_request + end + end +end diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb new file mode 100644 index 000000000..3220d2d78 --- /dev/null +++ b/app/helpers/forms_helper.rb @@ -0,0 +1,8 @@ +module FormsHelper + def auto_submit_form_with(**attributes, &) + data = attributes.delete(:data) || {} + data[:controller] = "auto-submit #{data[:controller]}".strip + + form_with **attributes, data: data, & + end +end diff --git a/app/helpers/qr_codes_helper.rb b/app/helpers/qr_codes_helper.rb new file mode 100644 index 000000000..64bf243a2 --- /dev/null +++ b/app/helpers/qr_codes_helper.rb @@ -0,0 +1,6 @@ +module QrCodesHelper + def qr_code_image(url) + qr_code_link = QrCodeLink.new(url) + image_tag qr_code_path(qr_code_link.signed), class: "qr-code center", alt: "QR Code" + end +end diff --git a/app/models/qr_code_link.rb b/app/models/qr_code_link.rb new file mode 100644 index 000000000..81a65fa25 --- /dev/null +++ b/app/models/qr_code_link.rb @@ -0,0 +1,26 @@ +class QrCodeLink + attr_reader :url + + def initialize(url) + @url = url + end + + def signed + self.class.verifier.generate(@url, purpose: :qr_code) + end + + def self.from_signed(signed) + new verifier.verify(signed, purpose: :qr_code) + end + + private + class << self + def verifier + ActiveSupport::MessageVerifier.new(secret, url_safe: true) + end + + def secret + Rails.application.key_generator.generate_key("qr_codes") + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index bfa562008..02c20ef98 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,6 @@ class User < ApplicationRecord + include Transferable + belongs_to :account has_many :sessions, dependent: :destroy diff --git a/app/models/user/transferable.rb b/app/models/user/transferable.rb new file mode 100644 index 000000000..096913aab --- /dev/null +++ b/app/models/user/transferable.rb @@ -0,0 +1,15 @@ +module User::Transferable + extend ActiveSupport::Concern + + TRANSFER_LINK_EXPIRY_DURATION = 4.hours + + class_methods do + def find_by_transfer_id(id) + find_signed(id, purpose: :transfer) + end + end + + def transfer_id + signed_id(purpose: :transfer, expires_in: TRANSFER_LINK_EXPIRY_DURATION) + end +end diff --git a/app/views/accounts/users/index.html.erb b/app/views/accounts/users/index.html.erb index 5d93666d8..1980efb1f 100644 --- a/app/views/accounts/users/index.html.erb +++ b/app/views/accounts/users/index.html.erb @@ -13,11 +13,6 @@ <%= image_tag "bolt.svg", aria: { hidden: true }, size: 24 %> Workflows <% end %> - - <%= button_to session_path, method: :delete, class: "btn" do %> - <%= image_tag "logout.svg", aria: { hidden: true }, size: 24 %> - Sign out - <% end %> <% end %> diff --git a/app/views/sessions/transfers/show.html.erb b/app/views/sessions/transfers/show.html.erb new file mode 100644 index 000000000..897e39922 --- /dev/null +++ b/app/views/sessions/transfers/show.html.erb @@ -0,0 +1 @@ +<%= auto_submit_form_with method: :put %> diff --git a/app/views/users/_transfer.html.erb b/app/views/users/_transfer.html.erb new file mode 100644 index 000000000..314d8ff39 --- /dev/null +++ b/app/views/users/_transfer.html.erb @@ -0,0 +1,37 @@ +
+ <% url = session_transfer_url(user.transfer_id) %> + + + +
+
+ <%= tag.button class: "btn", data: { action: "dialog#open" } do %> + <%= image_tag "qr-code.svg", aria: { hidden: "true" }, size: 24, class: "colorize--black" %> + Show auto-login QR code + <% end %> + + + <%= qr_code_image(url) %> + +
+ +
+
+
+ + <%= button_to_copy_to_clipboard(url) do %> + <%= image_tag "copy-paste.svg", aria: { hidden: "true" }, size: 24, class: "colorize--black" %> + Copy auto-login link + <% end %> +
+
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index c36fc5a63..9fe91b249 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -14,22 +14,29 @@
<%= image_tag user_avatar_path(@user), alt: "Profile avatar for #{@user.name}", class: "avatar", size: 128 %>
- <% if @user.active? %> -
-

<%= @user.name %>

-
<%= mail_to @user.email_address %>
-
- <% if Current.user == @user %> -
- - <%#= render "users/profiles/transfer", user: @user %> - <% end %> - <% else %> -
-

<%= @user.name %>

-
<%= @user.name %> is no longer on this account
+
+

<%= @user.name %>

+
+ <% if @user.active? %> + <%= mail_to @user.email_address %> + <% else %> + <%= @user.name %> is no longer on this account + <% end %>
- <% end %> +
+ +<% if Current.user == @user %> +
+ <%= render "users/transfer", user: @user %> +
+ +
+ <%= button_to session_path, method: :delete, class: "btn center txt-medium" do %> + <%= image_tag "logout.svg", aria: { hidden: true }, size: 24 %> + Sign out + <% end %> +
+<% end %> diff --git a/config/routes.rb b/config/routes.rb index c11aff6ee..705b43dd6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,7 +38,13 @@ Rails.application.routes.draw do resources :filters resource :first_run - resource :session + resources :qr_codes + + resource :session do + scope module: "sessions" do + resources :transfers, only: %i[ show update ] + end + end resources :users do scope module: :users do diff --git a/test/controllers/sessions/transfers_controller_test.rb b/test/controllers/sessions/transfers_controller_test.rb new file mode 100644 index 000000000..d8cbe9842 --- /dev/null +++ b/test/controllers/sessions/transfers_controller_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +class Sessions::TransfersControllerTest < ActionDispatch::IntegrationTest + test "show renders when not signed in" do + get session_transfer_url("some-token") + + assert_response :success + end + + test "update establishes a session when the code is valid" do + user = users(:david) + + put session_transfer_url(user.transfer_id) + + assert_redirected_to root_url + assert parsed_cookies.signed[:session_token] + end +end diff --git a/test/models/qr_code_link_test.rb b/test/models/qr_code_link_test.rb new file mode 100644 index 000000000..47f189b41 --- /dev/null +++ b/test/models/qr_code_link_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class QrCodeLinkTest < ActiveSupport::TestCase + test "links can be signed and verified" do + link = QrCodeLink.new "https://example.com" + signed_link = link.signed + + verified = QrCodeLink.from_signed(signed_link) + assert_equal link.url, verified.url + end +end From b323cd4e4ca2690a364a8bb1ee354d7d7aef6253 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 18 Dec 2024 14:28:06 -0600 Subject: [PATCH 05/10] Edit user profile --- app/controllers/users_controller.rb | 10 ++++++- app/views/users/edit.html.erb | 46 +++++++++++++++++++++++++++++ app/views/users/show.html.erb | 7 +++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 app/views/users/edit.html.erb diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 47e240865..a70195c9e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ class UsersController < ApplicationController require_unauthenticated_access only: %i[ new create ] - before_action :set_user, only: :show + before_action :set_user, only: %i[ show edit update ] before_action :set_account_from_join_code, only: %i[ new create ] def new @@ -17,6 +17,14 @@ class UsersController < ApplicationController def show end + def edit + end + + def update + @user.update user_params + redirect_to user_path(@user) + end + private def set_account_from_join_code @account = Account.find_by_join_code!(params[:join_code]) diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb new file mode 100644 index 000000000..bffe4052e --- /dev/null +++ b/app/views/users/edit.html.erb @@ -0,0 +1,46 @@ +<% @page_title = "Edit your profile" %> + +<% content_for :header do %> + +<% end %> + +
+
+
+ <%= image_tag user_avatar_path(@user), alt: "Profile avatar for #{@user.name}", class: "avatar", size: 128 %> +
+ + <%= form_with model: @user, method: :patch, class: "flex flex-column gap", data: { controller: "form" } do |form| %> +
+ <%= translation_button :user_name %> + +
+
+ <%= translation_button :email_address %> + +
+
+ <%= translation_button :password %> + +
+ + <% end %> +
+
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 9fe91b249..f126ae6b0 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -6,6 +6,13 @@ <%= image_tag "arrow-left.svg", aria: { hidden: true }, size: 24 %> Back <% end %> + + <% if Current.user == @user %> + <%= link_to edit_user_path(@user), class: "btn flex-item-justify-end" do %> + <%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %> + Edit + <% end %> + <% end %> <% end %> From 295d8ce088e1f6ada911254340baf50b3c30826e Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 18 Dec 2024 14:44:05 -0600 Subject: [PATCH 06/10] Upload user avatar --- app/assets/stylesheets/inputs.css | 7 +++++++ app/controllers/users_controller.rb | 2 +- app/models/user.rb | 2 +- app/models/user/avatar.rb | 17 +++++++++++++++++ app/views/users/edit.html.erb | 11 +++++++---- 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 app/models/user/avatar.rb diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css index 602ff7976..eb4fbc821 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -69,6 +69,13 @@ --input-border-size: 0.15rem; } } + + &:is(.avatar) { + img, + input[type="file"] { + border-radius: 50%; + } + } } .input--select { diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a70195c9e..32e5c340c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -35,6 +35,6 @@ class UsersController < ApplicationController end def user_params - params.expect(user: [ :name, :email_address, :password ]) + params.expect(user: [ :name, :email_address, :password, :avatar ]) end end diff --git a/app/models/user.rb b/app/models/user.rb index 02c20ef98..907881cc5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ class User < ApplicationRecord - include Transferable + include Avatar, Transferable belongs_to :account diff --git a/app/models/user/avatar.rb b/app/models/user/avatar.rb new file mode 100644 index 000000000..a9d2ad12e --- /dev/null +++ b/app/models/user/avatar.rb @@ -0,0 +1,17 @@ +module User::Avatar + extend ActiveSupport::Concern + + included do + has_one_attached :avatar + end + + class_methods do + def from_avatar_token(sid) + find_signed!(sid, purpose: :avatar) + end + end + + def avatar_token + signed_id(purpose: :avatar) + end +end diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index bffe4052e..5ef66db39 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -11,11 +11,14 @@
-
- <%= image_tag user_avatar_path(@user), alt: "Profile avatar for #{@user.name}", class: "avatar", size: 128 %> -
+ <%= form_with model: @user, method: :patch, class: "flex flex-column gap", data: { controller: "form upload-preview" } do |form| %> + - <%= form_with model: @user, method: :patch, class: "flex flex-column gap", data: { controller: "form" } do |form| %>
<%= translation_button :user_name %>