From 18df8fe449b3a1de7f03e1340c109f8bef54b4d6 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Sun, 29 Nov 2020 17:31:08 +0100 Subject: [PATCH 01/20] Add account creation service --- app/services/application_service.rb | 5 ++++ app/services/create_account.rb | 43 ++++++++++++++++++++++++++++ spec/services/create_account_spec.rb | 33 +++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 app/services/application_service.rb create mode 100644 app/services/create_account.rb create mode 100644 spec/services/create_account_spec.rb diff --git a/app/services/application_service.rb b/app/services/application_service.rb new file mode 100644 index 0000000..6185f03 --- /dev/null +++ b/app/services/application_service.rb @@ -0,0 +1,5 @@ +class ApplicationService + def self.call(*args, &block) + new(*args, &block).call + end +end diff --git a/app/services/create_account.rb b/app/services/create_account.rb new file mode 100644 index 0000000..6f9c3d3 --- /dev/null +++ b/app/services/create_account.rb @@ -0,0 +1,43 @@ +class CreateAccount < ApplicationService + def initialize(args) + @username = args[:username] + @email = args[:email] + @password = args[:password] + @invited_by_id = args[:invited_by_id] + end + + def call + add_ldap_document + end + + private + + def add_ldap_document + dn = "cn=#{@username},ou=kosmos.org,cn=users,dc=kosmos,dc=org" + attr = { + objectclass: ["top", "account", "person", "extensibleObject"], + cn: @username, + sn: @username, + uid: @username, + mail: @email, + userPassword: Devise.ldap_auth_password_builder.call(@password) + } + + ldap_client.add(dn: dn, attributes: attr) + end + + def ldap_client + ldap_client ||= Net::LDAP.new host: ldap_config['host'], + port: ldap_config['port'], + encryption: ldap_config['ssl'], + auth: { + method: :simple, + username: ldap_config['admin_user'], + password: ldap_config['admin_password'] + } + end + + def ldap_config + ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] + end +end diff --git a/spec/services/create_account_spec.rb b/spec/services/create_account_spec.rb new file mode 100644 index 0000000..dad16fb --- /dev/null +++ b/spec/services/create_account_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +RSpec.describe CreateAccount, type: :model do + let(:ldap_client_mock) { instance_double(Net::LDAP) } + + before do + allow(service).to receive(:ldap_client).and_return(ldap_client_mock) + end + + describe "#add_ldap_document" do + let(:service) { CreateAccount.new( + username: 'halfinney', + email: 'halfinney@example.com', + password: 'remember-remember-the-5th-of-november' + )} + + it "creates a new document with the correct attributes" do + expect(ldap_client_mock).to receive(:add).with( + dn: "cn=halfinney,ou=kosmos.org,cn=users,dc=kosmos,dc=org", + attributes: { + objectclass: ["top", "account", "person", "extensibleObject"], + cn: "halfinney", + sn: "halfinney", + uid: "halfinney", + mail: "halfinney@example.com", + userPassword: /^{SSHA512}.{171}=/ + } + ) + + service.send(:add_ldap_document) + end + end +end From d7fbda0855277a8ed56834e23a82d9dad4ff9be6 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Wed, 2 Dec 2020 15:22:58 +0100 Subject: [PATCH 02/20] Add basic invitations --- README.md | 2 +- app/controllers/invitations_controller.rb | 33 +++++++++++++++++++ app/helpers/invitations_helper.rb | 2 ++ app/models/invitation.rb | 14 ++++++++ app/models/user.rb | 3 ++ app/views/invitations/index.html.erb | 23 +++++++++++++ config/routes.rb | 6 ++-- .../20201130132533_create_invitations.rb | 12 +++++++ db/schema.rb | 11 ++++++- spec/fixtures/invitations.yml | 13 ++++++++ spec/helpers/invitations_helper_spec.rb | 15 +++++++++ spec/models/invitation_spec.rb | 22 +++++++++++++ 12 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 app/controllers/invitations_controller.rb create mode 100644 app/helpers/invitations_helper.rb create mode 100644 app/models/invitation.rb create mode 100644 app/views/invitations/index.html.erb create mode 100644 db/migrate/20201130132533_create_invitations.rb create mode 100644 spec/fixtures/invitations.yml create mode 100644 spec/helpers/invitations_helper_spec.rb create mode 100644 spec/models/invitation_spec.rb diff --git a/README.md b/README.md index 57c819c..6a1d5a6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ credentials, invites, donations, etc.. * [x] View LDAP users as admin * [ ] List my donations * [ ] Invite new users from your account -* [ ] Sign up for a new account via invite +* [ ] Sign up for a new account via invitation * [ ] Sign up for a new account by donating upfront * [ ] Sign up for a new account via proving contributions (via cryptographic signature) * [ ] ... diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb new file mode 100644 index 0000000..7809a07 --- /dev/null +++ b/app/controllers/invitations_controller.rb @@ -0,0 +1,33 @@ +class InvitationsController < ApplicationController + before_action :require_user_signed_in + + # GET /invitations + def index + @invitations = current_user.invitations + end + + # POST /invitations + def create + @invitation = Invitation.new(user: current_user) + + respond_to do |format| + if @invitation.save + format.html { redirect_to @invitation, notice: 'Invitation was successfully created.' } + format.json { render :show, status: :created, location: @invitation } + else + format.html { render :new } + format.json { render json: @invitation.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /invitations/1 + def destroy + @invitation = current_user.invitations.find(params[:id]) + @invitation.destroy + respond_to do |format| + format.html { redirect_to invitations_url } + format.json { head :no_content } + end + end +end diff --git a/app/helpers/invitations_helper.rb b/app/helpers/invitations_helper.rb new file mode 100644 index 0000000..1483b9e --- /dev/null +++ b/app/helpers/invitations_helper.rb @@ -0,0 +1,2 @@ +module InvitationsHelper +end diff --git a/app/models/invitation.rb b/app/models/invitation.rb new file mode 100644 index 0000000..74c3998 --- /dev/null +++ b/app/models/invitation.rb @@ -0,0 +1,14 @@ +class Invitation < ApplicationRecord + # Relations + belongs_to :user + + validates_presence_of :user + + before_create :generate_token + + private + + def generate_token + self.token = SecureRandom.hex(8) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 4d60b98..bc28e90 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,7 @@ class User < ApplicationRecord + # Relations + has_many :invitations, dependent: :destroy + # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :ldap_authenticatable, diff --git a/app/views/invitations/index.html.erb b/app/views/invitations/index.html.erb new file mode 100644 index 0000000..4e081ec --- /dev/null +++ b/app/views/invitations/index.html.erb @@ -0,0 +1,23 @@ +
+

