punk 0.0.3 → 0.2.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.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +9 -0
  3. data/.github/workflows/ship.yml +28 -0
  4. data/.github/workflows/test.yml +45 -0
  5. data/.rdoc_options +23 -0
  6. data/.rgignore +1 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +243 -0
  9. data/Gemfile +6 -6
  10. data/Gemfile.lock +18 -30
  11. data/README.md +8 -0
  12. data/Rakefile +7 -9
  13. data/VERSION +1 -1
  14. data/app/migrations/001_lets_punk.rb +3 -0
  15. data/app/routes/hello.rb +4 -0
  16. data/bin/punk +0 -1
  17. data/env/.gitignore +3 -0
  18. data/env/spec/test.sh +3 -0
  19. data/env/test.sh +5 -0
  20. data/lib/punk/actions/.keep +0 -0
  21. data/lib/punk/actions/groups/list.rb +24 -0
  22. data/lib/punk/actions/sessions/clear.rb +21 -0
  23. data/lib/punk/actions/sessions/create.rb +64 -0
  24. data/lib/punk/actions/sessions/list.rb +18 -0
  25. data/lib/punk/actions/sessions/verify.rb +24 -0
  26. data/lib/punk/actions/tenants/list.rb +18 -0
  27. data/lib/punk/actions/users/list_group.rb +18 -0
  28. data/lib/punk/actions/users/list_tenant.rb +18 -0
  29. data/lib/punk/actions/users/show.rb +18 -0
  30. data/lib/punk/commands/http.rb +3 -3
  31. data/lib/punk/commands/list.rb +12 -6
  32. data/lib/punk/config/defaults.json +3 -0
  33. data/lib/punk/config/schema.json +3 -0
  34. data/lib/punk/core/app.rb +6 -8
  35. data/lib/punk/core/commander.rb +9 -6
  36. data/lib/punk/core/exec.rb +2 -0
  37. data/lib/punk/core/load.rb +0 -1
  38. data/lib/punk/framework/command.rb +5 -1
  39. data/lib/punk/framework/plugins/validation.rb +0 -14
  40. data/lib/punk/framework/runnable.rb +1 -1
  41. data/lib/punk/helpers/loggable.rb +1 -1
  42. data/lib/punk/migrations/001_punk.rb +103 -0
  43. data/lib/punk/models/.keep +0 -0
  44. data/lib/punk/models/group.rb +20 -0
  45. data/lib/punk/models/group_user_metadata.rb +17 -0
  46. data/lib/punk/models/identity.rb +29 -0
  47. data/lib/punk/models/session.rb +89 -0
  48. data/lib/punk/models/tenant.rb +19 -0
  49. data/lib/punk/models/tenant_user_metadata.rb +17 -0
  50. data/lib/punk/models/user.rb +31 -0
  51. data/lib/punk/routes/groups.rb +31 -0
  52. data/lib/punk/routes/plivo.rb +4 -0
  53. data/lib/punk/routes/sessions.rb +108 -0
  54. data/lib/punk/routes/swagger.rb +9 -0
  55. data/lib/punk/routes/tenants.rb +29 -0
  56. data/lib/punk/routes/users.rb +36 -0
  57. data/lib/punk/services/.keep +0 -0
  58. data/lib/punk/services/challenge_claim.rb +46 -0
  59. data/lib/punk/services/create_identities.rb +25 -0
  60. data/lib/punk/services/generate_swagger.rb +25 -0
  61. data/lib/punk/services/prove_claim.rb +29 -0
  62. data/lib/punk/services/secret.rb +9 -0
  63. data/lib/punk/templates/groups/list.jbuilder +7 -0
  64. data/lib/punk/templates/plivo.slim +16 -0
  65. data/lib/punk/templates/sessions/list.jbuilder +6 -0
  66. data/lib/punk/templates/sessions/pending.jbuilder +4 -0
  67. data/lib/punk/templates/tenants/list.jbuilder +7 -0
  68. data/lib/punk/templates/tenants/list.slim +8 -0
  69. data/lib/punk/templates/users/list.jbuilder +7 -0
  70. data/lib/punk/templates/users/list.rcsv +4 -0
  71. data/lib/punk/templates/users/show.jbuilder +5 -0
  72. data/lib/punk/views/groups/list.rb +22 -0
  73. data/lib/punk/views/plivo_store.rb +15 -0
  74. data/lib/punk/views/sessions/list.rb +22 -0
  75. data/lib/punk/views/sessions/pending.rb +28 -0
  76. data/lib/punk/views/tenants/list.rb +22 -0
  77. data/lib/punk/views/users/list.rb +22 -0
  78. data/lib/punk/views/users/show.rb +22 -0
  79. data/lib/punk/workers/.keep +0 -0
  80. data/lib/punk/workers/expire_sessions.rb +9 -0
  81. data/lib/punk/workers/geocode_session_worker.rb +48 -0
  82. data/lib/punk/workers/identify_session_worker.rb +45 -0
  83. data/lib/punk/workers/secret.rb +18 -0
  84. data/lib/punk/workers/send_email_worker.rb +51 -0
  85. data/lib/punk/workers/send_sms_worker.rb +40 -0
  86. data/punk.gemspec +149 -16
  87. data/schema.psql +345 -0
  88. data/spec/actions/groups/punk/list_groups_action_spec.rb +36 -0
  89. data/spec/actions/sessions/punk/clear_session_action_spec.rb +29 -0
  90. data/spec/actions/sessions/punk/create_session_action_spec.rb +33 -0
  91. data/spec/actions/sessions/punk/list_sessions_action_spec.rb +26 -0
  92. data/spec/actions/sessions/punk/verify_session_action_spec.rb +59 -0
  93. data/spec/actions/tenants/punk/list_tenants_action_spec.rb +25 -0
  94. data/spec/actions/users/punk/list_group_users_action_spec.rb +26 -0
  95. data/spec/actions/users/punk/list_tenant_users_action_spec.rb +26 -0
  96. data/spec/factories/group.rb +12 -0
  97. data/spec/factories/group_user_metadata.rb +10 -0
  98. data/spec/factories/identity.rb +19 -0
  99. data/spec/factories/session.rb +12 -0
  100. data/spec/factories/tenant.rb +10 -0
  101. data/spec/factories/tenant_user_metadata.rb +10 -0
  102. data/spec/factories/user.rb +12 -0
  103. data/spec/lib/commands/auth_spec.rb +11 -0
  104. data/spec/lib/commands/generate_spec.rb +7 -0
  105. data/spec/lib/commands/http_spec.rb +23 -0
  106. data/spec/lib/commands/list_spec.rb +7 -0
  107. data/spec/lib/commands/swagger_spec.rb +7 -0
  108. data/spec/lib/engine/punk_env_spec.rb +13 -0
  109. data/spec/lib/engine/punk_exec_spec.rb +9 -0
  110. data/spec/lib/engine/punk_init_spec.rb +9 -0
  111. data/spec/lib/engine/punk_store_spec.rb +10 -0
  112. data/spec/lib/punk.env +7 -0
  113. data/spec/models/punk/group_spec.rb +50 -0
  114. data/spec/models/punk/group_user_metadata_spec.rb +61 -0
  115. data/spec/models/punk/identity_spec.rb +61 -0
  116. data/spec/models/punk/session_spec.rb +156 -0
  117. data/spec/models/punk/tenant_spec.rb +51 -0
  118. data/spec/models/punk/tenant_user_metadata_spec.rb +61 -0
  119. data/spec/models/punk/user_spec.rb +115 -0
  120. data/spec/routes/groups/get_groups_spec.rb +33 -0
  121. data/spec/routes/plivo/get_plivo_spec.rb +11 -0
  122. data/spec/routes/sessions/delete_session_spec.rb +11 -0
  123. data/spec/routes/sessions/get_sessions_spec.rb +30 -0
  124. data/spec/routes/sessions/patch_session_spec.rb +11 -0
  125. data/spec/routes/sessions/post_session_spec.rb +11 -0
  126. data/spec/routes/swagger/get_swagger_spec.rb +12 -0
  127. data/spec/routes/tenants/get_tenants_spec.rb +31 -0
  128. data/spec/routes/users/get_users_spec.rb +60 -0
  129. data/spec/services/punk/challenge_claim_service_spec.rb +7 -0
  130. data/spec/services/punk/create_identities_service_spec.rb +14 -0
  131. data/spec/services/punk/generate_swagger_service_spec.rb +7 -0
  132. data/spec/services/punk/prove_claim_service_spec.rb +7 -0
  133. data/spec/services/punk/secret_service_spec.rb +7 -0
  134. data/spec/spec_helper.rb +122 -0
  135. data/spec/vcr_cassettes/PUNK_GeocodeSessionWorker/updates_the_session_data.yml +57 -0
  136. data/spec/vcr_cassettes/PUNK_IdentifySessionWorker/updates_the_session_data.yml +112 -0
  137. data/spec/views/punk/plivo_store_spec.rb +7 -0
  138. data/spec/views/sessions/punk/list_sessions_view_spec.rb +7 -0
  139. data/spec/views/sessions/punk/pending_session_view_spec.rb +7 -0
  140. data/spec/views/tenants/punk/list_tenants_view_spec.rb +7 -0
  141. data/spec/views/users/punk/list_groups_view_spec.rb +7 -0
  142. data/spec/views/users/punk/list_users_view_spec.rb +7 -0
  143. data/spec/workers/punk/expire_sessions_worker_spec.rb +31 -0
  144. data/spec/workers/punk/geocode_session_worker_spec.rb +14 -0
  145. data/spec/workers/punk/identify_session_worker_spec.rb +15 -0
  146. data/spec/workers/punk/secret_worker_spec.rb +20 -0
  147. data/spec/workers/punk/send_email_worker_spec.rb +46 -0
  148. data/spec/workers/punk/send_sms_worker_spec.rb +33 -0
  149. metadata +169 -13
  150. data/lib/punk/views/all.rb +0 -4
