osso 0.0.5.pre.lambda → 0.0.6
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/.buildkite/pipeline.yml +6 -4
- data/.github/dependabot.yml +8 -0
- data/.github/workflows/automerge.yml +19 -0
- data/.rubocop.yml +4 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +48 -27
- data/bin/annotate +3 -1
- data/db/schema.rb +40 -3
- data/lib/osso.rb +0 -1
- data/lib/osso/db/migrate/20201023142158_add_rodauth_tables.rb +47 -0
- data/lib/osso/db/migrate/20201105122026_add_token_index_to_access_tokens.rb +5 -0
- data/lib/osso/db/migrate/20201106154936_add_requested_to_authorization_codes_and_access_tokens.rb +6 -0
- data/lib/osso/db/migrate/20201109160851_add_sso_issuer_to_identity_providers.rb +12 -0
- data/lib/osso/db/migrate/20201110190754_remove_oauth_client_id_from_enterprise_accounts.rb +9 -0
- data/lib/osso/db/migrate/20201112160120_add_ping_to_identity_provider_service_enum.rb +28 -0
- data/lib/osso/error/account_configuration_error.rb +1 -0
- data/lib/osso/error/oauth_error.rb +6 -3
- data/lib/osso/graphql/mutation.rb +1 -0
- data/lib/osso/graphql/mutations.rb +1 -0
- data/lib/osso/graphql/mutations/create_enterprise_account.rb +0 -7
- data/lib/osso/graphql/mutations/create_identity_provider.rb +7 -6
- data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
- data/lib/osso/graphql/query.rb +8 -0
- data/lib/osso/graphql/resolvers/enterprise_accounts.rb +2 -2
- data/lib/osso/graphql/types.rb +2 -2
- data/lib/osso/graphql/types/admin_user.rb +9 -0
- data/lib/osso/graphql/types/base_object.rb +1 -1
- data/lib/osso/graphql/types/identity_provider.rb +2 -0
- data/lib/osso/graphql/types/identity_provider_service.rb +2 -1
- data/lib/osso/lib/app_config.rb +1 -1
- data/lib/osso/lib/route_map.rb +0 -16
- data/lib/osso/lib/saml_handler.rb +5 -0
- data/lib/osso/models/access_token.rb +4 -2
- data/lib/osso/models/account.rb +34 -0
- data/lib/osso/models/authorization_code.rb +2 -1
- data/lib/osso/models/enterprise_account.rb +3 -1
- data/lib/osso/models/identity_provider.rb +18 -4
- data/lib/osso/models/models.rb +1 -0
- data/lib/osso/models/oauth_client.rb +0 -1
- data/lib/osso/routes/admin.rb +39 -33
- data/lib/osso/routes/auth.rb +9 -9
- data/lib/osso/routes/oauth.rb +34 -16
- data/lib/osso/version.rb +1 -1
- data/lib/osso/views/admin.erb +5 -0
- data/lib/osso/views/error.erb +1 -0
- data/lib/osso/views/layout.erb +0 -0
- data/lib/osso/views/multiple_providers.erb +1 -0
- data/lib/osso/views/welcome.erb +0 -0
- data/lib/tasks/bootstrap.rake +25 -4
- data/osso-rb.gemspec +5 -0
- data/spec/factories/account.rb +24 -0
- data/spec/factories/enterprise_account.rb +11 -3
- data/spec/factories/identity_providers.rb +10 -2
- data/spec/factories/user.rb +4 -0
- data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
- data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
- data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
- data/spec/graphql/query/identity_provider_spec.rb +2 -2
- data/spec/models/enterprise_account_spec.rb +18 -0
- data/spec/models/identity_provider_spec.rb +24 -3
- data/spec/routes/admin_spec.rb +7 -41
- data/spec/routes/auth_spec.rb +17 -18
- data/spec/routes/oauth_spec.rb +87 -5
- data/spec/spec_helper.rb +3 -3
- data/spec/support/views/layout.erb +1 -0
- metadata +98 -7
- data/lib/osso/helpers/auth.rb +0 -94
- data/lib/osso/helpers/helpers.rb +0 -8
- data/spec/helpers/auth_spec.rb +0 -269
@@ -0,0 +1,9 @@
|
|
1
|
+
class RemoveOauthClientIdFromEnterpriseAccounts < ActiveRecord::Migration[6.0]
|
2
|
+
def up
|
3
|
+
remove_reference :enterprise_accounts, :oauth_client, index: true
|
4
|
+
end
|
5
|
+
|
6
|
+
def down
|
7
|
+
add_reference :enterprise_accounts, :oauth_client, type: :uuid, index: true
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class AddPingToIdentityProviderServiceEnum < ActiveRecord::Migration[6.0]
|
2
|
+
disable_ddl_transaction!
|
3
|
+
|
4
|
+
def up
|
5
|
+
execute <<-SQL
|
6
|
+
ALTER TYPE identity_provider_service ADD VALUE 'PING';
|
7
|
+
SQL
|
8
|
+
end
|
9
|
+
|
10
|
+
def down
|
11
|
+
execute <<~SQL
|
12
|
+
CREATE TYPE identity_provider_service_new AS ENUM ('AZURE', 'OKTA', 'ONELOGIN', 'GOOGLE');
|
13
|
+
|
14
|
+
-- Remove values that won't be compatible with new definition
|
15
|
+
DELETE FROM identity_providers WHERE service = 'PING';
|
16
|
+
|
17
|
+
-- Convert to new type, casting via text representation
|
18
|
+
ALTER TABLE identity_providers
|
19
|
+
ALTER COLUMN service TYPE identity_provider_service_new
|
20
|
+
USING (service::text::identity_provider_service_new);
|
21
|
+
|
22
|
+
-- and swap the types
|
23
|
+
DROP TYPE identity_provider_service;
|
24
|
+
|
25
|
+
ALTER TYPE identity_provider_service_new RENAME TO identity_provider_service;
|
26
|
+
SQL
|
27
|
+
end
|
28
|
+
end
|
@@ -10,6 +10,7 @@ module Osso
|
|
10
10
|
|
11
11
|
class NoAccountForOAuthClientError < OAuthError
|
12
12
|
def initialize(domain: 'the requested domain')
|
13
|
+
super
|
13
14
|
@domain = domain
|
14
15
|
end
|
15
16
|
|
@@ -30,13 +31,15 @@ module Osso
|
|
30
31
|
|
31
32
|
class InvalidRedirectUri < OAuthError
|
32
33
|
def initialize(redirect_uri:)
|
34
|
+
super
|
33
35
|
@redirect_uri = redirect_uri
|
34
36
|
end
|
35
37
|
|
36
38
|
def message
|
37
|
-
"The requested redirect URI #{@redirect_uri} is not on the allow-list for the rquested
|
38
|
-
|
39
|
-
|
39
|
+
"The requested redirect URI #{@redirect_uri} is not on the allow-list for the rquested " \
|
40
|
+
'OAuth client identifier. Review our OAuth documentation, check you\'re using the correct ' \
|
41
|
+
'OAuth client identifier, and confirm your Redirect URI allow-list includes the ' \
|
42
|
+
'appropriate URI(s).'
|
40
43
|
end
|
41
44
|
end
|
42
45
|
end
|
@@ -13,6 +13,7 @@ module Osso
|
|
13
13
|
field :delete_enterprise_account, mutation: Mutations::DeleteEnterpriseAccount
|
14
14
|
field :delete_identity_provider, mutation: Mutations::DeleteIdentityProvider
|
15
15
|
field :delete_oauth_client, mutation: Mutations::DeleteOauthClient
|
16
|
+
field :invite_admin_user, mutation: Mutations::InviteAdminUser
|
16
17
|
field :set_redirect_uris, mutation: Mutations::SetRedirectUris
|
17
18
|
field :regenerate_oauth_credentials, mutation: Mutations::RegenerateOauthCredentials
|
18
19
|
field :update_app_config, mutation: Mutations::UpdateAppConfig
|
@@ -13,6 +13,7 @@ require_relative 'mutations/create_oauth_client'
|
|
13
13
|
require_relative 'mutations/delete_enterprise_account'
|
14
14
|
require_relative 'mutations/delete_identity_provider'
|
15
15
|
require_relative 'mutations/delete_oauth_client'
|
16
|
+
require_relative 'mutations/invite_admin_user'
|
16
17
|
require_relative 'mutations/regenerate_oauth_credentials'
|
17
18
|
require_relative 'mutations/set_redirect_uris'
|
18
19
|
require_relative 'mutations/update_app_config'
|
@@ -8,24 +8,17 @@ module Osso
|
|
8
8
|
|
9
9
|
argument :domain, String, required: true
|
10
10
|
argument :name, String, required: true
|
11
|
-
argument :oauth_client_id, String, required: false
|
12
11
|
|
13
12
|
field :enterprise_account, Types::EnterpriseAccount, null: false
|
14
13
|
field :errors, [String], null: false
|
15
14
|
|
16
15
|
def resolve(**args)
|
17
16
|
enterprise_account = Osso::Models::EnterpriseAccount.new(args)
|
18
|
-
enterprise_account.oauth_client_id ||= find_client_db_id(context[:oauth_client_id])
|
19
17
|
|
20
18
|
return response_data(enterprise_account: enterprise_account) if enterprise_account.save
|
21
19
|
|
22
20
|
response_error(enterprise_account.errors)
|
23
21
|
end
|
24
|
-
|
25
|
-
def find_client_db_id(oauth_client_identifier)
|
26
|
-
Osso::Models::OauthClient.find_by(identifier: oauth_client_identifier).
|
27
|
-
id
|
28
|
-
end
|
29
22
|
end
|
30
23
|
end
|
31
24
|
end
|
@@ -8,17 +8,18 @@ module Osso
|
|
8
8
|
|
9
9
|
argument :enterprise_account_id, ID, required: true
|
10
10
|
argument :service, Types::IdentityProviderService, required: false
|
11
|
+
argument :oauth_client_id, String, required: true
|
11
12
|
|
12
13
|
field :identity_provider, Types::IdentityProvider, null: false
|
13
14
|
field :errors, [String], null: false
|
14
15
|
|
15
|
-
def resolve(service: nil,
|
16
|
-
customer = enterprise_account(
|
16
|
+
def resolve(service: nil, enterprise_account_id:, oauth_client_id:)
|
17
|
+
customer = enterprise_account(enterprise_account_id: enterprise_account_id)
|
17
18
|
|
18
19
|
identity_provider = customer.identity_providers.build(
|
19
20
|
service: service,
|
20
21
|
domain: customer.domain,
|
21
|
-
oauth_client_id:
|
22
|
+
oauth_client_id: oauth_client_id,
|
22
23
|
)
|
23
24
|
|
24
25
|
return response_data(identity_provider: identity_provider) if identity_provider.save
|
@@ -26,11 +27,11 @@ module Osso
|
|
26
27
|
response_error(identity_provider.errors)
|
27
28
|
end
|
28
29
|
|
29
|
-
def domain(**args)
|
30
|
-
enterprise_account(
|
30
|
+
def domain(enterprise_account_id:, **args)
|
31
|
+
enterprise_account(enterprise_account_id: enterprise_account_id)&.domain
|
31
32
|
end
|
32
33
|
|
33
|
-
def enterprise_account(enterprise_account_id
|
34
|
+
def enterprise_account(enterprise_account_id:)
|
34
35
|
@enterprise_account ||= Osso::Models::EnterpriseAccount.find(enterprise_account_id)
|
35
36
|
end
|
36
37
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module GraphQL
|
5
|
+
module Mutations
|
6
|
+
class InviteAdminUser < BaseMutation
|
7
|
+
null false
|
8
|
+
|
9
|
+
argument :email, String, required: true
|
10
|
+
argument :oauth_client_id, ID, required: false
|
11
|
+
argument :role, String, required: true
|
12
|
+
|
13
|
+
field :admin_user, Types::AdminUser, null: true
|
14
|
+
field :errors, [String], null: false
|
15
|
+
|
16
|
+
def resolve(email:, role:, oauth_client_id: nil)
|
17
|
+
admin_user = Osso::Models::Account.new(
|
18
|
+
email: email,
|
19
|
+
role: role,
|
20
|
+
oauth_client_id: oauth_client_id,
|
21
|
+
)
|
22
|
+
|
23
|
+
if admin_user.save
|
24
|
+
verify_user(email)
|
25
|
+
|
26
|
+
return response_data(admin_user: admin_user)
|
27
|
+
end
|
28
|
+
|
29
|
+
response_error(admin_user.errors)
|
30
|
+
end
|
31
|
+
|
32
|
+
def ready?(*)
|
33
|
+
admin_ready?
|
34
|
+
end
|
35
|
+
|
36
|
+
def verify_user(email)
|
37
|
+
context[:rodauth].account_from_login(email)
|
38
|
+
context[:rodauth].setup_account_verification
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/osso/graphql/query.rb
CHANGED
@@ -5,6 +5,7 @@ module Osso
|
|
5
5
|
module Types
|
6
6
|
class QueryType < ::GraphQL::Schema::Object
|
7
7
|
field :enterprise_accounts, null: true, resolver: Resolvers::EnterpriseAccounts do
|
8
|
+
argument :search, String, required: false
|
8
9
|
argument :sort_column, String, required: false
|
9
10
|
argument :sort_order, String, required: false
|
10
11
|
end
|
@@ -40,6 +41,13 @@ module Osso
|
|
40
41
|
argument :id, ID, required: true
|
41
42
|
end
|
42
43
|
|
44
|
+
field(
|
45
|
+
:admin_users,
|
46
|
+
[Types::AdminUser],
|
47
|
+
null: false,
|
48
|
+
resolve: ->(_obj, _args, _context) { Osso::Models::Account.all },
|
49
|
+
)
|
50
|
+
|
43
51
|
field(
|
44
52
|
:current_user,
|
45
53
|
Types::AdminUser,
|
@@ -6,11 +6,11 @@ module Osso
|
|
6
6
|
class EnterpriseAccounts < BaseResolver
|
7
7
|
type Types::EnterpriseAccount.connection_type, null: true
|
8
8
|
|
9
|
-
def resolve(sort_column: nil, sort_order: nil)
|
9
|
+
def resolve(sort_column: nil, sort_order: nil, search: nil)
|
10
10
|
return Array(Osso::Models::EnterpriseAccount.find_by(domain: context_domain)) unless internal_authorized?
|
11
11
|
|
12
12
|
accounts = Osso::Models::EnterpriseAccount
|
13
|
-
|
13
|
+
accounts = accounts.where('domain ilike ? OR name ilike ?', "%#{search}%", "%#{search}%") if search
|
14
14
|
accounts = accounts.order(sort_column.underscore => sort_order_sym(sort_order)) if sort_column && sort_order
|
15
15
|
|
16
16
|
accounts.all
|
data/lib/osso/graphql/types.rb
CHANGED
@@ -14,8 +14,8 @@ require_relative 'types/app_config'
|
|
14
14
|
require_relative 'types/error'
|
15
15
|
require_relative 'types/identity_provider_service'
|
16
16
|
require_relative 'types/identity_provider_status'
|
17
|
+
require_relative 'types/redirect_uri'
|
18
|
+
require_relative 'types/oauth_client'
|
17
19
|
require_relative 'types/identity_provider'
|
18
20
|
require_relative 'types/enterprise_account'
|
19
|
-
require_relative 'types/redirect_uri'
|
20
21
|
require_relative 'types/redirect_uri_input'
|
21
|
-
require_relative 'types/oauth_client'
|
@@ -11,11 +11,20 @@ module Osso
|
|
11
11
|
field :id, ID, null: false
|
12
12
|
field :email, String, null: false
|
13
13
|
field :scope, String, null: false
|
14
|
+
field :role, String, null: false
|
14
15
|
field :oauth_client_id, ID, null: true
|
15
16
|
|
16
17
|
def self.authorized?(_object, _context)
|
17
18
|
true
|
18
19
|
end
|
20
|
+
|
21
|
+
def created_at
|
22
|
+
12.hours.ago
|
23
|
+
end
|
24
|
+
|
25
|
+
def updated_at
|
26
|
+
12.hours.ago
|
27
|
+
end
|
19
28
|
end
|
20
29
|
end
|
21
30
|
end
|
@@ -28,7 +28,7 @@ module Osso
|
|
28
28
|
def self.authorized?(object, context)
|
29
29
|
# we first receive the payload object as a hash, but can depend on the
|
30
30
|
# return type to hide the actual objects non-admins shouldn't see
|
31
|
-
return true if object.
|
31
|
+
return true if object.instance_of?(Hash)
|
32
32
|
|
33
33
|
internal_authorized?(context) || enterprise_authorized?(context, object&.domain)
|
34
34
|
end
|
@@ -13,10 +13,12 @@ module Osso
|
|
13
13
|
field :service, Types::IdentityProviderService, null: true
|
14
14
|
field :domain, String, null: false
|
15
15
|
field :acs_url, String, null: false
|
16
|
+
field :sso_issuer, String, null: false
|
16
17
|
field :sso_url, String, null: true
|
17
18
|
field :sso_cert, String, null: true
|
18
19
|
field :status, Types::IdentityProviderStatus, null: false
|
19
20
|
field :acs_url_validator, String, null: false
|
21
|
+
field :oauth_client, Types::OauthClient, null: false
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|
@@ -5,9 +5,10 @@ module Osso
|
|
5
5
|
module Types
|
6
6
|
class IdentityProviderService < BaseEnum
|
7
7
|
value('AZURE', 'Microsoft Azure Identity Provider', value: 'AZURE')
|
8
|
+
value('GOOGLE', 'Google SAML Identity Provider', value: 'GOOGLE')
|
8
9
|
value('OKTA', 'Okta Identity Provider', value: 'OKTA')
|
9
10
|
value('ONELOGIN', 'OneLogin Identity Provider', value: 'ONELOGIN')
|
10
|
-
value('
|
11
|
+
value('PING', 'PingID Identity Provider', value: 'PING')
|
11
12
|
end
|
12
13
|
end
|
13
14
|
end
|
data/lib/osso/lib/app_config.rb
CHANGED
@@ -7,7 +7,7 @@ module Osso
|
|
7
7
|
def self.included(klass)
|
8
8
|
klass.class_eval do
|
9
9
|
use Rack::JSONBodyParser
|
10
|
-
use Rack::Session::Cookie, secret: ENV
|
10
|
+
use Rack::Session::Cookie, secret: ENV.fetch('SESSION_SECRET')
|
11
11
|
|
12
12
|
error ActiveRecord::RecordNotFound do
|
13
13
|
status 404
|
data/lib/osso/lib/route_map.rb
CHANGED
@@ -1,29 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/MethodLength
|
4
|
-
|
5
3
|
module Osso
|
6
4
|
module RouteMap
|
7
5
|
def self.included(klass)
|
8
6
|
klass.class_eval do
|
9
|
-
|
10
7
|
use Osso::Admin
|
11
8
|
use Osso::Auth
|
12
9
|
use Osso::Oauth
|
13
|
-
|
14
|
-
post '/graphql' do
|
15
|
-
token_protected!
|
16
|
-
|
17
|
-
result = Osso::GraphQL::Schema.execute(
|
18
|
-
params[:query],
|
19
|
-
variables: params[:variables],
|
20
|
-
context: current_user.symbolize_keys,
|
21
|
-
)
|
22
|
-
|
23
|
-
json result
|
24
|
-
end
|
25
10
|
end
|
26
11
|
end
|
27
12
|
end
|
28
13
|
end
|
29
|
-
# rubocop:enable Metrics/MethodLength
|
@@ -55,6 +55,7 @@ module Osso
|
|
55
55
|
@authorization_code ||= user.authorization_codes.create!(
|
56
56
|
oauth_client: provider.oauth_client,
|
57
57
|
redirect_uri: redirect_uri_base,
|
58
|
+
requested: requested_param,
|
58
59
|
)
|
59
60
|
end
|
60
61
|
|
@@ -81,5 +82,9 @@ module Osso
|
|
81
82
|
def valid_idp_initiated_flow
|
82
83
|
!session[:osso_oauth_redirect_uri] && !session[:osso_oauth_state]
|
83
84
|
end
|
85
|
+
|
86
|
+
def requested_param
|
87
|
+
@session.delete(:osso_oauth_requested)
|
88
|
+
end
|
84
89
|
end
|
85
90
|
end
|
@@ -39,9 +39,11 @@ end
|
|
39
39
|
# updated_at :datetime not null
|
40
40
|
# user_id :uuid
|
41
41
|
# oauth_client_id :uuid
|
42
|
+
# requested :jsonb
|
42
43
|
#
|
43
44
|
# Indexes
|
44
45
|
#
|
45
|
-
# index_access_tokens_on_oauth_client_id
|
46
|
-
#
|
46
|
+
# index_access_tokens_on_oauth_client_id (oauth_client_id)
|
47
|
+
# index_access_tokens_on_token_and_expires_at (token,expires_at) UNIQUE
|
48
|
+
# index_access_tokens_on_user_id (user_id)
|
47
49
|
#
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Osso
|
4
|
+
module Models
|
5
|
+
class Account < ::ActiveRecord::Base
|
6
|
+
enum status_id: { 1 => :Unverified, 2 => :Verified, 3 => :Closed }
|
7
|
+
|
8
|
+
def context
|
9
|
+
{
|
10
|
+
email: email,
|
11
|
+
id: id,
|
12
|
+
scope: role,
|
13
|
+
oauth_client_id: oauth_client_id,
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# == Schema Information
|
21
|
+
#
|
22
|
+
# Table name: accounts
|
23
|
+
#
|
24
|
+
# id :uuid not null, primary key
|
25
|
+
# email :citext not null
|
26
|
+
# status_id :integer default(NULL), not null
|
27
|
+
# role :string default("admin"), not null
|
28
|
+
# oauth_client_id :string
|
29
|
+
#
|
30
|
+
# Indexes
|
31
|
+
#
|
32
|
+
# index_accounts_on_email (email) UNIQUE WHERE (status_id = ANY (ARRAY[1, 2]))
|
33
|
+
# index_accounts_on_oauth_client_id (oauth_client_id)
|
34
|
+
#
|
@@ -7,7 +7,7 @@ module Osso
|
|
7
7
|
|
8
8
|
def access_token
|
9
9
|
@access_token ||= expired! &&
|
10
|
-
user.access_tokens.create(oauth_client: oauth_client)
|
10
|
+
user.access_tokens.create(oauth_client: oauth_client, requested: requested)
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -25,6 +25,7 @@ end
|
|
25
25
|
# updated_at :datetime not null
|
26
26
|
# user_id :uuid
|
27
27
|
# oauth_client_id :uuid
|
28
|
+
# requested :jsonb
|
28
29
|
#
|
29
30
|
# Indexes
|
30
31
|
#
|
@@ -8,10 +8,11 @@ module Osso
|
|
8
8
|
# includes fields for external IDs such that you can persist
|
9
9
|
# your ID for an account in your Osso instance.
|
10
10
|
class EnterpriseAccount < ActiveRecord::Base
|
11
|
-
belongs_to :oauth_client
|
12
11
|
has_many :users
|
13
12
|
has_many :identity_providers
|
14
13
|
|
14
|
+
validates_format_of :domain, with: /\A[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,5}\z/
|
15
|
+
|
15
16
|
def single_provider?
|
16
17
|
identity_providers.not_pending.one?
|
17
18
|
end
|
@@ -40,6 +41,7 @@ end
|
|
40
41
|
# name :string not null
|
41
42
|
# created_at :datetime not null
|
42
43
|
# updated_at :datetime not null
|
44
|
+
# users_count :integer default(0)
|
43
45
|
#
|
44
46
|
# Indexes
|
45
47
|
#
|