Invitations

+ + + + + + + + + + + + <% @invitations.each do |invitation| %> + + + + + + <% end %> + +
TokenCreated at
<%= invitation.token %><%= invitation.created_at %><%= link_to 'Delete', invitation, method: :delete, data: { confirm: 'Are you sure?' } %>
+
diff --git a/config/routes.rb b/config/routes.rb index 72a33b2..8b08a0f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,13 @@ Rails.application.routes.draw do devise_for :users + get 'welcome', to: 'welcome#index' + get 'check_your_email', to: 'welcome#check_your_email' + get 'settings', to: 'settings#index' post 'settings_reset_password', to: 'settings#reset_password' - get 'welcome', to: 'welcome#index' - get 'check_your_email', to: 'welcome#check_your_email' + resources :invitations, only: ['index', 'create', 'destroy'] namespace :admin do root to: 'dashboard#index' diff --git a/db/migrate/20201130132533_create_invitations.rb b/db/migrate/20201130132533_create_invitations.rb new file mode 100644 index 0000000..75a410a --- /dev/null +++ b/db/migrate/20201130132533_create_invitations.rb @@ -0,0 +1,12 @@ +class CreateInvitations < ActiveRecord::Migration[6.0] + def change + create_table :invitations do |t| + t.string :token + t.integer :user_id + t.integer :invited_user_id + t.datetime :used_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d2d374e..3d99780 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,16 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_11_09_090739) do +ActiveRecord::Schema.define(version: 2020_11_30_132533) do + + create_table "invitations", force: :cascade do |t| + t.string "token" + t.integer "user_id" + t.integer "invited_user_id" + t.datetime "used_at" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end create_table "users", force: :cascade do |t| t.string "cn" diff --git a/spec/fixtures/invitations.yml b/spec/fixtures/invitations.yml new file mode 100644 index 0000000..e856184 --- /dev/null +++ b/spec/fixtures/invitations.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + token: fedcba654321 + user_id: 1 + invited_user_id: nil + used_at: nil + +two: + token: abcdef123456 + user_id: 1 + invited_user_id: nil + used_at: nil diff --git a/spec/helpers/invitations_helper_spec.rb b/spec/helpers/invitations_helper_spec.rb new file mode 100644 index 0000000..4ca2783 --- /dev/null +++ b/spec/helpers/invitations_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the InvitationsHelper. For example: +# +# describe InvitationsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe InvitationsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb new file mode 100644 index 0000000..7f7b953 --- /dev/null +++ b/spec/models/invitation_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe Invitation, type: :model do + let(:user) { build :user } + + describe "tokens" do + it "requires a user" do + expect { Invitation.create! }.to raise_error(ActiveRecord::RecordInvalid) + end + + it "generates a random token when created" do + invitation = Invitation.new(user: user) + invitation.save! + token = invitation.token + expect(token).to be_a(String) + expect(token.length).to eq(16) + + invitation_2 = Invitation.create(user: user) + expect(token).not_to eq(invitation_2.token) + end + end +end From e5fe843814b1118618e705cd93f07eefc8783cff Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Wed, 2 Dec 2020 15:23:18 +0100 Subject: [PATCH 03/20] Add task for generating invitations --- lib/tasks/invitations.rake | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 lib/tasks/invitations.rake diff --git a/lib/tasks/invitations.rake b/lib/tasks/invitations.rake new file mode 100644 index 0000000..1accd05 --- /dev/null +++ b/lib/tasks/invitations.rake @@ -0,0 +1,13 @@ +namespace :invitations do + desc "Generate invitations for all users" + task :generate_for_all_users, [:amount_per_user] => :environment do |t, args| + count = 0 + User.all.each do |user| + args[:amount_per_user].to_i.times do + user.invitations << Invitation.create(user: user) + count += 1 + end + end + puts "Created #{count} new invitations" + end +end From 69b99711e5367f33e30b7d8fafcc8f00d211ed21 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Wed, 2 Dec 2020 15:40:41 +0100 Subject: [PATCH 04/20] Remove fixtures, configure factory generation --- config/application.rb | 9 +++++---- spec/fixtures/invitations.yml | 13 ------------- spec/fixtures/users.yml | 7 ------- 3 files changed, 5 insertions(+), 24 deletions(-) delete mode 100644 spec/fixtures/invitations.yml delete mode 100644 spec/fixtures/users.yml diff --git a/config/application.rb b/config/application.rb index f171fca..127cf2f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -33,10 +33,11 @@ module Akkounts config.generators.system_tests = nil config.generators do |g| - g.orm :active_record - g.template_engine :erb - g.test_framework :rspec, fixture: true - g.stylesheets false + g.orm :active_record + g.template_engine :erb + g.test_framework :rspec, fixture: true + g.fixture_replacement :factory_bot, suffix_factory: 'factory', dir: 'spec/factories' + g.stylesheets false end end end diff --git a/spec/fixtures/invitations.yml b/spec/fixtures/invitations.yml deleted file mode 100644 index e856184..0000000 --- a/spec/fixtures/invitations.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -one: - token: fedcba654321 - user_id: 1 - invited_user_id: nil - used_at: nil - -two: - token: abcdef123456 - user_id: 1 - invited_user_id: nil - used_at: nil diff --git a/spec/fixtures/users.yml b/spec/fixtures/users.yml deleted file mode 100644 index 1e8c596..0000000 --- a/spec/fixtures/users.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -one: - uid: MyString - -two: - uid: MyString From 7aadb5cb5107e83ec3ea639df133866b1dce0272 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Wed, 2 Dec 2020 19:20:01 +0100 Subject: [PATCH 05/20] Require valid invitation to start sign-up process --- app/controllers/application_controller.rb | 6 +++ app/controllers/invitations_controller.rb | 17 +++++++- app/controllers/signup_controller.rb | 33 +++++++++++++++ app/helpers/signup_helper.rb | 2 + app/models/user.rb | 4 ++ app/views/layouts/signup.html.erb | 41 +++++++++++++++++++ app/views/shared/status_forbidden.html.erb | 2 +- app/views/shared/status_unauthorized.html.erb | 0 app/views/signup/index.html.erb | 12 ++++++ config/routes.rb | 4 +- spec/factories/invitations.rb | 7 ++++ spec/features/signup_spec.rb | 39 ++++++++++++++++++ spec/helpers/signup_helper_spec.rb | 15 +++++++ spec/models/user_spec.rb | 8 ++++ spec/requests/signup_request_spec.rb | 14 +++++++ 15 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 app/controllers/signup_controller.rb create mode 100644 app/helpers/signup_helper.rb create mode 100644 app/views/layouts/signup.html.erb create mode 100644 app/views/shared/status_unauthorized.html.erb create mode 100644 app/views/signup/index.html.erb create mode 100644 spec/factories/invitations.rb create mode 100644 spec/features/signup_spec.rb create mode 100644 spec/helpers/signup_helper_spec.rb create mode 100644 spec/requests/signup_request_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dd4cf1c..c672886 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,12 @@ class ApplicationController < ActionController::Base end end + def require_user_signed_out + if user_signed_in? + redirect_to root_path and return + end + end + def authorize_admin http_status :forbidden unless current_user.is_admin? end diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb index 7809a07..46bd2b1 100644 --- a/app/controllers/invitations_controller.rb +++ b/app/controllers/invitations_controller.rb @@ -1,11 +1,26 @@ class InvitationsController < ApplicationController - before_action :require_user_signed_in + before_action :require_user_signed_in, except: ["show"] + before_action :require_user_signed_out, only: ["show"] + + layout "signup", only: ["show"] # GET /invitations def index @invitations = current_user.invitations end + # GET /invitations/a-random-invitation-token + def show + token = session[:invitation_token] = params[:id] + + if Invitation.where(token: token, used_at: nil).exists? + redirect_to signup_path and return + else + flash.now[:alert] = "This invitation either doesn't exist or has already been used." + http_status :unauthorized + end + end + # POST /invitations def create @invitation = Invitation.new(user: current_user) diff --git a/app/controllers/signup_controller.rb b/app/controllers/signup_controller.rb new file mode 100644 index 0000000..21dd7b3 --- /dev/null +++ b/app/controllers/signup_controller.rb @@ -0,0 +1,33 @@ +class SignupController < ApplicationController + before_action :require_user_signed_out + before_action :require_invitation + before_action :set_invitation + + layout "signup" + + def index + @invited_by_name = @invitation.user.address + end + + private + + def require_invitation + if session[:invitation_token].blank? + flash.now[:alert] = "You need an invitation to sign up for an account." + http_status :unauthorized + elsif !valid_invitation?(session[:invitation_token]) + flash.now[:alert] = "This invitation either doesn't exist or has already been used." + http_status :unauthorized + end + + @invitation = Invitation.find_by(token: session[:invitation_token]) + end + + def valid_invitation?(token) + Invitation.where(token: session[:invitation_token], used_at: nil).exists? + end + + def set_invitation + @invitation = Invitation.find_by(token: session[:invitation_token]) + end +end diff --git a/app/helpers/signup_helper.rb b/app/helpers/signup_helper.rb new file mode 100644 index 0000000..f954dda --- /dev/null +++ b/app/helpers/signup_helper.rb @@ -0,0 +1,2 @@ +module SignupHelper +end diff --git a/app/models/user.rb b/app/models/user.rb index bc28e90..e6f80f0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,4 +36,8 @@ class User < ApplicationRecord false end end + + def address + "#{self.cn}@#{self.ou}" + end end diff --git a/app/views/layouts/signup.html.erb b/app/views/layouts/signup.html.erb new file mode 100644 index 0000000..05fbf73 --- /dev/null +++ b/app/views/layouts/signup.html.erb @@ -0,0 +1,41 @@ + + + + Sign up | Kosmos Accounts + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + + + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> + <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + + + +
+
+