@@ -6,11 +6,11 @@ command :test do |c|
6
6
  say('Running tests...')
7
7
  unless ENV['PUNK_ENV'] == 'test'
8
8
  error('!!! PUNK_ENV should be test !!!')
9
- exit 1
9
+ exit 1 # rubocop:disable Rails/Exit
10
10
  end
11
11
  ENV.delete_if { |name, _value| name =~ /^PUNK_/ }
12
12
  system('rubocop') &&
13
- # system('quasar build -m pwa') && TODO
13
+ # system('quasar build -m pwa') && TODO
14
14
  system('PUNK_ENV=test rspec')
15
15
  exit $CHILD_STATUS.exitstatus # rubocop:disable Rails/Exit
16
16
  end
@@ -74,10 +74,13 @@ command 'db migrate' do |c|
74
74
  say('Migrating db...')
75
75
  PUNK.boot
76
76
  Sequel.extension :migration
77
- if options.relative.nil?
78
- Sequel::Migrator.run(PUNK.db, File.join(PUNK.get.app.path, 'migrations'))
79
- else
80
- Sequel::Migrator.run(PUNK.db, File.join(PUNK.get.app.path, 'migrations'), relative: options.relative)
77
+ migrations_path = File.join(PUNK.get.app.path, 'migrations')
78
+ if File.exist?(migrations_path)
79
+ if options.relative.nil?
80
+ Sequel::Migrator.run(PUNK.db, migrations_path)
81
+ else
82
+ Sequel::Migrator.run(PUNK.db, migrations_path, relative: options.relative)
83
+ end
81
84
  end
