multi_tenant_subdomain 0.1.0 → 0.1.1
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 +4 -4
- data/generators/multi_tenant_subdomain/multi_tenant_subdomain_generator.rb +73 -0
- data/lib/generators/multi_tenant_subdomain/multi_tenant_subdomain_generator.rb +11 -14
- data/lib/generators/multi_tenant_subdomain/templates/multi_tenant_subdomain.rb +8 -0
- data/lib/multi_tenant_subdomain/active_record_extension.rb +65 -4
- data/lib/multi_tenant_subdomain/middleware.rb +21 -4
- data/lib/multi_tenant_subdomain/tenant_manager.rb +5 -0
- data/lib/multi_tenant_subdomain/version.rb +1 -1
- data/lib/multi_tenant_subdomain.rb +75 -7
- data/multi_tenant_subdomain-0.1.0.gem +0 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17f95fbb6c386d5bcb5e6f3d0f118edede54214733aba63b8846e863aaeea99a
|
4
|
+
data.tar.gz: 5058ee214f80b0af3c21b036f86a1d990b8b1292400703ade74625766d961a6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03ddf93849556da343a19662e80dd124dc0919fae3b6ff6a3bd9b0c248e1c45a8bec754cb698924e5561ea22c592e7598ebb9c082d77aec6172ac188f2ed9693
|
7
|
+
data.tar.gz: 17a6c92634f5e3a40a87f1d3c435692de0837ba0dcfa8423994732af3716028a4885220f38e9e746eee12c13c96366002828372b507b656d196bea548542b444
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "active_record"
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module MultiTenantSubdomain
|
6
|
+
class MultiTenantSubdomainGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
argument :model_name, type: :string
|
9
|
+
|
10
|
+
def create_model
|
11
|
+
return if behavior != :invoke
|
12
|
+
|
13
|
+
if model_exists?
|
14
|
+
add_subdomain_field if migration_needed?
|
15
|
+
else
|
16
|
+
generate "model", "#{model_name} subdomain:string:index"
|
17
|
+
end
|
18
|
+
|
19
|
+
create_initializer
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy
|
23
|
+
return if behavior != :revoke
|
24
|
+
|
25
|
+
remove_initializer
|
26
|
+
remove_model if model_exists?
|
27
|
+
remove_migrations
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def model_exists?
|
33
|
+
File.exist?(Rails.root.join("app", "models", "#{model_name.underscore}.rb"))
|
34
|
+
end
|
35
|
+
|
36
|
+
def table_exists?
|
37
|
+
ActiveRecord::Base.connection.data_source_exists?(model_name.tableize)
|
38
|
+
end
|
39
|
+
|
40
|
+
def column_exists?(column)
|
41
|
+
return false unless table_exists?
|
42
|
+
|
43
|
+
ActiveRecord::Base.connection.column_exists?(model_name.tableize, column)
|
44
|
+
end
|
45
|
+
|
46
|
+
def migration_needed?
|
47
|
+
table_exists? && !column_exists?(:subdomain)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_subdomain_field
|
51
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
52
|
+
migration_file = "db/migrate/#{timestamp}_add_subdomain_to_#{model_name.tableize}.rb"
|
53
|
+
|
54
|
+
create_file migration_file, <<~RUBY
|
55
|
+
class AddSubdomainTo#{model_name.camelize} < ActiveRecord::Migration[#{Rails.version.to_f}]
|
56
|
+
def change
|
57
|
+
add_column :#{model_name.tableize}, :subdomain, :string, index: true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
RUBY
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_initializer
|
64
|
+
initializer_file = "config/initializers/multi_tenant_subdomain.rb"
|
65
|
+
|
66
|
+
create_file initializer_file, <<~RUBY
|
67
|
+
MultiTenantSubdomain.configure do |config|
|
68
|
+
config.tenant_model = "#{model_name}"
|
69
|
+
end
|
70
|
+
RUBY
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -10,10 +10,10 @@ module MultiTenantSubdomain
|
|
10
10
|
def create_model
|
11
11
|
return if behavior != :invoke
|
12
12
|
|
13
|
-
|
14
|
-
generate "model", "#{model_name} subdomain:string:index"
|
15
|
-
else
|
13
|
+
if model_exists?
|
16
14
|
add_subdomain_field if migration_needed?
|
15
|
+
else
|
16
|
+
generate "model", "#{model_name} subdomain:string:index"
|
17
17
|
end
|
18
18
|
|
19
19
|
create_initializer
|
@@ -21,6 +21,7 @@ module MultiTenantSubdomain
|
|
21
21
|
|
22
22
|
def destroy
|
23
23
|
return if behavior != :revoke
|
24
|
+
|
24
25
|
remove_initializer
|
25
26
|
remove_model if model_exists?
|
26
27
|
remove_migrations
|
@@ -62,23 +63,19 @@ module MultiTenantSubdomain
|
|
62
63
|
def create_initializer
|
63
64
|
initializer_path = Rails.root.join("config", "initializers", "multi_tenant_subdomain.rb")
|
64
65
|
|
65
|
-
|
66
|
-
create_file initializer_path, <<~RUBY
|
67
|
-
MultiTenantSubdomain.configure do |config|
|
68
|
-
config.tenant_model = #{model_name.to_s.camelize}
|
69
|
-
end
|
70
|
-
RUBY
|
71
|
-
else
|
66
|
+
if File.exist?(initializer_path)
|
72
67
|
puts "X Initializer already exists. Skipping..."
|
68
|
+
else
|
69
|
+
template "multi_tenant_subdomain.rb", initializer_path
|
73
70
|
end
|
74
71
|
end
|
75
72
|
|
76
73
|
def remove_initializer
|
77
74
|
initializer_path = Rails.root.join("config", "initializers", "multi_tenant_subdomain.rb")
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
75
|
+
return unless File.exist?(initializer_path)
|
76
|
+
|
77
|
+
FileUtils.rm_f(initializer_path)
|
78
|
+
puts "X Removed initializer: config/initializers/multi_tenant_subdomain.rb"
|
82
79
|
end
|
83
80
|
|
84
81
|
def remove_model
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
MultiTenantSubdomain.configure do |config|
|
4
|
+
config.tenant_model_class = "<%= model_name.to_s.camelize %>"
|
5
|
+
config.tenant_model_table = "<%= model_name.tableize %>"
|
6
|
+
config.tenant_model_pk = "id"
|
7
|
+
config.tenant_model_fk = "<%= model_name.underscore %>_id"
|
8
|
+
end
|
@@ -1,18 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MultiTenantSubdomain
|
4
|
+
# This module is used to extend ActiveRecord models to support multi-tenant
|
5
|
+
# subdomains.
|
6
|
+
#
|
7
|
+
# It adds a default scope to the model to ensure that all records are
|
8
|
+
# scoped to the current tenant.
|
9
|
+
#
|
10
|
+
# It also adds a before_validation callback to set the tenant_id on the
|
11
|
+
# model.
|
2
12
|
module ActiveRecordExtension
|
3
13
|
extend ActiveSupport::Concern
|
4
14
|
|
5
15
|
included do
|
6
|
-
|
7
|
-
|
16
|
+
# Don't try to access the database during class loading
|
17
|
+
# Instead, set up a hook that will be called when the model is used
|
18
|
+
after_initialize :ensure_tenant_configured, if: :tenant_configured?
|
19
|
+
|
20
|
+
# Class method to temporarily disable tenant scoping
|
21
|
+
define_singleton_method(
|
22
|
+
"without_#{MultiTenantSubdomain.config.tenant_model_class.to_s.underscore}_scope".to_sym
|
23
|
+
) do |&block|
|
24
|
+
unscoped(&block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class_methods do
|
29
|
+
def tenant_configured?
|
30
|
+
# Safely check if we can access table information without raising errors
|
31
|
+
begin
|
32
|
+
connected? && table_exists? &&
|
33
|
+
column_names.include?(MultiTenantSubdomain.config.tenant_model_fk)
|
34
|
+
rescue => e
|
35
|
+
false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Apply tenant scoping when the model is used
|
40
|
+
def apply_tenant_scoping
|
41
|
+
# Only apply if not already applied
|
42
|
+
return if instance_variable_defined?(:@tenant_scoping_applied)
|
43
|
+
|
44
|
+
before_validation :set_tenant_id
|
45
|
+
default_scope do
|
46
|
+
where(
|
47
|
+
MultiTenantSubdomain.config.tenant_model_fk =>
|
48
|
+
MultiTenantSubdomain::TenantManager.current_tenant.send(
|
49
|
+
MultiTenantSubdomain.config.tenant_model_pk.to_sym
|
50
|
+
)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
@tenant_scoping_applied = true
|
54
|
+
end
|
8
55
|
end
|
9
56
|
|
10
57
|
private
|
11
58
|
|
59
|
+
def tenant_configured?
|
60
|
+
self.class.tenant_configured?
|
61
|
+
end
|
62
|
+
|
63
|
+
def ensure_tenant_configured
|
64
|
+
self.class.apply_tenant_scoping
|
65
|
+
end
|
66
|
+
|
12
67
|
def set_tenant_id
|
13
|
-
self.
|
68
|
+
self[MultiTenantSubdomain.config.tenant_model_fk.to_sym] = MultiTenantSubdomain::TenantManager.current_tenant.send(
|
69
|
+
MultiTenantSubdomain.config.tenant_model_pk.to_sym
|
70
|
+
)
|
14
71
|
end
|
15
72
|
end
|
16
73
|
end
|
17
74
|
|
18
|
-
|
75
|
+
# Use ActiveSupport.on_load to include the module in ActiveRecord::Base
|
76
|
+
# This ensures that the module is loaded at the right time in the Rails initialization process
|
77
|
+
ActiveSupport.on_load(:active_record) do
|
78
|
+
include MultiTenantSubdomain::ActiveRecordExtension
|
79
|
+
end
|
@@ -1,11 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MultiTenantSubdomain
|
4
|
+
# This middleware is used to set the current tenant based on the subdomain
|
5
|
+
# of the request.
|
6
|
+
#
|
7
|
+
# It is used to ensure that all requests are scoped to a single tenant.
|
2
8
|
class Middleware
|
3
9
|
def initialize(app)
|
4
10
|
@app = app
|
5
11
|
end
|
6
12
|
|
7
13
|
def call(env)
|
8
|
-
|
14
|
+
subdomain = extract_subdomain(env)
|
15
|
+
tenant = MultiTenantSubdomain.config.tenant_model_table.classify.constantize.find_by(subdomain: subdomain)
|
16
|
+
|
17
|
+
if tenant.nil? && subdomain.present?
|
18
|
+
# Option 1: Return a 404 Not Found response
|
19
|
+
return [404, {'Content-Type' => 'text/html'}, ['Tenant not found']]
|
20
|
+
|
21
|
+
# Option 2: Redirect to main domain (uncomment to use)
|
22
|
+
# main_domain = env['HTTP_HOST'].split('.').drop(1).join('.')
|
23
|
+
# return [302, {'Location' => "https://#{main_domain}"}, []]
|
24
|
+
end
|
25
|
+
|
9
26
|
MultiTenantSubdomain::TenantManager.current_tenant = tenant
|
10
27
|
@app.call(env)
|
11
28
|
ensure
|
@@ -18,13 +35,13 @@ module MultiTenantSubdomain
|
|
18
35
|
|
19
36
|
def extract_subdomain(env)
|
20
37
|
request = Rack::Request.new(env)
|
21
|
-
host_parts = request.host.split(
|
22
|
-
|
38
|
+
host_parts = request.host.split(".")
|
39
|
+
|
23
40
|
# Avoid affecting primary domain
|
24
41
|
return nil if host_parts.length < 3
|
25
42
|
|
26
43
|
# Avoid affecting www subdomain
|
27
|
-
return nil if host_parts.first ==
|
44
|
+
return nil if host_parts.first == "www"
|
28
45
|
|
29
46
|
host_parts.first
|
30
47
|
end
|
@@ -7,21 +7,89 @@ require_relative "multi_tenant_subdomain/middleware"
|
|
7
7
|
require_relative "multi_tenant_subdomain/active_record_extension"
|
8
8
|
require_relative "generators/multi_tenant_subdomain/multi_tenant_subdomain_generator"
|
9
9
|
|
10
|
+
##
|
11
|
+
# This module provides multi-tenant subdomain functionality for Rails applications.
|
12
|
+
#
|
13
|
+
# It allows you to easily scope your application's data and functionality
|
14
|
+
# to different tenants based on the subdomain in the request URL.
|
15
|
+
#
|
16
|
+
# To use this module, you need to:
|
17
|
+
#
|
18
|
+
# 1. Generate the necessary migrations and configurations using the provided generator:
|
19
|
+
#
|
20
|
+
# `rails generate multi_tenant_subdomain YourTenantModelName`
|
21
|
+
#
|
22
|
+
# 2. Configure the module with your tenant model class and primary/foreign keys:
|
23
|
+
#
|
24
|
+
# ```ruby
|
25
|
+
# MultiTenantSubdomain.configure do |config|
|
26
|
+
# config.tenant_model_class = "Account" # Default: "Tenant"
|
27
|
+
# config.tenant_model_table = "accounts" # Default: "tenants"
|
28
|
+
# config.tenant_model_pk = "id" # Default: "id"
|
29
|
+
# config.tenant_model_fk = "account_id" # Default: "tenant_id"
|
30
|
+
# end
|
31
|
+
# ```
|
32
|
+
#
|
33
|
+
# 3. Use the `MultiTenantSubdomain::TenantManager.current_tenant` method to access the
|
34
|
+
# current tenant in your controllers and views.
|
10
35
|
module MultiTenantSubdomain
|
36
|
+
##
|
37
|
+
# Error class for any errors raised by the module.
|
11
38
|
class Error < StandardError; end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Configuration class for the module.
|
42
|
+
class Configuration
|
43
|
+
##
|
44
|
+
# @!attribute tenant_model_class
|
45
|
+
# @return [String] The name of the tenant model class. Default: "Tenant".
|
46
|
+
attr_accessor :tenant_model_class
|
47
|
+
|
48
|
+
##
|
49
|
+
# @!attribute tenant_model_table
|
50
|
+
# @return [String] The name of the tenant model table. Default: "tenants".
|
51
|
+
attr_accessor :tenant_model_table
|
52
|
+
|
53
|
+
##
|
54
|
+
# @!attribute tenant_model_pk
|
55
|
+
# @return [String] The name of the primary key column on the tenant model. Default: "id".
|
56
|
+
attr_accessor :tenant_model_pk
|
57
|
+
|
58
|
+
##
|
59
|
+
# @!attribute tenant_model_fk
|
60
|
+
# @return [String] The name of the foreign key column on the models that should be scoped to a tenant. Default: "tenant_id".
|
61
|
+
attr_accessor :tenant_model_fk
|
62
|
+
|
63
|
+
# Initializes a new Configuration object with default values.
|
64
|
+
def initialize
|
65
|
+
@tenant_model_class = "Tenant"
|
66
|
+
@tenant_model_table = "tenants"
|
67
|
+
@tenant_model_pk = "id"
|
68
|
+
@tenant_model_fk = "tenant_id"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
12
72
|
class << self
|
73
|
+
##
|
74
|
+
# @!attribute config
|
75
|
+
# @return [Configuration] The configuration object for the module.
|
13
76
|
attr_accessor :config
|
14
77
|
|
78
|
+
##
|
79
|
+
# Configures the module.
|
80
|
+
#
|
81
|
+
# @yield [config] The configuration object.
|
82
|
+
# @yieldparam config [Configuration] The configuration object.
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# MultiTenantSubdomain.configure do |config|
|
86
|
+
# config.tenant_model_class = "Account"
|
87
|
+
# config.tenant_model_pk = "account_id"
|
88
|
+
# config.tenant_model_fk = "account_id"
|
89
|
+
# end
|
15
90
|
def configure
|
16
91
|
self.config ||= Configuration.new
|
17
92
|
yield(config) if block_given?
|
18
93
|
end
|
19
94
|
end
|
20
|
-
class Configuration
|
21
|
-
attr_accessor :tenant_model
|
22
|
-
|
23
|
-
def initialize
|
24
|
-
@tenant_model = "Tenant"
|
25
|
-
end
|
26
|
-
end
|
27
95
|
end
|
Binary file
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: multi_tenant_subdomain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharvy Ahmed
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-02-
|
10
|
+
date: 2025-02-28 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rspec
|
@@ -80,13 +80,16 @@ files:
|
|
80
80
|
- LICENSE.txt
|
81
81
|
- README.md
|
82
82
|
- Rakefile
|
83
|
+
- generators/multi_tenant_subdomain/multi_tenant_subdomain_generator.rb
|
83
84
|
- lib/generators/multi_tenant_subdomain/multi_tenant_subdomain_generator.rb
|
85
|
+
- lib/generators/multi_tenant_subdomain/templates/multi_tenant_subdomain.rb
|
84
86
|
- lib/multi_tenant_subdomain.rb
|
85
87
|
- lib/multi_tenant_subdomain/active_record_extension.rb
|
86
88
|
- lib/multi_tenant_subdomain/middleware.rb
|
87
89
|
- lib/multi_tenant_subdomain/railtie.rb
|
88
90
|
- lib/multi_tenant_subdomain/tenant_manager.rb
|
89
91
|
- lib/multi_tenant_subdomain/version.rb
|
92
|
+
- multi_tenant_subdomain-0.1.0.gem
|
90
93
|
- sig/multi_tenant_subdomain.rbs
|
91
94
|
homepage: https://github.com/sharvy/multi_tenant_subdomain
|
92
95
|
licenses:
|