+ Kosmos + Sign Up + +

+ <% if user_signed_in? %> +

+ Signed in as <%= current_user.cn %>@kosmos.org. + <%= link_to "Log out", destroy_user_session_path, method: :delete %> +

+ <% end %> +
+ + <% flash.each do |type, msg| %> +
+

<%= msg %>

+
+ <% end %> + +
+ <%= yield %> +
+
+ + diff --git a/app/views/shared/status_forbidden.html.erb b/app/views/shared/status_forbidden.html.erb index a00cb2d..cff9a63 100644 --- a/app/views/shared/status_forbidden.html.erb +++ b/app/views/shared/status_forbidden.html.erb @@ -1,2 +1,2 @@

Access forbidden

-

Not with those shoes, buddy.

+

Sorry, you're not allowed to access this page.

diff --git a/app/views/shared/status_unauthorized.html.erb b/app/views/shared/status_unauthorized.html.erb new file mode 100644 index 0000000..e69de29 diff --git a/app/views/signup/index.html.erb b/app/views/signup/index.html.erb new file mode 100644 index 0000000..f3782fa --- /dev/null +++ b/app/views/signup/index.html.erb @@ -0,0 +1,12 @@ +

Welcome

+

+ Hey there! You were invited to sign up for a Kosmos account by + <%= @invited_by_name %>. +

