From 4567f3ef6eae8a9ad333032b3c7a91e8128b4429 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Mon, 12 Jan 2026 19:24:51 +0100 Subject: [PATCH] init invitation --- app/controllers/invitations_controller.rb | 25 +++++++++++++++++++ app/mailers/user_mailer.rb | 8 ++++++ app/models/user.rb | 25 +++++++++++++++++++ app/views/invitations/edit.html.erb | 24 ++++++++++++++++++ .../user_mailer/invitation_email.html.erb | 3 +++ config/routes.rb | 5 +++- ...457_add_invitation_accepted_at_to_users.rb | 5 ++++ db/schema.rb | 3 ++- 8 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 app/controllers/invitations_controller.rb create mode 100644 app/mailers/user_mailer.rb create mode 100644 app/views/invitations/edit.html.erb create mode 100644 app/views/user_mailer/invitation_email.html.erb create mode 100644 db/migrate/20260112150457_add_invitation_accepted_at_to_users.rb diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb new file mode 100644 index 0000000..609d5da --- /dev/null +++ b/app/controllers/invitations_controller.rb @@ -0,0 +1,25 @@ +class InvitationsController < ApplicationController + before_action :set_user_by_token, only: %i[ edit update ] + allow_unauthenticated_access + + def edit + end + + def update + password_params = params.expect(user: [ :password, :password_confirmation ]) + + if @user.update(password_params.merge(invitation_accepted_at: Time.current)) + redirect_to root_path, notice: "Account activated!" + else + redirect_to invitation_path(params[:token]), alert: "Passwords did not match." + end + end + + private + + def set_user_by_token + @user = User.find_by_invitation_token(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to root_path, alert: "Invitation link is invalid or has expired." + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..3c46f54 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,8 @@ +class UserMailer < ApplicationMailer + def invitation_email(user, token) + @user = user + @invite_url = edit_invitation_url(token: token) + + mail(to: @user.email_address, subject: "Vous êtes invité à rejoindre la kluk") + end +end diff --git a/app/models/user.rb b/app/models/user.rb index c88d5b0..b646207 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,4 +3,29 @@ class User < ApplicationRecord has_many :sessions, dependent: :destroy normalizes :email_address, with: ->(e) { e.strip.downcase } + + generates_token_for :invitation, expires_in: 5.days do + invitation_accepted_at + end + + def invite_user!(email) + invited_user = User.create!( + email_address: email, + password: SecureRandom.hex(16) # temporary password + ) + + # Send invitation email using the generated token + token = invited_user.generate_token_for(:invitation) + UserMailer.invitation_email(invited_user, token).deliver_later + + invited_user + end + + def self.find_by_invitation_token(token) + find_by_token_for(:invitation, token) + end + + def invitation_token + generate_token_for(:invitation) + end end diff --git a/app/views/invitations/edit.html.erb b/app/views/invitations/edit.html.erb new file mode 100644 index 0000000..6262474 --- /dev/null +++ b/app/views/invitations/edit.html.erb @@ -0,0 +1,24 @@ +
+

Set up your password

+

Please set your password to activate your account.

+ + <%= form_with model: @user, url: invitation_path(token: params[:token]), method: :patch do |form| %> +
+ <%= form.password_field :password, + required: true, + autocomplete: "new-password", + placeholder: "Enter new password" %> +
+ +
+ <%= form.password_field :password_confirmation, + required: true, + autocomplete: "new-password", + placeholder: "Repeat new password" %> +
+ +
+ <%= form.submit "Activate Account" %> +
+ <% end %> +
diff --git a/app/views/user_mailer/invitation_email.html.erb b/app/views/user_mailer/invitation_email.html.erb new file mode 100644 index 0000000..18f6535 --- /dev/null +++ b/app/views/user_mailer/invitation_email.html.erb @@ -0,0 +1,3 @@ +

You're invited!

+

Click here to set up your account:

+<%= link_to "Set up account", @invite_url %> diff --git a/config/routes.rb b/config/routes.rb index 8de0ef4..7fb2674 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Rails.application.routes.draw do resource :session resources :passwords, param: :token - post '/rfid_tags' => 'rfid_tags#create' + post "/rfid_tags" => "rfid_tags#create" # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. @@ -14,4 +14,7 @@ Rails.application.routes.draw do # Defines the root path route ("/") root "moods#index" + + get "/invite/:token", to: "invitations#edit", as: :edit_invitation + patch "/invite/:token", to: "invitations#update", as: :invitation end diff --git a/db/migrate/20260112150457_add_invitation_accepted_at_to_users.rb b/db/migrate/20260112150457_add_invitation_accepted_at_to_users.rb new file mode 100644 index 0000000..4a85c20 --- /dev/null +++ b/db/migrate/20260112150457_add_invitation_accepted_at_to_users.rb @@ -0,0 +1,5 @@ +class AddInvitationAcceptedAtToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :invitation_accepted_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 320c5c8..6f33dca 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[8.0].define(version: 2026_01_12_141055) do +ActiveRecord::Schema[8.0].define(version: 2026_01_12_150457) do create_table "moods", force: :cascade do |t| t.string "mode" t.datetime "recorded_at" @@ -40,6 +40,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_12_141055) do t.string "password_digest", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "invitation_accepted_at" t.index ["email_address"], name: "index_users_on_email_address", unique: true end