82
85
  database = File.basename(PUNK.get.db.url)
83
86
  `pg_dump #{database} --schema-only > schema.psql`
@@ -23,6 +23,7 @@ end
23
23
 
24
24
  PUNK::Interface.register(:app) do
25
25
  require_relative 'app'
26
+ PUNK.require_all(File.join(__dir__, '..', 'routes'))
26
27
  PUNK.require_all(File.join(PUNK.get.app.path, 'routes'))
27
28
  retval = PUNK.get.app.reloadable ? PUNK.loader : PUNK::App.freeze.app
28
29
  SemanticLogger.flush
@@ -32,6 +33,7 @@ end
32
33
  PUNK.inject :loader, :app
33
34
 
34
35
  ['actions', 'models', 'views', 'services', 'workers'].each do |dir|
36
+ PUNK.require_all(File.join(__dir__, '..', dir))
35
37
  PUNK.require_all(File.join(PUNK.get.app.path, dir))
36
38
  end
37
39
 
@@ -4,6 +4,5 @@ PUNK.db
4
4
 
5
5
  require_relative '../helpers/all'
6
6
  require_relative '../framework/all'
7
- require_relative '../views/all'
8
7
 
9
8
  PUNK.store[:state] = :loaded
@@ -71,7 +71,11 @@ module PUNK
71
71
  define_method(:options) do |opt|