+

+ This invitation can only be used once, and sign-up is currently only possible + by invitation. Seems like you have good friends! +

+

+ <%= link_to "Get started", signup_path, class: "next-step" %> +

diff --git a/config/routes.rb b/config/routes.rb index 8b08a0f..f6aa90c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,10 +4,12 @@ Rails.application.routes.draw do get 'welcome', to: 'welcome#index' get 'check_your_email', to: 'welcome#check_your_email' + get 'signup', to: 'signup#index' + get 'settings', to: 'settings#index' post 'settings_reset_password', to: 'settings#reset_password' - resources :invitations, only: ['index', 'create', 'destroy'] + resources :invitations, only: ['index', 'show', 'create', 'destroy'] namespace :admin do root to: 'dashboard#index' diff --git a/spec/factories/invitations.rb b/spec/factories/invitations.rb new file mode 100644 index 0000000..621ac80 --- /dev/null +++ b/spec/factories/invitations.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :invitation do + id { 1 } + token { "abcdef123456" } + user + end +end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb new file mode 100644 index 0000000..e6ea504 --- /dev/null +++ b/spec/features/signup_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +RSpec.describe "Signup", type: :feature do + let(:user) { create :user } + + before do + @unused_invitation = Invitation.create(user: user) + @used_invitation = Invitation.create(user: user) + @used_invitation.update_attribute :used_at, DateTime.now - 1.day + end + + scenario "Follow link for non-existing invitation" do + visit invitation_url(id: "123") + + within ".flash-msg.alert" do + expect(page).to have_content("doesn't exist") + end + end + + scenario "Follow link for used invitation" do + visit invitation_url(id: @used_invitation.token) + + within ".flash-msg.alert" do + expect(page).to have_content("has already been used") + end + end + + scenario "Follow link for unused invitation" do + visit invitation_url(id: @unused_invitation.token) + + expect(current_url).to eq(signup_url) + expect(page).to have_content("Welcome") + end + + scenario "Successful signup" do + visit invitation_url(id: @unused_invitation.token) + click_link "Get started" + end +end diff --git a/spec/helpers/signup_helper_spec.rb b/spec/helpers/signup_helper_spec.rb new file mode 100644 index 0000000..0ed5adc --- /dev/null +++ b/spec/helpers/signup_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the SignupHelper. For example: +# +# describe SignupHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe SignupHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3bc4941..ed135d9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -20,4 +20,12 @@ RSpec.describe User, type: :model do expect(user.is_admin?).to be false end end + + describe "#address" do + let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" } + + it "returns the user address" do + expect(user.address).to eq("jimmy@kosmos.org") + end + end end diff --git a/spec/requests/signup_request_spec.rb b/spec/requests/signup_request_spec.rb new file mode 100644 index 0000000..a782f48 --- /dev/null +++ b/spec/requests/signup_request_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe "Signups", type: :request do + + describe "GET /index" do + context "without invitation" do + it "returns http unauthorized" do + get "/signup" + expect(response).to have_http_status(:unauthorized) + end + end + end + +end From 9cebfd3f582b9c1b8c0f45e96b827c59a17340f3 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Wed, 2 Dec 2020 20:46:46 +0100 Subject: [PATCH 06/20] Signup steps with validation --- app/assets/stylesheets/forms.scss | 28 ++++++++ app/controllers/signup_controller.rb | 77 ++++++++++++++++++++++ app/models/user.rb | 9 +++ app/views/layouts/signup.html.erb | 2 +- app/views/shared/status_not_found.html.erb | 2 + app/views/signup/index.html.erb | 2 +- app/views/signup/steps.html.erb | 55 ++++++++++++++++ config/routes.rb | 2 + 8 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/forms.scss create mode 100644 app/views/shared/status_not_found.html.erb create mode 100644 app/views/signup/steps.html.erb diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss new file mode 100644 index 0000000..64fcb0b --- /dev/null +++ b/app/assets/stylesheets/forms.scss @@ -0,0 +1,28 @@ +form { + .field_with_errors { + display: inline-block; + } +} + +.layout-signup { + label { + display: none; + } + + input[type=text], input[type=email], input[type=password] { + font-size: 1.25rem; + padding: 0.5rem 1rem; + } + + span.at-sign, span.domain { + font-size: 1.25rem; + } + + .error-msg { + color: #bc0101; + } + + .actions { + margin-top: 2rem; + } +} diff --git a/app/controllers/signup_controller.rb b/app/controllers/signup_controller.rb index 21dd7b3..237b77e 100644 --- a/app/controllers/signup_controller.rb +++ b/app/controllers/signup_controller.rb @@ -2,6 +2,7 @@ class SignupController < ApplicationController before_action :require_user_signed_out before_action :require_invitation before_action :set_invitation + before_action :set_new_user, only: ["steps", "validate"] layout "signup" @@ -9,8 +10,61 @@ class SignupController < ApplicationController @invited_by_name = @invitation.user.address end + def steps + @step = params[:step].to_i + http_status :not_found unless [1,2,3].include?(@step) + @validation_error = session[:validation_error] + end + + def validate + session[:validation_error] = nil + + case user_params.keys.first + when "cn" + @user.cn = user_params[:cn] + @user.valid? + session[:new_user] = @user + + if @user.errors[:cn].present? + session[:validation_error] = @user.errors[:cn].first # Store user including validation errors + redirect_to signup_steps_path(1) and return + else + redirect_to signup_steps_path(2) and return + end + when "email" + @user.email = user_params[:email] + @user.valid? + session[:new_user] = @user + + if @user.errors[:email].present? + session[:validation_error] = @user.errors[:email].first # Store user including validation errors + redirect_to signup_steps_path(2) and return + else + redirect_to signup_steps_path(3) and return + end + when "password" + @user.password = user_params[:password] + @user.password_confirmation = user_params[:password] + @user.valid? + session[:new_user] = @user + + if @user.errors[:password].present? + session[:validation_error] = @user.errors[:password].first # Store user including validation errors + redirect_to signup_steps_path(3) and return + else + complete_signup + msg = "Almost done! We have sent you an email to confirm your address." + redirect_to(check_your_email_path, notice: msg) and return + end + end + end + private + def user_params + params.require(:user).permit(:cn, :email, :password) + end + def require_invitation if session[:invitation_token].blank? flash.now[:alert] = "You need an invitation to sign up for an account." @@ -30,4 +84,27 @@ class SignupController < ApplicationController def set_invitation @invitation = Invitation.find_by(token: session[:invitation_token]) end + + def set_new_user + if session[:new_user].present? + @user = User.new(session[:new_user]) + else + @user = User.new(ou: "kosmos.org") + end + end + + def complete_signup + @user.save! + session[:new_user] = nil + session[:validation_error] = nil + + CreateAccount.call( + username: @user.cn, + email: @user.email, + password: @user.password + ) + + @invitation.update_attributes invited_user_id: @user.id, + used_at: DateTime.now + end end diff --git a/app/models/user.rb b/app/models/user.rb index e6f80f0..b2f3693 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,6 +2,10 @@ class User < ApplicationRecord # Relations has_many :invitations, dependent: :destroy + validates_uniqueness_of :cn + validates_uniqueness_of :email + validates_length_of :cn, :minimum => 3 + # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :ldap_authenticatable, @@ -40,4 +44,9 @@ class User < ApplicationRecord def address "#{self.cn}@#{self.ou}" end + + def valid_attribute?(attribute_name) + self.valid? + self.errors[attribute_name].blank? + end end diff --git a/app/views/layouts/signup.html.erb b/app/views/layouts/signup.html.erb index 05fbf73..1a49165 100644 --- a/app/views/layouts/signup.html.erb +++ b/app/views/layouts/signup.html.erb @@ -11,7 +11,7 @@ <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> - +

