rails-multitenant-signup-flow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +135 -0
- data/README.md +34 -0
- data/lib/activerecord_tenanted_auth_generator.rb +7 -0
- data/lib/generators/rails_multitenant_signup_flow/install/USAGE +6 -0
- data/lib/generators/rails_multitenant_signup_flow/install/install_generator.rb +173 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/database.yml.tt +42 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/global_record.rb.tt +4 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/host_url.rb.tt +20 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/session.rb.tt +3 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/sessions_controller.rb.tt +31 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/sign_ups_controller.rb.tt +72 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/sign_ups_show.html.erb.tt +41 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/tenant.rb.tt +14 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/tenant_service.rb.tt +21 -0
- data/lib/generators/rails_multitenant_signup_flow/install/templates/user.rb.tt +8 -0
- data/lib/rails_multitenant_signup_flow/version.rb +5 -0
- data/lib/rails_multitenant_signup_flow.rb +7 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c6d7c58293b3cf8ebbdc5c2da7dba0bdcc49222586ee38222d7a5b7262761f81
|
|
4
|
+
data.tar.gz: 907accb11cda8e032553fd8b7d3626dc1b4955d01765f897619b134857ed089b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3ccbfdeb308ab2c30a1585c1908bf856db89e0da10f76d3e009d81e74780b68589ce5034b00aeff50e95d68cbe0f5ebf13c4360da2f5dfe7808a39ee6ee8a032
|
|
7
|
+
data.tar.gz: 72c5d613aacc55df554209a237ce4333e0c60a2e19dea67cbc656547cad64c4ee382107d4c3022cb5145bbf735b66604e3ed20c4deed3c6062e91962c104d014
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
rails-multitenant-signup-flow (1.0.0)
|
|
5
|
+
activerecord-tenanted (~> 0.6, >= 0.6.0)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
actionpack (8.1.1)
|
|
11
|
+
actionview (= 8.1.1)
|
|
12
|
+
activesupport (= 8.1.1)
|
|
13
|
+
nokogiri (>= 1.8.5)
|
|
14
|
+
rack (>= 2.2.4)
|
|
15
|
+
rack-session (>= 1.0.1)
|
|
16
|
+
rack-test (>= 0.6.3)
|
|
17
|
+
rails-dom-testing (~> 2.2)
|
|
18
|
+
rails-html-sanitizer (~> 1.6)
|
|
19
|
+
useragent (~> 0.16)
|
|
20
|
+
actionview (8.1.1)
|
|
21
|
+
activesupport (= 8.1.1)
|
|
22
|
+
builder (~> 3.1)
|
|
23
|
+
erubi (~> 1.11)
|
|
24
|
+
rails-dom-testing (~> 2.2)
|
|
25
|
+
rails-html-sanitizer (~> 1.6)
|
|
26
|
+
activemodel (8.1.1)
|
|
27
|
+
activesupport (= 8.1.1)
|
|
28
|
+
activerecord (8.1.1)
|
|
29
|
+
activemodel (= 8.1.1)
|
|
30
|
+
activesupport (= 8.1.1)
|
|
31
|
+
timeout (>= 0.4.0)
|
|
32
|
+
activerecord-tenanted (0.6.0)
|
|
33
|
+
activerecord (>= 8.1.beta)
|
|
34
|
+
railties (>= 8.1.beta)
|
|
35
|
+
zeitwerk
|
|
36
|
+
activesupport (8.1.1)
|
|
37
|
+
base64
|
|
38
|
+
bigdecimal
|
|
39
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
40
|
+
connection_pool (>= 2.2.5)
|
|
41
|
+
drb
|
|
42
|
+
i18n (>= 1.6, < 2)
|
|
43
|
+
json
|
|
44
|
+
logger (>= 1.4.2)
|
|
45
|
+
minitest (>= 5.1)
|
|
46
|
+
securerandom (>= 0.3)
|
|
47
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
48
|
+
uri (>= 0.13.1)
|
|
49
|
+
base64 (0.3.0)
|
|
50
|
+
bigdecimal (3.3.1)
|
|
51
|
+
builder (3.3.0)
|
|
52
|
+
concurrent-ruby (1.3.5)
|
|
53
|
+
connection_pool (2.5.4)
|
|
54
|
+
crass (1.0.6)
|
|
55
|
+
date (3.5.0)
|
|
56
|
+
drb (2.2.3)
|
|
57
|
+
erb (5.1.3)
|
|
58
|
+
erubi (1.13.1)
|
|
59
|
+
globalid (1.3.0)
|
|
60
|
+
activesupport (>= 6.1)
|
|
61
|
+
i18n (1.14.7)
|
|
62
|
+
concurrent-ruby (~> 1.0)
|
|
63
|
+
io-console (0.8.1)
|
|
64
|
+
irb (1.15.3)
|
|
65
|
+
pp (>= 0.6.0)
|
|
66
|
+
rdoc (>= 4.0.0)
|
|
67
|
+
reline (>= 0.4.2)
|
|
68
|
+
json (2.16.0)
|
|
69
|
+
logger (1.7.0)
|
|
70
|
+
loofah (2.24.1)
|
|
71
|
+
crass (~> 1.0.2)
|
|
72
|
+
nokogiri (>= 1.12.0)
|
|
73
|
+
minitest (5.26.0)
|
|
74
|
+
nokogiri (1.18.10-x86_64-linux-gnu)
|
|
75
|
+
racc (~> 1.4)
|
|
76
|
+
pp (0.6.3)
|
|
77
|
+
prettyprint
|
|
78
|
+
prettyprint (0.2.0)
|
|
79
|
+
psych (5.2.6)
|
|
80
|
+
date
|
|
81
|
+
stringio
|
|
82
|
+
racc (1.8.1)
|
|
83
|
+
rack (3.2.4)
|
|
84
|
+
rack-session (2.1.1)
|
|
85
|
+
base64 (>= 0.1.0)
|
|
86
|
+
rack (>= 3.0.0)
|
|
87
|
+
rack-test (2.2.0)
|
|
88
|
+
rack (>= 1.3)
|
|
89
|
+
rackup (2.2.1)
|
|
90
|
+
rack (>= 3)
|
|
91
|
+
rails-dom-testing (2.3.0)
|
|
92
|
+
activesupport (>= 5.0.0)
|
|
93
|
+
minitest
|
|
94
|
+
nokogiri (>= 1.6)
|
|
95
|
+
rails-html-sanitizer (1.6.2)
|
|
96
|
+
loofah (~> 2.21)
|
|
97
|
+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
|
98
|
+
railties (8.1.1)
|
|
99
|
+
actionpack (= 8.1.1)
|
|
100
|
+
activesupport (= 8.1.1)
|
|
101
|
+
irb (~> 1.13)
|
|
102
|
+
rackup (>= 1.0.0)
|
|
103
|
+
rake (>= 12.2)
|
|
104
|
+
thor (~> 1.0, >= 1.2.2)
|
|
105
|
+
tsort (>= 0.2)
|
|
106
|
+
zeitwerk (~> 2.6)
|
|
107
|
+
rake (13.3.1)
|
|
108
|
+
rdoc (6.15.1)
|
|
109
|
+
erb
|
|
110
|
+
psych (>= 4.0.0)
|
|
111
|
+
tsort
|
|
112
|
+
reline (0.6.2)
|
|
113
|
+
io-console (~> 0.5)
|
|
114
|
+
securerandom (0.4.1)
|
|
115
|
+
stringio (3.1.7)
|
|
116
|
+
thor (1.4.0)
|
|
117
|
+
timeout (0.4.4)
|
|
118
|
+
tsort (0.2.0)
|
|
119
|
+
tzinfo (2.0.6)
|
|
120
|
+
concurrent-ruby (~> 1.0)
|
|
121
|
+
uri (1.1.1)
|
|
122
|
+
useragent (0.16.11)
|
|
123
|
+
zeitwerk (2.7.3)
|
|
124
|
+
|
|
125
|
+
PLATFORMS
|
|
126
|
+
x86_64-linux-gnu
|
|
127
|
+
|
|
128
|
+
DEPENDENCIES
|
|
129
|
+
globalid (>= 1.0)
|
|
130
|
+
minitest (~> 5.20)
|
|
131
|
+
rails-multitenant-signup-flow!
|
|
132
|
+
railties (>= 7.0)
|
|
133
|
+
|
|
134
|
+
BUNDLED WITH
|
|
135
|
+
2.7.2
|
data/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Rails Multitenant Signup Flow
|
|
2
|
+
|
|
3
|
+
A Rails generator that installs multi-tenant authentication scaffolding, services, concerns, and configuration based on `activerecord-tenanted`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
gem "rails-multitenant-signup-flow", path: "../rails-multitenant-signup-flow"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
bin/rails generate rails_multitenant_signup_flow:install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Run with `--force` to overwrite existing files.
|
|
26
|
+
|
|
27
|
+
## After running the generator
|
|
28
|
+
|
|
29
|
+
- Update `config/routes.rb` so your app has a root route, for example:
|
|
30
|
+
```ruby
|
|
31
|
+
root to: "sign_ups#show"
|
|
32
|
+
```
|
|
33
|
+
Adjust the controller/action to whatever should serve as your landing page.
|
|
34
|
+
- Start the server with `bin/rails server` and test subdomains locally using [lvh.me](https://lvh.me), which always resolves to `127.0.0.1`. For example, sign in at `http://app.lvh.me:3000` after creating a tenant named `app`.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module RailsMultitenantSignupFlow
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :force, type: :boolean, default: false, desc: "Overwrite existing files"
|
|
12
|
+
|
|
13
|
+
def ensure_authentication_scaffold
|
|
14
|
+
return if authentication_generated?
|
|
15
|
+
|
|
16
|
+
say_status :invoke, "rails generate authentication", :green
|
|
17
|
+
rails_command "generate authentication"
|
|
18
|
+
move_authentication_migrations_to_global
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def copy_sessions_controller
|
|
22
|
+
template "sessions_controller.rb.tt", "app/controllers/sessions_controller.rb", force: force?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def copy_sign_ups_controller
|
|
26
|
+
template "sign_ups_controller.rb.tt", "app/controllers/sign_ups_controller.rb", force: force?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def copy_sign_up_view
|
|
30
|
+
template "sign_ups_show.html.erb.tt", "app/views/sign_ups/show.html.erb", force: force?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def copy_global_record
|
|
34
|
+
template "global_record.rb.tt", "app/models/global_record.rb", force: force?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def copy_host_url_concern
|
|
38
|
+
template "host_url.rb.tt", "app/controllers/concerns/host_url.rb", force: force?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def copy_tenant_service
|
|
42
|
+
template "tenant_service.rb.tt", "lib/tenant_service.rb", force: force?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def copy_tenant_model
|
|
46
|
+
template "tenant.rb.tt", "app/models/tenant.rb", force: force?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def copy_user_model
|
|
50
|
+
template "user.rb.tt", "app/models/user.rb", force: force?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def copy_session_model
|
|
54
|
+
template "session.rb.tt", "app/models/session.rb", force: force?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def configure_application_record
|
|
58
|
+
path = "app/models/application_record.rb"
|
|
59
|
+
if File.exist?(path)
|
|
60
|
+
ensure_tenanted_in_application_record(path)
|
|
61
|
+
else
|
|
62
|
+
say_status :missing, path, :yellow
|
|
63
|
+
create_default_application_record(path)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def configure_database
|
|
68
|
+
template "database.yml.tt", "config/database.yml", force: force?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ensure_sign_up_route
|
|
72
|
+
routes_path = "config/routes.rb"
|
|
73
|
+
return unless File.exist?(routes_path)
|
|
74
|
+
|
|
75
|
+
content = File.read(routes_path)
|
|
76
|
+
return if content.include?("resource :sign_up")
|
|
77
|
+
|
|
78
|
+
route "resource :sign_up, only: [:show, :create]"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def generate_tenant_migrations
|
|
82
|
+
say_status :invoke, "rails generate migration CreateTenants", :green
|
|
83
|
+
rails_command "generate migration CreateTenants name:string --database=global"
|
|
84
|
+
|
|
85
|
+
say_status :invoke, "rails generate migration AddTenantToUsers", :green
|
|
86
|
+
rails_command "generate migration AddTenantToUsers tenant:references --database=global"
|
|
87
|
+
|
|
88
|
+
modify_add_tenant_to_users_migration
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def authentication_generated?
|
|
94
|
+
File.exist?("app/models/user.rb") && File.read("app/models/user.rb").match?(/has_secure_password/)
|
|
95
|
+
rescue Errno::ENOENT
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ensure_tenanted_in_application_record(path)
|
|
100
|
+
contents = File.read(path)
|
|
101
|
+
return if contents.include?("tenanted")
|
|
102
|
+
|
|
103
|
+
if contents.match?(/primary_abstract_class/)
|
|
104
|
+
gsub_file path, /primary_abstract_class\s*\n/, "primary_abstract_class\n tenanted\n"
|
|
105
|
+
else
|
|
106
|
+
inject_into_class path, "ApplicationRecord", " tenanted\n"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def create_default_application_record(path)
|
|
111
|
+
create_file path, <<~RUBY, force: force?
|
|
112
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
113
|
+
primary_abstract_class
|
|
114
|
+
tenanted
|
|
115
|
+
end
|
|
116
|
+
RUBY
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def force?
|
|
120
|
+
options[:force]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def move_authentication_migrations_to_global
|
|
124
|
+
Dir.glob("db/migrate/*_create_users.rb").each do |file|
|
|
125
|
+
move_migration file
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
Dir.glob("db/migrate/*_create_sessions.rb").each do |file|
|
|
129
|
+
move_migration file
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def move_migration(file)
|
|
134
|
+
destination = file.gsub("db/migrate", "db/global_migrate")
|
|
135
|
+
FileUtils.mkdir_p("db/global_migrate")
|
|
136
|
+
FileUtils.mv(file, destination)
|
|
137
|
+
say_status :move, "#{file} -> #{destination}", :green
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def modify_add_tenant_to_users_migration
|
|
141
|
+
migration_file = Dir.glob("db/global_migrate/*_add_tenant_to_users.rb").max_by { |f| File.mtime(f) }
|
|
142
|
+
return unless migration_file
|
|
143
|
+
|
|
144
|
+
content = File.read(migration_file)
|
|
145
|
+
|
|
146
|
+
new_content = content.gsub(
|
|
147
|
+
/def change\s*\n\s*add_reference :users, :tenant.*?\n\s*end/m,
|
|
148
|
+
<<~RUBY.strip
|
|
149
|
+
def change
|
|
150
|
+
add_reference :users, :tenant, null: true, foreign_key: true
|
|
151
|
+
|
|
152
|
+
# Create a default tenant using SQL
|
|
153
|
+
execute <<-SQL
|
|
154
|
+
INSERT INTO tenants (name, created_at, updated_at) VALUES ('default', datetime('now'), datetime('now'))
|
|
155
|
+
SQL
|
|
156
|
+
|
|
157
|
+
# Assign all existing users to the default tenant (id=1 since it's the first)
|
|
158
|
+
execute <<-SQL
|
|
159
|
+
UPDATE users SET tenant_id = 1
|
|
160
|
+
SQL
|
|
161
|
+
|
|
162
|
+
# Make the column NOT NULL
|
|
163
|
+
change_column_null :users, :tenant_id, false
|
|
164
|
+
end
|
|
165
|
+
RUBY
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
File.write(migration_file, new_content)
|
|
169
|
+
say_status :modify, migration_file, :yellow
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
default: &default
|
|
2
|
+
adapter: sqlite3
|
|
3
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
|
4
|
+
timeout: 5000
|
|
5
|
+
|
|
6
|
+
development:
|
|
7
|
+
primary:
|
|
8
|
+
<<: *default
|
|
9
|
+
database: storage/development/%{tenant}/development.sqlite3
|
|
10
|
+
tenanted: true
|
|
11
|
+
global:
|
|
12
|
+
<<: *default
|
|
13
|
+
database: storage/development/global.sqlite3
|
|
14
|
+
migrations_paths: db/global_migrate
|
|
15
|
+
|
|
16
|
+
test:
|
|
17
|
+
primary:
|
|
18
|
+
<<: *default
|
|
19
|
+
database: storage/test/%{tenant}/test.sqlite3
|
|
20
|
+
tenanted: true
|
|
21
|
+
global:
|
|
22
|
+
<<: *default
|
|
23
|
+
database: storage/test/global.sqlite3
|
|
24
|
+
migrations_paths: db/global_migrate
|
|
25
|
+
|
|
26
|
+
production:
|
|
27
|
+
primary:
|
|
28
|
+
<<: *default
|
|
29
|
+
database: storage/production/%{tenant}/production.sqlite3
|
|
30
|
+
tenanted: true
|
|
31
|
+
cache:
|
|
32
|
+
<<: *default
|
|
33
|
+
database: storage/production_cache.sqlite3
|
|
34
|
+
migrations_paths: db/cache_migrate
|
|
35
|
+
queue:
|
|
36
|
+
<<: *default
|
|
37
|
+
database: storage/production_queue.sqlite3
|
|
38
|
+
migrations_paths: db/queue_migrate
|
|
39
|
+
cable:
|
|
40
|
+
<<: *default
|
|
41
|
+
database: storage/production_cable.sqlite3
|
|
42
|
+
migrations_paths: db/cable_migrate
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module HostUrl
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
private
|
|
5
|
+
def host_url(tenant_name: nil, root: false)
|
|
6
|
+
host = if root
|
|
7
|
+
request.domain
|
|
8
|
+
elsif TenantService.tenant_subdomain?(request)
|
|
9
|
+
request.host
|
|
10
|
+
elsif Current.user
|
|
11
|
+
"#{Current.user&.tenant&.name}.#{request.host}"
|
|
12
|
+
elsif tenant_name.present?
|
|
13
|
+
"#{tenant_name}.#{request.host}"
|
|
14
|
+
else
|
|
15
|
+
request.host
|
|
16
|
+
end
|
|
17
|
+
host += ":#{request.port}" unless [80, 443].include?(request.port)
|
|
18
|
+
host
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class SessionsController < ApplicationController
|
|
2
|
+
include HostUrl
|
|
3
|
+
|
|
4
|
+
allow_unauthenticated_access only: %i[new create]
|
|
5
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
|
|
6
|
+
before_action :redirect_if_authenticated, only: %i[new create]
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
if (user = User.authenticate_by(params.permit(:email_address, :password)))
|
|
13
|
+
start_new_session_for user
|
|
14
|
+
|
|
15
|
+
redirect_to root_url(host: host_url(tenant_name: user.tenant.name)), allow_other_host: true
|
|
16
|
+
else
|
|
17
|
+
redirect_to new_session_path, alert: "Try another email address or password."
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
terminate_session
|
|
23
|
+
redirect_to root_url(host: host_url(root: true)), allow_other_host: true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def redirect_if_authenticated
|
|
29
|
+
redirect_to root_path if authenticated?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
class SignUpsController < ApplicationController
|
|
2
|
+
include HostUrl
|
|
3
|
+
allow_unauthenticated_access
|
|
4
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to sign_up_path, alert: "Try again later." }
|
|
5
|
+
|
|
6
|
+
before_action :redirect_if_on_subdomain, only: :show
|
|
7
|
+
before_action :validate_tenant_subdomain, only: :create
|
|
8
|
+
before_action :validate_tenant_availability, only: :create
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@user = User.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
tenant_name = sign_up_params[:tenant_name]
|
|
16
|
+
|
|
17
|
+
@user = User.new(sign_up_params.except(:tenant_name))
|
|
18
|
+
|
|
19
|
+
User.transaction do
|
|
20
|
+
@user.tenant = if TenantService.tenant_subdomain?(request)
|
|
21
|
+
Tenant.find_by!(name: TenantService.current_tenant_name(request))
|
|
22
|
+
elsif tenant_name.present?
|
|
23
|
+
TenantService.create!(tenant_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if @user.save
|
|
27
|
+
start_new_session_for(@user)
|
|
28
|
+
redirect_to root_url(host: host_url(tenant_name: @user.tenant.name)), allow_other_host: true
|
|
29
|
+
else
|
|
30
|
+
render :show, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def sign_up_params
|
|
38
|
+
params.expect(user: %i[email_address password password_confirmation tenant_name])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_tenant_availability
|
|
42
|
+
return unless sign_up_params[:tenant_name].present?
|
|
43
|
+
|
|
44
|
+
tenant_name = sign_up_params[:tenant_name]
|
|
45
|
+
|
|
46
|
+
if TenantService.tenant_exists?(tenant_name) && !TenantService.tenant_subdomain?(request)
|
|
47
|
+
@user = User.new(sign_up_params.except(:tenant_name))
|
|
48
|
+
flash.now[:alert] = "That tenant already exists. Please choose another name."
|
|
49
|
+
render :show, status: :unprocessable_entity
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_tenant_subdomain
|
|
54
|
+
if TenantService.tenant_subdomain?(request)
|
|
55
|
+
tenant_name = TenantService.current_tenant_name(request)
|
|
56
|
+
unless TenantService.tenant_exists?(tenant_name)
|
|
57
|
+
raise ActionController::BadRequest, "Invalid tenant subdomain"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
tenant_name_to_validate = sign_up_params[:tenant_name]
|
|
61
|
+
if tenant_name_to_validate.present? && tenant_name_to_validate != tenant_name
|
|
62
|
+
raise ActionController::BadRequest, "Tenant name does not match subdomain"
|
|
63
|
+
end
|
|
64
|
+
elsif sign_up_params[:tenant_name].blank?
|
|
65
|
+
raise ActionController::BadRequest, "Tenant name is required"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def redirect_if_on_subdomain
|
|
70
|
+
redirect_to sign_up_url(host: host_url(root: true)), allow_other_host: true if TenantService.tenant_subdomain?(request)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<h1>Sign up</h1>
|
|
2
|
+
|
|
3
|
+
<%%= form_with model: @user, url: sign_up_path do |form| %>
|
|
4
|
+
<%% if form.object.errors.any? %>
|
|
5
|
+
<div class="form-errors">
|
|
6
|
+
<h2><%%= pluralize(form.object.errors.count, "error") %> prohibited this account from being created:</h2>
|
|
7
|
+
<ul>
|
|
8
|
+
<%% form.object.errors.each do |error| %>
|
|
9
|
+
<li><%%= error.full_message %></li>
|
|
10
|
+
<%% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
<%% end %>
|
|
14
|
+
|
|
15
|
+
<div class="form-field">
|
|
16
|
+
<%%= form.label :email_address, "Email address" %><br>
|
|
17
|
+
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "email" %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<%% unless TenantService.tenant_subdomain?(request) %>
|
|
21
|
+
<div class="form-field">
|
|
22
|
+
<%%= form.label :tenant_name, "Tenant name" %><br>
|
|
23
|
+
<%%= form.text_field :tenant_name, required: true, autocomplete: "organization", placeholder: "example" %>
|
|
24
|
+
<p class="help-text">Use lowercase letters, numbers, and hyphens. This becomes your tenant subdomain.</p>
|
|
25
|
+
</div>
|
|
26
|
+
<%% end %>
|
|
27
|
+
|
|
28
|
+
<div class="form-field">
|
|
29
|
+
<%%= form.label :password, "Password" %><br>
|
|
30
|
+
<%%= form.password_field :password, required: true, autocomplete: "new-password", maxlength: 72 %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="form-field">
|
|
34
|
+
<%%= form.label :password_confirmation, "Confirm password" %><br>
|
|
35
|
+
<%%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", maxlength: 72 %>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="form-actions">
|
|
39
|
+
<%%= form.submit "Sign up" %>
|
|
40
|
+
</div>
|
|
41
|
+
<%% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class Tenant < GlobalRecord
|
|
2
|
+
has_many :users
|
|
3
|
+
|
|
4
|
+
validates :name, presence: true, uniqueness: { case_sensitive: false }, length: { in: 1..63 }
|
|
5
|
+
validates :name, format: { with: /\A[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/, message: "must be a valid subdomain (lowercase letters, numbers, and hyphens only, no leading/trailing hyphens)" }
|
|
6
|
+
|
|
7
|
+
before_validation :downcase_name
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def downcase_name
|
|
12
|
+
self.name = name.downcase if name.present?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class TenantService
|
|
2
|
+
def self.create!(tenant_name)
|
|
3
|
+
Tenant.transaction do
|
|
4
|
+
tenant = Tenant.create!(name: tenant_name)
|
|
5
|
+
ApplicationRecord.create_tenant(tenant_name)
|
|
6
|
+
tenant
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.tenant_exists?(tenant_name)
|
|
11
|
+
Tenant.exists?(name: tenant_name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.tenant_subdomain?(request)
|
|
15
|
+
request.subdomain.present? && request.subdomain != "www"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.current_tenant_name(request)
|
|
19
|
+
request.subdomain if tenant_subdomain?(request)
|
|
20
|
+
end
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-multitenant-signup-flow
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- WToa
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord-tenanted
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.6'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.6.0
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '0.6'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 0.6.0
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: minitest
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '5.20'
|
|
39
|
+
type: :development
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '5.20'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: railties
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '7.0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '7.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: globalid
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '1.0'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '1.0'
|
|
74
|
+
description: Installs controllers, services, and configuration to enable multi-tenant
|
|
75
|
+
authentication using activerecord-tenanted.
|
|
76
|
+
email:
|
|
77
|
+
- will@wtoa.dev
|
|
78
|
+
executables: []
|
|
79
|
+
extensions: []
|
|
80
|
+
extra_rdoc_files: []
|
|
81
|
+
files:
|
|
82
|
+
- Gemfile
|
|
83
|
+
- Gemfile.lock
|
|
84
|
+
- README.md
|
|
85
|
+
- lib/activerecord_tenanted_auth_generator.rb
|
|
86
|
+
- lib/generators/rails_multitenant_signup_flow/install/USAGE
|
|
87
|
+
- lib/generators/rails_multitenant_signup_flow/install/install_generator.rb
|
|
88
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/database.yml.tt
|
|
89
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/global_record.rb.tt
|
|
90
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/host_url.rb.tt
|
|
91
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/session.rb.tt
|
|
92
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/sessions_controller.rb.tt
|
|
93
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/sign_ups_controller.rb.tt
|
|
94
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/sign_ups_show.html.erb.tt
|
|
95
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/tenant.rb.tt
|
|
96
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/tenant_service.rb.tt
|
|
97
|
+
- lib/generators/rails_multitenant_signup_flow/install/templates/user.rb.tt
|
|
98
|
+
- lib/rails_multitenant_signup_flow.rb
|
|
99
|
+
- lib/rails_multitenant_signup_flow/version.rb
|
|
100
|
+
homepage: https://github.com/WToa/rails-multitenant-signup-flow
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
metadata:
|
|
104
|
+
homepage_uri: https://github.com/WToa/rails-multitenant-signup-flow
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: 3.0.0
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
requirements: []
|
|
119
|
+
rubygems_version: 3.6.9
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: Generators for configuring a Rails multi-tenant signup flow with activerecord-tenanted.
|
|
122
|
+
test_files: []
|