72
72
  punk_command = PUNK.store.commands[match]
73
73
  punk_command.instance_variable_get(:@options).each_value do |option|
74
- opt.on option[:shortcut], option[:name], option[:description], argument: true, as: option[:type]
74
+ if option[:type].present?
75
+ opt.on option[:shortcut], option[:name], option[:description], argument: true, as: option[:type]
76
+ else
77
+ opt.on option[:shortcut], option[:name], option[:description]
78
+ end
75
79
  end
76
80
  end
77
81
  define_method(:process) do
@@ -28,20 +28,6 @@ module PUNK
28
28
  end
29
29
  end
30
30
 
31
- def validates_parse_id(atts, opts={})
32
- default = { message: "is not a Parse ID" }
33
- validatable_attributes(atts, default.merge(opts)) do |_name, value, message|
34
- message unless /^[[:alnum:]]{10}$/.match(value)
35
- end
36
- end
37
-
38
- def validates_subdomain(atts, opts={})
39
- default = { message: "is not a subdomain" }
40
- validatable_attributes(atts, default.merge(opts)) do |_name, value, message|
41
- message unless /^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/.match(value)
42
- end
43
- end
44
-
45
31
  def validates_state(name, state)
46
32
  errors.add(name, "is not in #{state} state") unless self[name].send("#{state}?")
47
33
  end
@@ -12,7 +12,7 @@ module PUNK
12
12
 
13
13
  def method_missing(key, *args, &block)
14
14
  val = super
15
- val = val.to_h if val.class == self.class || val.class.instance_of?(self.class)
15
+ val = val.to_h if val.instance_of?(self.class)
16
16
  val
17
17
  end
18
18
 
@@ -22,7 +22,7 @@ module PUNK
22
22
 
23
23
  def exception(e, extra={})
24
24
  if ENV.key?('SENTRY_DSN')