diff --git a/app/views/shared/status_not_found.html.erb b/app/views/shared/status_not_found.html.erb new file mode 100644 index 0000000..c31810c --- /dev/null +++ b/app/views/shared/status_not_found.html.erb @@ -0,0 +1,2 @@ +

Not found

+

Sorry, this page does not exist.

diff --git a/app/views/signup/index.html.erb b/app/views/signup/index.html.erb index f3782fa..f3d8b80 100644 --- a/app/views/signup/index.html.erb +++ b/app/views/signup/index.html.erb @@ -8,5 +8,5 @@ by invitation. Seems like you have good friends!

- <%= link_to "Get started", signup_path, class: "next-step" %> + <%= link_to "Get started", signup_steps_path(1), class: "next-step" %>

diff --git a/app/views/signup/steps.html.erb b/app/views/signup/steps.html.erb new file mode 100644 index 0000000..107ed5a --- /dev/null +++ b/app/views/signup/steps.html.erb @@ -0,0 +1,55 @@ +<% case @step %> +<% when 1 %> +

Choose a username

+ <%= form_for @user, :url => signup_validate_url do |f| %> +
+

+ <%= f.label :cn, 'Username' %>
+ <%= f.text_field :cn, autofocus: true, autocomplete: "username" %> + @ + kosmos.org +

+ <% if @validation_error.present? %> +

Username <%= @validation_error %>

+ <% end %> +
+
+

<%= f.submit "Continue" %>

+
+ <% end %> + +<% when 2 %> +

What's your email?

+ <%= form_for @user, :url => signup_validate_url do |f| %> +
+

+ <%= f.label :email, 'Email address' %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +

+ <% if @validation_error.present? %> +

Email <%= @validation_error %>

+ <% end %> +
+
+

<%= f.submit "Continue" %>

+
+ <% end %> + +<% when 3 %> +

Choose a password

+ + <%= form_for @user, :url => signup_validate_url do |f| %> +
+

+ <%= f.label :password, 'Password' %>
+ <%= f.password_field :password, autofocus: true %> +

+ <% if @validation_error.present? %> +

Password <%= @validation_error %>

+ <% end %> +
+
+

<%= f.submit "Continue" %>

+
+ <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index f6aa90c..bdefef0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,8 @@ Rails.application.routes.draw do get 'check_your_email', to: 'welcome#check_your_email' get 'signup', to: 'signup#index' + match 'signup/:step', to: 'signup#steps', as: :signup_steps, via: [:get, :post] + post 'signup_validate', to: 'signup#validate' get 'settings', to: 'settings#index' post 'settings_reset_password', to: 'settings#reset_password' From ba31ed559ab3967545be9e880c1372ed871793be Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 00:53:43 +0100 Subject: [PATCH 07/20] Improve wording --- config/locales/devise.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index d3c0c52..9003184 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -3,7 +3,7 @@ en: devise: confirmations: - confirmed: "Your email address has been confirmed. You can now log in below." + confirmed: "Thanks for confirming your email address! Your account has been activated." send_instructions: "You will receive an email with instructions for how to confirm your email address in a moment." send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." failure: @@ -15,7 +15,7 @@ en: not_found_in_database: "Invalid %{authentication_keys} or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." - unconfirmed: "You have to confirm your email address before continuing." + unconfirmed: "Please confirm your email address before continuing." mailer: confirmation_instructions: subject: "Confirmation instructions" From 7a58babd8e4e727c44ef6b509efbf27db2c33f0c Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 13:48:23 +0100 Subject: [PATCH 08/20] Remove obsolete argument --- app/services/create_account.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/services/create_account.rb b/app/services/create_account.rb index 6f9c3d3..474427a 100644 --- a/app/services/create_account.rb +++ b/app/services/create_account.rb @@ -3,7 +3,6 @@ class CreateAccount < ApplicationService @username = args[:username] @email = args[:email] @password = args[:password] - @invited_by_id = args[:invited_by_id] end def call From b8e75c7c4abcd3d6df7d47b09ef4ede97bc44091 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 13:50:07 +0100 Subject: [PATCH 09/20] Re-order services on dashboard Docs and forums are more important for most users than Gitea and Drone. --- app/views/dashboard/index.html.erb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 3b4d234..534a39e 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -11,18 +11,6 @@ Chat rooms and instant messaging (XMPP/Jabber)

-
-

<%= link_to "Gitea", "https://gitea.kosmos.org" %>

-

- Code hosting and collaboration for software projects -

-
-
-

<%= link_to "Drone CI", "https://drone.kosmos.org" %>

-

- Continuous integration for software projects, tied to our Gitea -

-

<%= link_to "Wiki", "https://wiki.kosmos.org" %>

@@ -35,6 +23,18 @@ Kosmos community forums and user support/help site

+
+

<%= link_to "Gitea", "https://gitea.kosmos.org" %>

+

+ Code hosting and collaboration for software projects +

+
+
+

<%= link_to "Drone CI", "https://drone.kosmos.org" %>

+

+ Continuous integration for software projects, tied to our Gitea +