25
- ::Raven.capture_exception(
25
+ ::Sentry.capture_exception(
26
26
  e,
27
27
  message: e.message,
28
28
  extra: extra,
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ def _tenants
4
+ create_table :tenants do
5
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
6
+ punk_state :state, null: false, default: 'created'
7
+ String :name, null: false, text: true
8
+ String :icon, text: true
9
+ jsonb :data, default: '{}'
10
+ DateTime :created_at
11
+ DateTime :updated_at
12
+ end
13
+ end
14
+
15
+ def _users
16
+ create_table :users do
17
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
18
+ punk_state :state, null: false, default: 'created'
19
+ String :name, null: false, text: true
20
+ String :icon, text: true
21
+ String :email, text: true, unique: true
22
+ String :phone, text: true, unique: true
23
+ jsonb :data, default: '{}'
24
+ DateTime :created_at
25
+ DateTime :updated_at
26
+ end
27
+ end
28
+
29
+ def _tenants_users
30
+ create_table :tenants_users do
31
+ primary_key [:tenant_id, :user_id]
32
+ foreign_key :tenant_id, :tenants, null: false, type: :uuid
33
+ foreign_key :user_id, :users, null: false, type: :uuid
34
+ index [:tenant_id, :user_id]
35
+ end
36
+ end
37
+
38
+ def _groups
39
+ create_table :groups do
40
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
41
+ punk_state :state, null: false, default: 'created'
42
+ String :name, null: false, text: true
43
+ String :icon, text: true
44
+ jsonb :data, default: '{}'
45
+ DateTime :created_at
46
+ DateTime :updated_at
47
+ foreign_key :tenant_id, :tenants, null: false, type: :uuid
48
+ end
49
+ end
50
+
51
+ def _groups_users
52
+ create_table :groups_users do
53
+ primary_key [:group_id, :user_id]
54
+ foreign_key :group_id, :groups, null: false, type: :uuid
55
+ foreign_key :user_id, :users, null: false, type: :uuid
56
+ index [:group_id, :user_id]
57
+ end
58
+ end
59
+
60
+ def _identities
61
+ create_enum(:claim_type, %w[email phone])
62
+ create_table :identities do
63
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
64
+ punk_state :state, null: false, default: 'created'
65
+ claim_type :claim_type, null: false
66
+ String :claim, text: true, null: false, unique: true
67
+ jsonb :data, default: '{}'
68
+ DateTime :created_at
69
+ DateTime :updated_at
70
+ foreign_key :user_id, :users, null: true, type: :uuid
71
+ end
72
+ end
73
+
74
+ def _sessions
75
+ create_enum(:session_state, %w[pending created active deleted expired])
76
+ create_table :sessions do
77
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
78
+ uuid :slug, default: Sequel.function(:gen_random_uuid)
79
+ session_state :state, null: false, default: 'created'
80
+ File :salt, text: true
81
+ File :hash, text: true
82
+ Integer :attempt_count, null: false, default: 0
83
+ cidr :remote_addr, null: false, default: '127.0.0.1'
84
+ String :user_agent, text: true, null: false, default: 'Mozilla/5.0 (compatible; Punk!; +https://punk.kranzky.com)'
85
+ jsonb :data, default: '{}'
86
+ DateTime :created_at
87
+ DateTime :updated_at
88
+ foreign_key :identity_id, :identities, null: false, type: :uuid
89
+ end
90
+ end
91
+
92
+ PUNK.migration do
93
+ change do
94
+ create_enum(:punk_state, %w[created active deleted])
95
+ _tenants
96
+ _users
97
+ _tenants_users
98
+ _groups
99
+ _groups_users
100
+ _identities
101
+ _sessions
102
+ end
103
+ end
File without changes
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ # @model
5
+ # @property id(required) [string] a unique identifier for the group
6
+ # @property name(required) [string] the name of the group
7
+ # @property icon(required) [string] an image URL
8
+ class Group < PUNK::Model
9
+ alias to_s name
10
+
11
+ many_to_one :tenant
12
+ many_to_many :users
13
+
14
+ def validate
15
+ validates_presence :tenant
16
+ validates_presence :name
17
+ validates_url :icon, allow_blank: true
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ class GroupUserMetadata < PUNK::Model(:groups_users)
5
+ many_to_one :group
6
+ many_to_one :user
7
+
8
+ def validate
9
+ validates_presence :group
10
+ validates_presence :user
11
+ end
12
+
13
+ def to_s
14
+ "#{group_id}|#{user_id}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ class Identity < PUNK::Model
5
+ alias to_s claim
6
+
7
+ many_to_one :user
8
+ one_to_many :sessions
9
+
10
+ symbolize :claim_type
11
+
12
+ def validate
13
+ validates_presence :claim
14
+ validates_presence :claim_type
15
+ validates_unique :claim
16
+ validates_includes [:email, :phone], :claim_type
17
+ validates_email :claim if email?
18
+ validates_phone :claim if phone?
19
+ end
20
+
21
+ def email?
22
+ claim_type == :email
23
+ end
24
+
25
+ def phone?
26
+ claim_type == :phone
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ # @model
5
+ # @property slug(required) [string] a unique identifier for the session while it is being challenged
6
+ # @property message(required) [string] a message to be displayed to the user to let them know what to do
7
+ class Session < PUNK::Model
8
+ alias to_s state
9
+
10
+ many_to_one :identity
11
+ one_through_one :user, join_table: :identities, left_key: :id, left_primary_key: :identity_id
12
+
13
+ symbolize :state
14
+
15
+ aasm :state do
16
+ state :created, initial: true
17
+ state :pending
18
+ state :active
19
+ state :expired
20
+ state :deleted
21
+
22
+ event :challenge do
23
+ transitions from: :created, to: :pending, guard: :current?
24
+ end
25
+
26
+ event :verify, after: :erase do
27
+ transitions from: :pending, to: :active, guard: :current?
28
+ end
29
+
30
+ event :timeout, after: :erase do
31
+ transitions from: [:created, :pending, :active], to: :expired
32
+ end
33
+
34
+ event :clear, after: :erase do
35
+ transitions from: :active, to: :deleted
36
+ end
37
+ end
38
+
39
+ dataset_module do
40
+ def created
41
+ where(state: 'created')
42
+ end
43
+
44
+ def pending
45
+ where(state: 'pending')
46
+ end
47
+
48
+ def active
49
+ where(state: 'active')
50
+ end
51
+
52
+ def expired
53
+ where(state: 'expired')
54
+ end
55
+
56
+ def deleted
57
+ where(state: 'deleted')
58
+ end
59
+
60
+ def expiring
61
+ where { Sequel.&({ state: ['created', 'pending'] }, (created_at < 5.minutes.ago)) }.or { Sequel.&({ state: 'active' }, ((updated_at < 1.month.ago) | (created_at < 1.year.ago))) }
62
+ end
63
+ end
64
+
65
+ def validate
66
+ validates_presence :identity
67
+ validates_includes [:created, :pending, :active, :expired, :deleted], :state
68
+ validates_integer :attempt_count
69
+ validates_includes [0, 1, 2, 3], :attempt_count
70
+ end
71
+
72
+ def current?
73
+ !timeout?
74
+ end
75
+
76
+ def timeout?
77
+ timeout! if (created? || pending?) && created_at < 5.minutes.ago || active? && (updated_at < 1.month.ago || created_at < 1.year.ago)
78
+ expired?
79
+ end
80
+
81
+ def erase
82
+ update(slug: nil, salt: nil, hash: nil)
83
+ end
84
+
85
+ def increment_attempts
86
+ update(attempt_count: attempt_count + 1)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ # @model
5
+ # @property id(required) [string] a unique identifier for the tenant
6
+ # @property name(required) [string] the name of the tenant
7
+ # @property icon(required) [string] an image URL
8
+ class Tenant < PUNK::Model
9
+ alias to_s name
10
+
11
+ many_to_many :users
12
+ one_to_many :groups
13
+
14
+ def validate
15
+ validates_presence :name
16
+ validates_url :icon, allow_blank: true
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ class TenantUserMetadata < PUNK::Model(:tenants_users)
5
+ many_to_one :tenant
6
+ many_to_one :user
7
+
8
+ def validate
9
+ validates_presence :tenant
10
+ validates_presence :user
11
+ end
12
+
13
+ def to_s
14
+ "#{tenant_id}|#{user_id}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ # @model
5
+ # @property id(required) [string] a unique identifier for the user
6
+ # @property name(required) [string] the name of the user
7
+ # @property icon(required) [string] an image URL
8
+ class User < Model
9
+ alias to_s name
10
+
11
+ many_to_many :tenants
12
+ many_to_many :groups
13
+ one_to_many :identities
14
+ many_through_many :sessions, through: [[:identities, :user_id, :id], [:sessions, :identity_id, :id]]
15
+
16
+ def validate
17
+ validates_presence :name
18
+ validates_url :icon, allow_blank: true
19
+ validates_presence :email if phone.blank?
20
+ validates_presence :phone if email.blank?
21
+ validates_email :email, allow_blank: true
22
+ validates_phone :phone, allow_blank: true
23
+ validates_unique :email, allow_blank: true
24
+ validates_unique :phone, allow_blank: true
25
+ end
26
+
27
+ def active_sessions
28
+ sessions_dataset.where(Sequel.lit('"sessions"."state"') => 'active')
29
+ end
30
+ end
31
+ end