+
From 8a7d809b92b7561eb411b1c8991845aaa0fd0423 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:04:58 +0100 Subject: [PATCH 10/20] Add scopes for invitations --- app/models/invitation.rb | 3 +++ spec/models/invitation_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 74c3998..6400e35 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -6,6 +6,9 @@ class Invitation < ApplicationRecord before_create :generate_token + scope :unused, -> { where(used_at: nil) } + scope :used, -> { where.not(used_at: nil) } + private def generate_token diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index 7f7b953..3ec7237 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -19,4 +19,26 @@ RSpec.describe Invitation, type: :model do expect(token).not_to eq(invitation_2.token) end end + + describe "scopes" do + before do + @unused_invitation = create :invitation, user: user + @used_invitation = create :invitation, user: user, used_at: DateTime.now + @used_invitation_2 = create :invitation, user: user, used_at: DateTime.now + end + + describe "#unused" do + it "returns unused invitations" do + expect(Invitation.unused.count).to eq(1) + expect(Invitation.unused.first).to eq(@unused_invitation) + end + end + + describe "#used" do + it "returns used invitations" do + expect(Invitation.used.count).to eq(2) + expect(Invitation.used.first).to eq(@used_invitation) + end + end + end end From f7e48ad3a629baca28e28c276756c9960849ed90 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:47:57 +0100 Subject: [PATCH 11/20] Accept non-existing terms Legal how does it work --- app/assets/stylesheets/forms.scss | 7 +++++++ app/views/signup/steps.html.erb | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 64fcb0b..e8afb30 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -25,4 +25,11 @@ form { .actions { margin-top: 2rem; } + + .accept-terms { + margin-top: 2rem; + font-size: 0.85rem; + line-height: 1.5em; + color: #888; + } } diff --git a/app/views/signup/steps.html.erb b/app/views/signup/steps.html.erb index 107ed5a..3c527f4 100644 --- a/app/views/signup/steps.html.erb +++ b/app/views/signup/steps.html.erb @@ -48,8 +48,14 @@

Password <%= @validation_error %>

<% end %> +

+ + By clicking the button below, you accept our future Terms of Service + and Privacy Policy. Don't worry, they will be excellent! + +

-

<%= f.submit "Continue" %>

+

<%= f.submit "Create account" %>

<% end %> <% end %> From a792d66c90012462d4c33fda5046f1f79659821d Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:48:43 +0100 Subject: [PATCH 12/20] Show unused invitations list --- app/controllers/invitations_controller.rb | 3 +- app/models/invitation.rb | 3 ++ app/views/invitations/index.html.erb | 41 ++++++++++++++++++----- spec/factories/invitations.rb | 1 - 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb index 46bd2b1..c0affd8 100644 --- a/app/controllers/invitations_controller.rb +++ b/app/controllers/invitations_controller.rb @@ -6,7 +6,8 @@ class InvitationsController < ApplicationController # GET /invitations def index - @invitations = current_user.invitations + @invitations_unused = current_user.invitations.unused + @invitations_used = current_user.invitations.used end # GET /invitations/a-random-invitation-token diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 6400e35..e4910a5 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -2,10 +2,13 @@ class Invitation < ApplicationRecord # Relations belongs_to :user + # Validations validates_presence_of :user + # Hooks before_create :generate_token + # Scopes scope :unused, -> { where(used_at: nil) } scope :used, -> { where.not(used_at: nil) } diff --git a/app/views/invitations/index.html.erb b/app/views/invitations/index.html.erb index 4e081ec..5b461ce 100644 --- a/app/views/invitations/index.html.erb +++ b/app/views/invitations/index.html.erb @@ -1,23 +1,46 @@

Invitations

+ <% if @invitations_unused.any? %> + + + + + + + + <% @invitations_unused.each do |invitation| %> + + + + <% end %> + +
URL
<%= invitation_url(invitation.token) %>
+ <% else %> +

+ You do not have any invitations to give away yet. All good + things come in time. +

+ <% end %> +
+<% if @invitations_used.any? %> +

Accepted Invitations

- - - + + + - - <% @invitations.each do |invitation| %> + <% @invitations_used.each do |invitation| %> - - - + + + <% end %>
TokenCreated atURLUsed atInvited user
<%= invitation.token %><%= invitation.created_at %><%= link_to 'Delete', invitation, method: :delete, data: { confirm: 'Are you sure?' } %><%= invitation_url(invitation.token) %><%= invitation.used_at %><%= User.find(invitation.invited_user_id).address %>
- +<% end %> diff --git a/spec/factories/invitations.rb b/spec/factories/invitations.rb index 621ac80..5ca1ad3 100644 --- a/spec/factories/invitations.rb +++ b/spec/factories/invitations.rb @@ -1,6 +1,5 @@ FactoryBot.define do factory :invitation do - id { 1 } token { "abcdef123456" } user end From c8ccb418b26dc7b623aa127fc0fa4d179cb1a415 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:49:02 +0100 Subject: [PATCH 13/20] Add indexes for invitations table --- db/migrate/20201130132533_create_invitations.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/migrate/20201130132533_create_invitations.rb b/db/migrate/20201130132533_create_invitations.rb index 75a410a..0b53910 100644 --- a/db/migrate/20201130132533_create_invitations.rb +++ b/db/migrate/20201130132533_create_invitations.rb @@ -8,5 +8,7 @@ class CreateInvitations < ActiveRecord::Migration[6.0] t.timestamps end + add_index :invitations, :user_id + add_index :invitations, :invited_user_id end end From e44535daee78485909d39eae9c0fb5e0a01ec69d Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:49:16 +0100 Subject: [PATCH 14/20] Don't use deprecated method --- app/controllers/signup_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/signup_controller.rb b/app/controllers/signup_controller.rb index 237b77e..b332bbe 100644 --- a/app/controllers/signup_controller.rb +++ b/app/controllers/signup_controller.rb @@ -104,7 +104,6 @@ class SignupController < ApplicationController password: @user.password ) - @invitation.update_attributes invited_user_id: @user.id, - used_at: DateTime.now + @invitation.update! invited_user_id: @user.id, used_at: DateTime.now end end From 2a2b0a90dcdd3f782d476ff1ad067488ccfd4a42 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:49:37 +0100 Subject: [PATCH 15/20] Validate email address properly --- app/models/concerns/email_validatable.rb | 15 +++++++++++++++ app/models/user.rb | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/email_validatable.rb diff --git a/app/models/concerns/email_validatable.rb b/app/models/concerns/email_validatable.rb new file mode 100644 index 0000000..6a83881 --- /dev/null +++ b/app/models/concerns/email_validatable.rb @@ -0,0 +1,15 @@ +require 'mail' + +module EmailValidatable + extend ActiveSupport::Concern + + class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + begin + a = Mail::Address.new(value) + rescue Mail::Field::ParseError + record.errors[attribute] << (options[:message] || "is not a valid address") + end + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index b2f3693..6987a0a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,10 +1,13 @@ class User < ApplicationRecord + include EmailValidatable + # Relations has_many :invitations, dependent: :destroy validates_uniqueness_of :cn - validates_uniqueness_of :email validates_length_of :cn, :minimum => 3 + validates_uniqueness_of :email + validates :email, email: true # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable From 022094ce51ffe9c2bfcd5555a4876ce377d8ed87 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:50:02 +0100 Subject: [PATCH 16/20] Add feature spec for whole signup process --- spec/features/signup_spec.rb | 110 +++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 23 deletions(-) diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index e6ea504..09ebcc5 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -3,37 +3,101 @@ require "rails_helper" RSpec.describe "Signup", type: :feature do let(:user) { create :user } - before do - @unused_invitation = Invitation.create(user: user) - @used_invitation = Invitation.create(user: user) - @used_invitation.update_attribute :used_at, DateTime.now - 1.day - end + describe "Invitation handling" do + before do + @unused_invitation = Invitation.create(user: user) + @used_invitation = Invitation.create(user: user) + @used_invitation.update_attribute :used_at, DateTime.now - 1.day + end - scenario "Follow link for non-existing invitation" do - visit invitation_url(id: "123") + scenario "Follow link for non-existing invitation" do + visit invitation_url(id: "123") - within ".flash-msg.alert" do - expect(page).to have_content("doesn't exist") + within ".flash-msg.alert" do + expect(page).to have_content("doesn't exist") + end + end + + scenario "Follow link for used invitation" do + visit invitation_url(id: @used_invitation.token) + + within ".flash-msg.alert" do + expect(page).to have_content("has already been used") + end + end + + scenario "Follow link for unused invitation" do + visit invitation_url(id: @unused_invitation.token) + + expect(current_url).to eq(signup_url) + expect(page).to have_content("Welcome") end end - scenario "Follow link for used invitation" do - visit invitation_url(id: @used_invitation.token) - - within ".flash-msg.alert" do - expect(page).to have_content("has already been used") + describe "Signup steps" do + before do + @invitation = Invitation.create(user: user) + visit invitation_url(id: @invitation.token) + click_link "Get started" end - end - scenario "Follow link for unused invitation" do - visit invitation_url(id: @unused_invitation.token) + scenario "Successful signup (happy path galore)" do + expect(page).to have_content("Choose a username") - expect(current_url).to eq(signup_url) - expect(page).to have_content("Welcome") - end + fill_in "user_cn", with: "tony" + click_button "Continue" + expect(page).to have_content("What's your email?") - scenario "Successful signup" do - visit invitation_url(id: @unused_invitation.token) - click_link "Get started" + fill_in "user_email", with: "tony@example.com" + click_button "Continue" + expect(page).to have_content("Choose a password") + + expect(CreateAccount).to receive(:call) + .with(username: "tony", email: "tony@example.com", password: "a-valid-password") + .and_return(true) + + fill_in "user_password", with: "a-valid-password" + click_button "Create account" + within ".flash-msg.notice" do + expect(page).to have_content("confirm your address") + end + expect(page).to have_content("close this window or tab now") + expect(User.last.confirmed_at).to be_nil + end + + scenario "Validation errors" do + fill_in "user_cn", with: "t" + click_button "Continue" + expect(page).to have_content("Username is too short") + fill_in "user_cn", with: "jimmy" + click_button "Continue" + expect(page).to have_content("Username has already been taken") + fill_in "user_cn", with: "tony" + click_button "Continue" + + fill_in "user_email", with: "tony@" + click_button "Continue" + expect(page).to have_content("Email is not a valid address") + fill_in "user_email", with: "" + click_button "Continue" + expect(page).to have_content("Email can't be blank") + fill_in "user_email", with: "tony@example.com" + click_button "Continue" + + fill_in "user_password", with: "123456" + click_button "Create account" + expect(page).to have_content("Password is too short") + + expect(CreateAccount).to receive(:call) + .with(username: "tony", email: "tony@example.com", password: "a-valid-password") + .and_return(true) + + fill_in "user_password", with: "a-valid-password" + click_button "Create account" + within ".flash-msg.notice" do + expect(page).to have_content("confirm your address") + end + expect(User.last.cn).to eq("tony") + end end end From 7ca91cf882bbc245337a2f5f487476c3232d618d Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:56:11 +0100 Subject: [PATCH 17/20] Don't run caching steps on CI when not master or PR --- .drone.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.drone.yml b/.drone.yml index d38f769..8d07bcf 100644 --- a/.drone.yml +++ b/.drone.yml @@ -12,6 +12,9 @@ steps: restore: true mount: - vendor + when: + branch: + - master - name: rspec image: guildeducation/rails:2.7.1-12.19.0 commands: @@ -30,6 +33,9 @@ steps: rebuild: true mount: - vendor + when: + branch: + - master volumes: - name: cache From f6512894100f95fdb3f0a5067ee37f1b9a951383 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 14:59:21 +0100 Subject: [PATCH 18/20] Add mailer host config for test So Devise can build URLs in confirmation emails --- config/environments/test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/environments/test.rb b/config/environments/test.rb index 3375f1c..ac775c3 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -43,4 +43,10 @@ Rails.application.configure do # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true + + config.action_mailer.default_url_options = { + host: "accounts.kosmos.org", + protocol: "https", + from: "accounts@kosmos.org" + } end From 458b585cdb39bb3673d62c10956cedc36221255c Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Thu, 3 Dec 2020 15:14:13 +0100 Subject: [PATCH 19/20] Check off invitation signup feature in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a1d5a6..0711052 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ credentials, invites, donations, etc.. * [x] Reset account password when logged in, via reset email * [x] Log in with admin permissions * [x] View LDAP users as admin +* [x] Sign up for a new account via invitation * [ ] List my donations * [ ] Invite new users from your account -* [ ] Sign up for a new account via invitation * [ ] Sign up for a new account by donating upfront * [ ] Sign up for a new account via proving contributions (via cryptographic signature) * [ ] ... From 5b6d6bbd006515d621813517f322ff04ab68b482 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Fri, 4 Dec 2020 13:29:07 +0100 Subject: [PATCH 20/20] Explain ApplicationService --- app/services/application_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/application_service.rb b/app/services/application_service.rb index 6185f03..401fe02 100644 --- a/app/services/application_service.rb +++ b/app/services/application_service.rb @@ -1,4 +1,6 @@ class ApplicationService + # This enables executing a service's `#call` method directly via + # `MyService.call(args)`, without creating a class instance it first. def self.call(*args, &block) new(*args, &block).call end