masks-server 0.11.0 → 0.12.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/masks/server/account_controller.rb +3 -0
  3. data/app/controllers/masks/server/app_passwords_controller.rb +41 -0
  4. data/app/controllers/masks/server/authorize_controller.rb +33 -2
  5. data/app/controllers/masks/server/forward_auth_controller.rb +104 -12
  6. data/app/controllers/masks/server/logins_controller.rb +4 -3
  7. data/app/controllers/masks/server/organization_access_controller.rb +39 -0
  8. data/app/graphql/masks/server/manage/mutations/base_mutation.rb +7 -0
  9. data/app/graphql/masks/server/manage/mutations/create_group.rb +6 -4
  10. data/app/graphql/masks/server/manage/mutations/decline_access.rb +27 -0
  11. data/app/graphql/masks/server/manage/mutations/grant_access.rb +29 -0
  12. data/app/graphql/masks/server/manage/mutations/group_source.rb +34 -0
  13. data/app/graphql/masks/server/manage/mutations/issue_ldap_credential.rb +5 -2
  14. data/app/graphql/masks/server/manage/mutations/refresh_group.rb +21 -0
  15. data/app/graphql/masks/server/manage/mutations/remove_group_member.rb +2 -0
  16. data/app/graphql/masks/server/manage/mutations/run_health_checks.rb +19 -0
  17. data/app/graphql/masks/server/manage/mutations/update_client.rb +6 -0
  18. data/app/graphql/masks/server/manage/mutations/update_group.rb +7 -4
  19. data/app/graphql/masks/server/manage/mutations/update_tenant.rb +14 -1
  20. data/app/graphql/masks/server/manage/types/access_request_type.rb +21 -0
  21. data/app/graphql/masks/server/manage/types/actor_type.rb +6 -0
  22. data/app/graphql/masks/server/manage/types/client_type.rb +12 -0
  23. data/app/graphql/masks/server/manage/types/group_condition_input.rb +16 -0
  24. data/app/graphql/masks/server/manage/types/group_condition_type.rb +29 -0
  25. data/app/graphql/masks/server/manage/types/group_member_type.rb +16 -0
  26. data/app/graphql/masks/server/manage/types/group_type.rb +17 -1
  27. data/app/graphql/masks/server/manage/types/health_check_type.rb +18 -0
  28. data/app/graphql/masks/server/manage/types/ldap_credential_type.rb +1 -0
  29. data/app/graphql/masks/server/manage/types/mutation_type.rb +4 -0
  30. data/app/graphql/masks/server/manage/types/query_type.rb +24 -0
  31. data/app/graphql/masks/server/manage/types/release_type.rb +16 -0
  32. data/app/graphql/masks/server/manage/types/tenant_type.rb +6 -1
  33. data/app/helpers/masks/server/account_helper.rb +4 -0
  34. data/app/jobs/masks/server/cleanup_job.rb +1 -0
  35. data/app/jobs/masks/server/health_check_job.rb +21 -0
  36. data/app/jobs/masks/server/refresh_groups_job.rb +18 -0
  37. data/app/jobs/masks/server/release_check_job.rb +21 -0
  38. data/app/ldap/masks/server/ldap/connection.rb +3 -2
  39. data/app/ldap/masks/server/ldap/gate.rb +31 -4
  40. data/app/logins/masks/server/login_states/second_factor.rb +1 -1
  41. data/app/mailers/masks/server/actor_mailer.rb +31 -0
  42. data/app/models/masks/server/access_request.rb +103 -0
  43. data/app/models/masks/server/actor.rb +6 -0
  44. data/app/models/masks/server/app_password.rb +91 -0
  45. data/app/models/masks/server/app_passwords.rb +82 -0
  46. data/app/models/masks/server/client.rb +15 -0
  47. data/app/models/masks/server/connection.rb +7 -0
  48. data/app/models/masks/server/declared.rb +10 -2
  49. data/app/models/masks/server/email_verification.rb +2 -2
  50. data/app/models/masks/server/event.rb +11 -1
  51. data/app/models/masks/server/federation/protocol.rb +1 -1
  52. data/app/models/masks/server/federation/saml.rb +1 -1
  53. data/app/models/masks/server/forward_auth_rules.rb +162 -0
  54. data/app/models/masks/server/group.rb +207 -4
  55. data/app/models/masks/server/group_member.rb +7 -0
  56. data/app/models/masks/server/group_rule.rb +63 -0
  57. data/app/models/masks/server/health.rb +243 -0
  58. data/app/models/masks/server/health_check.rb +27 -0
  59. data/app/models/masks/server/invitation.rb +2 -2
  60. data/app/models/masks/server/ldap_credential.rb +2 -2
  61. data/app/models/masks/server/login.rb +28 -2
  62. data/app/models/masks/server/mail_previews.rb +5 -0
  63. data/app/models/masks/server/mail_template.rb +2 -0
  64. data/app/models/masks/server/membership.rb +10 -3
  65. data/app/models/masks/server/notifications.rb +3 -1
  66. data/app/models/masks/server/password_reset.rb +2 -2
  67. data/app/models/masks/server/provider.rb +1 -0
  68. data/app/models/masks/server/queue.rb +60 -0
  69. data/app/models/masks/server/release.rb +74 -0
  70. data/app/models/masks/server/session.rb +8 -0
  71. data/app/models/masks/server/tenant.rb +39 -2
  72. data/app/models/masks/server/token.rb +2 -1
  73. data/app/views/masks/server/account/_app_passwords.html.erb +83 -0
  74. data/app/views/masks/server/account/_organizations.html.erb +33 -0
  75. data/app/views/masks/server/account/index.html.erb +5 -0
  76. data/app/views/masks/server/actor_mailer/access_declined.html.erb +5 -0
  77. data/app/views/masks/server/actor_mailer/access_declined.text.erb +5 -0
  78. data/app/views/masks/server/actor_mailer/access_granted.html.erb +7 -0
  79. data/app/views/masks/server/actor_mailer/access_granted.text.erb +7 -0
  80. data/app/views/masks/server/authorize/error.html.erb +25 -0
  81. data/config/locales/de/access_requests.yml +43 -0
  82. data/config/locales/de/account/app_passwords.yml +42 -0
  83. data/config/locales/de/events.yml +5 -0
  84. data/config/locales/de/flashes.yml +1 -0
  85. data/config/locales/de/group_sources.yml +5 -0
  86. data/config/locales/de/manage/access_requests.yml +12 -0
  87. data/config/locales/de/manage/app_passwords.yml +16 -0
  88. data/config/locales/de/manage/forward_auth_rules.yml +29 -0
  89. data/config/locales/de/manage/group_sources.yml +44 -0
  90. data/config/locales/de/manage/health.yml +78 -0
  91. data/config/locales/de/manage/links.yml +20 -0
  92. data/config/locales/en/access_requests.yml +43 -0
  93. data/config/locales/en/account/app_passwords.yml +42 -0
  94. data/config/locales/en/events.yml +5 -0
  95. data/config/locales/en/flashes.yml +1 -0
  96. data/config/locales/en/group_sources.yml +5 -0
  97. data/config/locales/en/manage/access_requests.yml +12 -0
  98. data/config/locales/en/manage/app_passwords.yml +16 -0
  99. data/config/locales/en/manage/forward_auth_rules.yml +29 -0
  100. data/config/locales/en/manage/group_sources.yml +44 -0
  101. data/config/locales/en/manage/health.yml +78 -0
  102. data/config/locales/en/manage/links.yml +20 -0
  103. data/config/locales/es/access_requests.yml +43 -0
  104. data/config/locales/es/account/app_passwords.yml +42 -0
  105. data/config/locales/es/events.yml +5 -0
  106. data/config/locales/es/flashes.yml +1 -0
  107. data/config/locales/es/group_sources.yml +5 -0
  108. data/config/locales/es/manage/access_requests.yml +12 -0
  109. data/config/locales/es/manage/app_passwords.yml +16 -0
  110. data/config/locales/es/manage/forward_auth_rules.yml +29 -0
  111. data/config/locales/es/manage/group_sources.yml +44 -0
  112. data/config/locales/es/manage/health.yml +78 -0
  113. data/config/locales/es/manage/links.yml +20 -0
  114. data/config/locales/fr/access_requests.yml +43 -0
  115. data/config/locales/fr/account/app_passwords.yml +42 -0
  116. data/config/locales/fr/events.yml +5 -0
  117. data/config/locales/fr/flashes.yml +1 -0
  118. data/config/locales/fr/group_sources.yml +5 -0
  119. data/config/locales/fr/manage/access_requests.yml +12 -0
  120. data/config/locales/fr/manage/app_passwords.yml +16 -0
  121. data/config/locales/fr/manage/forward_auth_rules.yml +29 -0
  122. data/config/locales/fr/manage/group_sources.yml +44 -0
  123. data/config/locales/fr/manage/health.yml +78 -0
  124. data/config/locales/fr/manage/links.yml +20 -0
  125. data/config/routes.rb +5 -0
  126. data/db/migrate/20261020100000_create_access_requests.rb +28 -0
  127. data/db/migrate/20261020110000_add_app_passwords_to_clients.rb +5 -0
  128. data/db/migrate/20261020120000_add_forward_auth_rules_to_clients.rb +6 -0
  129. data/db/migrate/20261020130000_add_sources_to_groups.rb +15 -0
  130. data/db/migrate/20261020140000_create_health_checks.rb +25 -0
  131. data/db/migrate/20261020150000_add_link_lifetimes_to_tenants.rb +8 -0
  132. data/db/migrate/20261021100000_add_pinned_origin_to_tenants.rb +5 -0
  133. data/lib/generators/masks/server/install/install_generator.rb +3 -2
  134. data/lib/masks/server/configuration.rb +2 -11
  135. data/lib/masks/server/schema_reference.rb +12 -0
  136. data/lib/masks/server/tenancy/middleware.rb +17 -6
  137. data/public/masks-assets/.vite/manifest.json +13 -13
  138. data/public/masks-assets/assets/{account-D46qDQCo.js → account-h7I6VUPR.js} +2 -2
  139. data/public/masks-assets/assets/{account-D46qDQCo.js.map → account-h7I6VUPR.js.map} +1 -1
  140. data/public/masks-assets/assets/{app-D7W0gAyT.css → app-BXOScMjL.css} +1 -1
  141. data/public/masks-assets/assets/{handshake-DIFZPSAK.js → handshake-YQpRNMIs.js} +2 -2
  142. data/public/masks-assets/assets/{handshake-DIFZPSAK.js.map → handshake-YQpRNMIs.js.map} +1 -1
  143. data/public/masks-assets/assets/initial-Cn-UuAsy.js +3 -0
  144. data/public/masks-assets/assets/{initial-C1GpCiEh.js.map → initial-Cn-UuAsy.js.map} +1 -1
  145. data/public/masks-assets/assets/{login-C_6igRZ3.js → login-C21_jb6L.js} +3 -3
  146. data/public/masks-assets/assets/{login-C_6igRZ3.js.map → login-C21_jb6L.js.map} +1 -1
  147. data/public/masks-assets/assets/{manage-D4MzaDvM.js → manage-DpVxe5sv.js} +80 -51
  148. data/public/masks-assets/assets/manage-DpVxe5sv.js.map +1 -0
  149. data/public/masks-assets/assets/passkey-5UYPVD3c.js +2 -0
  150. data/public/masks-assets/assets/{passkey-DIj_5LCY.js.map → passkey-5UYPVD3c.js.map} +1 -1
  151. data/version.txt +1 -1
  152. metadata +87 -14
  153. data/public/masks-assets/assets/initial-C1GpCiEh.js +0 -3
  154. data/public/masks-assets/assets/manage-D4MzaDvM.js.map +0 -1
  155. data/public/masks-assets/assets/passkey-DIj_5LCY.js +0 -2
@@ -18,8 +18,8 @@ module Masks
18
18
 
19
19
  before_create { self.invited_at ||= Time.current if pending? }
20
20
 
21
- def self.lifetime
22
- ::Rails.configuration.masks.organization_invitation_lifetime
21
+ def self.lifetime(tenant = Current.tenant)
22
+ Tenant.link_lifetime(:organization_invitation_lifetime, tenant)
23
23
  end
24
24
 
25
25
  normalizes :external_id, with: Actor::EXTERNAL_ID
@@ -29,13 +29,14 @@ module Masks
29
29
  validate :an_owner_remains, on: :update
30
30
 
31
31
  before_destroy :keep_an_owner
32
+ after_commit :settle_nested_groups
32
33
 
33
34
  def owner?
34
35
  role == Organization::OWNER && !pending?
35
36
  end
36
37
 
37
38
  def expires_at
38
- invited_at + self.class.lifetime if pending?
39
+ invited_at + self.class.lifetime(tenant) if pending?
39
40
  end
40
41
 
41
42
  def expired?
@@ -72,6 +73,12 @@ module Masks
72
73
 
73
74
  private
74
75
 
76
+ def settle_nested_groups
77
+ return if organization.nil? || actor.nil?
78
+
79
+ Group.nested_under_organization(organization.key).find_each { |group| group.settle!(actor) }
80
+ end
81
+
75
82
  def role_is_offered
76
83
  errors.add(:role, "is not one #{organization&.name} offers") unless organization&.role?(role)
77
84
  end
@@ -18,7 +18,9 @@ module Masks
18
18
  Event::TEXT_CODES_ENABLED,
19
19
  Event::TEXT_CODES_DISABLED,
20
20
  Event::RECOVERY_REQUESTED,
21
- Event::SECOND_FACTORS_RESET
21
+ Event::SECOND_FACTORS_RESET,
22
+ Event::APP_PASSWORD_CREATED,
23
+ Event::APP_PASSWORD_REVOKED
22
24
  ],
23
25
  "sessions" => [
24
26
  Event::SESSION_STARTED,
@@ -5,8 +5,8 @@ module Masks
5
5
 
6
6
  path "reset"
7
7
 
8
- def self.lifetime
9
- ::Rails.configuration.masks.password_reset_lifetime
8
+ def self.lifetime(tenant = Current.tenant)
9
+ Tenant.link_lifetime(:password_reset_lifetime, tenant)
10
10
  end
11
11
 
12
12
  def self.settle!(secret, password)
@@ -42,6 +42,7 @@ module Masks
42
42
  encrypts :private_key
43
43
 
44
44
  has_many :connections, dependent: :destroy
45
+ has_many :groups, dependent: :nullify
45
46
  has_many :delegations, through: :connections
46
47
 
47
48
  belongs_to :organization, optional: true
@@ -0,0 +1,60 @@
1
+ module Masks
2
+ module Server
3
+ module Queue
4
+ module_function
5
+
6
+ def run?
7
+ ::Rails.application.config.active_job.queue_adapter == :solid_queue
8
+ end
9
+
10
+ def silence
11
+ ::Rails.configuration.masks.worker_silence
12
+ end
13
+
14
+ def wait
15
+ ::Rails.configuration.masks.queue_wait
16
+ end
17
+
18
+ def failing_jobs
19
+ ::SolidQueue::FailedExecution.where(created_at: 1.day.ago..).joins(:job)
20
+ .distinct.pluck("solid_queue_jobs.class_name").sort
21
+ end
22
+
23
+ def worker_heartbeat_at
24
+ ::SolidQueue::Process.where(kind: "Worker").maximum(:last_heartbeat_at)
25
+ end
26
+
27
+ def oldest_waiting_at
28
+ ::SolidQueue::ReadyExecution.minimum(:created_at)
29
+ end
30
+
31
+ def worker_silent?
32
+ return false unless run? && silence.positive?
33
+
34
+ heartbeat = worker_heartbeat_at
35
+
36
+ heartbeat.nil? || heartbeat < silence.ago
37
+ end
38
+
39
+ def stuck_since
40
+ return nil unless run? && wait.positive?
41
+
42
+ oldest = oldest_waiting_at
43
+
44
+ oldest if oldest && oldest < wait.ago
45
+ end
46
+
47
+ def plain(duration)
48
+ ActiveSupport::Duration.build(duration.to_i).inspect
49
+ end
50
+
51
+ def worker_problem
52
+ return "no worker has reported in the last #{plain(silence)}" if worker_silent?
53
+
54
+ stuck = stuck_since
55
+
56
+ "a job has waited since #{stuck.utc.iso8601}, longer than #{plain(wait)}" if stuck
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,74 @@
1
+ module Masks
2
+ module Server
3
+ module Release
4
+ URL = "https://api.github.com/repos/masksrb/masks/releases?per_page=20".freeze
5
+ KEY = "masks:release".freeze
6
+ KEEP = 3.days
7
+ TAG = /\Aserver-v(\d+\.\d+\.\d+)\z/
8
+ OPERATOR = /^\s*Operator:/i
9
+ TIMEOUT = 5
10
+ CEILING = 1.megabyte
11
+ UNREADABLE = [ Outbound::Refused, JSON::ParserError, TypeError, NoMethodError ].freeze
12
+
13
+ module_function
14
+
15
+ def check!
16
+ found = fetch
17
+
18
+ return nil if found.nil?
19
+
20
+ was = latest
21
+ remember!(found)
22
+
23
+ found if newer?(found["version"]) && was&.dig("version") != found["version"]
24
+ end
25
+
26
+ def fetch
27
+ headers = { "Accept" => "application/vnd.github+json", "User-Agent" => "masks-server/#{VERSION}" }
28
+ body = Outbound.get!(URI(URL), open: TIMEOUT, read: TIMEOUT, within: TIMEOUT, ceiling: CEILING, headers: headers).body
29
+
30
+ JSON.parse(body).each do |release|
31
+ version = release["tag_name"].to_s[TAG, 1]
32
+
33
+ next if version.nil? || release["draft"] || release["prerelease"]
34
+
35
+ return {
36
+ "version" => version,
37
+ "url" => release["html_url"].to_s,
38
+ "operator_step" => release["body"].to_s.match?(OPERATOR),
39
+ "checked_at" => Time.current.utc.iso8601
40
+ }
41
+ end
42
+
43
+ nil
44
+ rescue *UNREADABLE => error
45
+ ::Rails.logger.warn("the release check could not read the releases: #{error.class}")
46
+ nil
47
+ end
48
+
49
+ def remember!(found)
50
+ ::Rails.cache.write(KEY, found, expires_in: KEEP)
51
+ end
52
+
53
+ def forget!
54
+ ::Rails.cache.delete(KEY)
55
+ end
56
+
57
+ def latest
58
+ ::Rails.cache.read(KEY)
59
+ end
60
+
61
+ def available
62
+ found = latest
63
+
64
+ found if found && newer?(found["version"])
65
+ end
66
+
67
+ def newer?(version)
68
+ Gem::Version.new(version) > Gem::Version.new(VERSION)
69
+ rescue ArgumentError
70
+ false
71
+ end
72
+ end
73
+ end
74
+ end
@@ -118,6 +118,14 @@ module Masks
118
118
  true
119
119
  end
120
120
 
121
+ def multi_factored?
122
+ Array(amr).include?(Issuer::MULTI_FACTOR) || !actor.second_factor?
123
+ end
124
+
125
+ def complete?
126
+ !second_factor_skipped?
127
+ end
128
+
121
129
  def fresh?
122
130
  authenticated_at.present? && authenticated_at > FRESHNESS.ago
123
131
  end
@@ -64,6 +64,12 @@ module Masks
64
64
 
65
65
  IDLE_DAYS = (60..3650)
66
66
  RETENTION_DAYS = (30..2555)
67
+ LINK_LIFETIMES = {
68
+ invitation_lifetime: (1.hour.to_i..30.days.to_i),
69
+ organization_invitation_lifetime: (1.hour.to_i..30.days.to_i),
70
+ password_reset_lifetime: (5.minutes.to_i..1.day.to_i),
71
+ email_verification_lifetime: (1.hour.to_i..14.days.to_i)
72
+ }.freeze
67
73
 
68
74
  validates :named_by, inclusion: { in: NAMES }, allow_nil: true
69
75
  validates :accent_color, format: { with: ACCENT }, allow_nil: true
@@ -71,6 +77,18 @@ module Masks
71
77
  validates :dynamic_registration, inclusion: { in: REGISTRATIONS }, allow_nil: true
72
78
  validates :suspend_after, :delete_after, numericality: { only_integer: true, in: IDLE_DAYS }, allow_nil: true
73
79
  validates :event_retention_days, numericality: { only_integer: true, in: RETENTION_DAYS }, allow_nil: true
80
+ def self.spoken(seconds)
81
+ unit, length = { "day" => 1.day, "hour" => 1.hour, "minute" => 1.minute }.find { |_, held| (seconds % held).zero? }
82
+ count = seconds / length
83
+
84
+ "#{count} #{unit.pluralize(count)}"
85
+ end
86
+
87
+ LINK_LIFETIMES.each do |name, range|
88
+ shortest, longest = [ range.begin, range.end ].map { |seconds| spoken(seconds) }
89
+
90
+ validates name, numericality: { only_integer: true, in: range, message: "must be from #{shortest} to #{longest}" }
91
+ end
74
92
  validates :delete_after, comparison: { greater_than: :suspend_after, message: "must be longer than suspend after" },
75
93
  if: -> { suspend_after && delete_after }
76
94
 
@@ -104,8 +122,20 @@ module Masks
104
122
  event_retention_days&.days || Event::RETENTION
105
123
  end
106
124
 
125
+ def self.link_lifetime(name, tenant = Current.tenant)
126
+ (tenant ? tenant[name] : column_defaults.fetch(name.to_s)).seconds
127
+ end
128
+
129
+ def self.default_link_lifetime(name)
130
+ column_defaults.fetch(name.to_s)
131
+ end
132
+
107
133
  def public_origin
108
- custom_origin || templated_origin
134
+ custom_origin || templated_origin || held_origin
135
+ end
136
+
137
+ def held_origin
138
+ pinned_origin if self.class.pins_origin?
109
139
  end
110
140
 
111
141
  def templated_origin
@@ -246,6 +276,10 @@ module Masks
246
276
  serving(host) || named(host)
247
277
  end
248
278
 
279
+ def pins_origin?
280
+ ::Rails.configuration.masks.public_origin_template.nil? && ::Rails.configuration.masks.pin_origin
281
+ end
282
+
249
283
  def served_domain
250
284
  template = ::Rails.configuration.masks.public_origin_template
251
285
 
@@ -403,7 +437,10 @@ module Masks
403
437
  end
404
438
 
405
439
  def set_up!
406
- update!(setup_token: nil) if setup_token.present?
440
+ self.setup_token = nil
441
+ self.pinned_origin ||= Current.origin.presence if self.class.pins_origin?
442
+
443
+ save! if changed?
407
444
  end
408
445
 
409
446
  def setup_announcement
@@ -22,7 +22,8 @@ module Masks
22
22
  "pushed" => "PushedRequest",
23
23
  "approval" => "SignInApproval",
24
24
  "forward_code" => "ForwardCode",
25
- "forward_session" => "ForwardSession"
25
+ "forward_session" => "ForwardSession",
26
+ "app_password" => "AppPassword"
26
27
  }.freeze
27
28
 
28
29
  self.inheritance_column = "kind"
@@ -0,0 +1,83 @@
1
+ <section class="group" id="app-passwords">
2
+ <h2><%= account_text("app_passwords.heading") %></h2>
3
+
4
+ <div class="entries">
5
+ <details class="entry" <%= "open" if app_password %>>
6
+ <summary>
7
+ <span class="entry-icon"><%= account_icon(:password) %></span>
8
+ <span class="entry-label"><%= account_text("app_passwords.entry") %></span>
9
+ <span class="entry-state">
10
+ <%= app_passwords.any? ? app_passwords.size : account_text("app_passwords.state.none") %>
11
+ </span>
12
+ <span class="entry-mark" aria-hidden="true"></span>
13
+ </summary>
14
+
15
+ <div class="entry-body">
16
+ <p class="aside"><%= account_text("app_passwords.lede") %></p>
17
+
18
+ <% if app_password %>
19
+ <p class="aside aside-bad" role="alert">
20
+ <%= account_text("app_passwords.shown_once", name: app_password["name"], app: app_password["app"]) %>
21
+ </p>
22
+
23
+ <ol class="codes">
24
+ <li><code><%= app_password["secret"] %></code></li>
25
+ </ol>
26
+ <% end %>
27
+
28
+ <% if app_passwords.any? %>
29
+ <div class="items">
30
+ <% app_passwords.each do |held| %>
31
+ <div class="item">
32
+ <span class="item-name">
33
+ <span><%= held.name %></span>
34
+ <span class="tag"><%= held.client.name %></span>
35
+ </span>
36
+
37
+ <span class="item-meta">
38
+ <%= account_text("app_passwords.made_ago", age: time_ago_in_words(held.created_at)) %>
39
+ ·
40
+ <% if held.used_at %>
41
+ <%= account_text("app_passwords.used_ago", age: time_ago_in_words(held.used_at)) %>
42
+ <% else %>
43
+ <%= account_text("app_passwords.never_used") %>
44
+ <% end %>
45
+ </span>
46
+
47
+ <%= button_to account_text("app_passwords.revoke"),
48
+ account_app_password_path(held.id), method: :delete,
49
+ class: "action action-quiet action-sm", form_class: "item-act",
50
+ form: { data: { turbo_confirm: account_text("app_passwords.confirm_revoke", name: held.name) } } %>
51
+ </div>
52
+ <% end %>
53
+ </div>
54
+ <% end %>
55
+
56
+ <% if offered.any? %>
57
+ <%= form_with url: account_app_passwords_path, method: :post, class: "member-add" do |form| %>
58
+ <label class="field member-add-email">
59
+ <span class="field-label"><%= account_text("app_passwords.name") %></span>
60
+ <%= form.text_field :name, required: true, autocomplete: "off", class: "control", id: "app-password-name",
61
+ maxlength: Masks::Server::AppPassword::NAME_LIMIT,
62
+ placeholder: account_text("app_passwords.name_placeholder") %>
63
+ </label>
64
+
65
+ <label class="field member-add-role">
66
+ <span class="field-label"><%= account_text("app_passwords.app") %></span>
67
+ <%= form.select :client_id, offered.map { |client| [ client.name, client.client_id ] }, {}, class: "control",
68
+ id: "app-password-client" %>
69
+ </label>
70
+
71
+ <button type="submit" class="action action-sm member-add-go">
72
+ <%= account_text(fresh ? "app_passwords.make" : "app_passwords.make_again") %>
73
+ </button>
74
+
75
+ <span class="field-hint member-add-hint"><%= account_text("app_passwords.hint") %></span>
76
+ <% end %>
77
+ <% else %>
78
+ <p class="aside"><%= account_text("app_passwords.none_offered") %></p>
79
+ <% end %>
80
+ </div>
81
+ </details>
82
+ </div>
83
+ </section>
@@ -135,6 +135,39 @@
135
135
  <% end %>
136
136
  </div>
137
137
 
138
+ <% asked = account_access_requests(organization) %>
139
+ <% if asked.any? %>
140
+ <p class="aside"><strong><%= account_text("access.heading") %></strong> <%= account_text("access.lede", organization: organization.name) %></p>
141
+
142
+ <div class="items" id="access-requests-<%= organization.key %>">
143
+ <% asked.each do |request| %>
144
+ <% roles = request.organization_roles.filter_map { |key, role| role if key == organization.key } %>
145
+ <div class="item item-member access-request" id="access-request-<%= request.id %>">
146
+ <span class="item-name"><%= request.actor.identifier %></span>
147
+ <span class="item-meta">
148
+ <%= account_text("access.asked", app: request.client.name, age: time_ago_in_words(request.created_at)) %>
149
+ </span>
150
+
151
+ <div class="item-act item-acts">
152
+ <%= form_with url: account_organization_access_path(organization.key, request.id), method: :post,
153
+ class: "item-acts" do |form| %>
154
+ <%= form.select :role, roles, {}, class: "control control-sm",
155
+ id: "access-role-#{organization.key}-#{request.id}",
156
+ aria: { label: account_text("access.role_of", name: request.actor.identifier) } %>
157
+ <button type="submit" class="action action-sm"><%= account_text("access.grant") %></button>
158
+ <% end %>
159
+
160
+ <%= button_to account_text("access.decline"),
161
+ account_organization_access_path(organization.key, request.id), method: :delete,
162
+ class: "action action-quiet action-sm",
163
+ form: { data: { turbo_confirm: account_text("access.confirm_decline", identifier: request.actor.identifier,
164
+ app: request.client.name) } } %>
165
+ </div>
166
+ </div>
167
+ <% end %>
168
+ </div>
169
+ <% end %>
170
+
138
171
  <%= form_with url: account_organization_members_path(organization.key), method: :post, class: "member-add" do |form| %>
139
172
  <label class="field member-add-email">
140
173
  <span class="field-label"><%= account_text("organizations.add_email") %></span>
@@ -31,6 +31,11 @@
31
31
 
32
32
  <%= render "masks/server/account/apps", apps: @apps, connections: @connections, linkable: @linkable %>
33
33
 
34
+ <% if @app_passwords.any? || @app_password_clients.any? %>
35
+ <%= render "masks/server/account/app_passwords", app_passwords: @app_passwords, offered: @app_password_clients,
36
+ app_password: @app_password, fresh: @fresh %>
37
+ <% end %>
38
+
34
39
  <%= render "masks/server/account/info", actor: @actor, directed: @directed, fresh: @fresh %>
35
40
 
36
41
  <%= render "masks/server/account/activity", events: @events %>
@@ -0,0 +1,5 @@
1
+ <%= render layout: "message" do %>
2
+ <p class="lede"><%= t(".lede", app: @app.name, tenant: @tenant_name) %></p>
3
+ <% end %>
4
+
5
+ <p class="meta"><%= t(".again") %></p>
@@ -0,0 +1,5 @@
1
+ <%= render layout: "message" do %>
2
+ <%= t(".lede", app: @app.name, tenant: @tenant_name) %>
3
+ <% end %>
4
+
5
+ <%= t(".again") %>
@@ -0,0 +1,7 @@
1
+ <%= render layout: "message" do %>
2
+ <p class="lede"><%= t(".lede", app: @app.name, tenant: @tenant_name) %></p>
3
+ <% end %>
4
+
5
+ <% if @url %>
6
+ <%= render "action", url: @url, label: t(".action", app: @app.name) %>
7
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%= render layout: "message" do %>
2
+ <%= t(".lede", app: @app.name, tenant: @tenant_name) %>
3
+ <% end %>
4
+ <% if @url %>
5
+
6
+ <%= @url %>
7
+ <% end %>
@@ -9,4 +9,29 @@
9
9
  <p class="said"><%= @error_code %> — <%= @error_description %></p>
10
10
 
11
11
  <p class="prompt-lede"><%= @refused ? t(".nothing_shared") : t(".contact") %></p>
12
+
13
+ <% if @asking %>
14
+ <div id="ask-access" class="flow" data-waiting="<%= @waiting.present? %>">
15
+ <% if flash[:alert].present? %>
16
+ <p class="note note-bad" role="alert"><%= flash[:alert] %></p>
17
+ <% elsif flash[:notice].present? %>
18
+ <p class="note note-plain" role="status"><%= flash[:notice] %></p>
19
+ <% end %>
20
+
21
+ <% if @waiting %>
22
+ <p class="prompt-lede"><%= t(".waiting", app: @asking.name) %></p>
23
+ <% else %>
24
+ <p class="prompt-lede"><%= t(".asking", app: @asking.name) %></p>
25
+
26
+ <%= form_with url: authorize_access_request_path, method: :post do |form| %>
27
+ <%= form.hidden_field :rid, value: params[:rid] %>
28
+ <button type="submit" class="action"><%= t(".ask") %></button>
29
+ <% end %>
30
+ <% end %>
31
+ </div>
32
+ <% end %>
33
+
34
+ <% if @return_to %>
35
+ <a class="action action-quiet" id="back-to-app" href="<%= @return_to %>"><%= t(".back", app: @asking&.name || t(".the_app")) %></a>
36
+ <% end %>
12
37
  </div>
@@ -0,0 +1,43 @@
1
+ de:
2
+ authorize:
3
+ error:
4
+ asking: "Ihr Konto ist in keiner Gruppe, die %{app} nutzen darf. Sie können um Zugang bitten. Die Betreiber von %{app} entscheiden, und Sie erhalten in jedem Fall eine E-Mail."
5
+ ask: "Um Zugang bitten"
6
+ asked: "Ihre Anfrage wurde gesendet. Sie erhalten eine E-Mail, sobald sie entschieden ist."
7
+ waiting: "Ihre Anfrage, %{app} zu nutzen, wartet. Sie erhalten eine E-Mail, sobald sie entschieden ist."
8
+ slow_down: "Sie haben vor weniger als einer Stunde um Zugang zu dieser App gebeten. Warten Sie auf die Antwort."
9
+ back: "Zurück zu %{app}"
10
+ the_app: "der App"
11
+
12
+ account:
13
+ index:
14
+ access:
15
+ heading: "Um Zugang gebeten"
16
+ lede: "Diese Personen wurden von einer App abgewiesen, die Mitglieder von %{organization} zulässt. Mit Genehmigen werden sie Mitglied mit der Rolle, die Sie wählen."
17
+ asked: "Hat vor %{age} gebeten, %{app} zu nutzen"
18
+ role_of: "Rolle für %{name}"
19
+ grant: "Genehmigen"
20
+ decline: "Ablehnen"
21
+ confirm_decline: "Die Anfrage von %{identifier}, %{app} zu nutzen, ablehnen? Die Person erhält eine E-Mail und kann in einer Stunde erneut bitten."
22
+
23
+ organization_access:
24
+ granted: "%{identifier} ist jetzt %{role} von %{organization} und kann %{app} nutzen."
25
+ declined: "Die Anfrage von %{identifier}, %{app} zu nutzen, ist abgelehnt."
26
+ unknown: "Diese Anfrage wartet nicht auf %{organization}."
27
+
28
+ actor_mailer:
29
+ access_granted:
30
+ subject: "Sie können %{app} jetzt nutzen"
31
+ lede: "Ihre Anfrage, %{app} bei %{tenant} zu nutzen, wurde genehmigt. Melden Sie sich erneut bei %{app} an, und Sie werden eingelassen."
32
+ action: "%{app} öffnen"
33
+ access_declined:
34
+ subject: "Ihre Anfrage, %{app} zu nutzen, wurde abgelehnt"
35
+ lede: "Jemand, der %{app} bei %{tenant} betreibt, hat Ihre Anfrage um Zugang abgelehnt."
36
+ again: "Wenn Sie das für einen Fehler halten, fragen Sie die Person direkt. In einer Stunde können Sie erneut bitten."
37
+
38
+ events:
39
+ actions:
40
+ access:
41
+ requested: "Um Zugang zu einer App gebeten"
42
+ granted: "Zugang zu einer App genehmigt"
43
+ declined: "Zugang zu einer App abgelehnt"
@@ -0,0 +1,42 @@
1
+ de:
2
+ account:
3
+ index:
4
+ app_passwords:
5
+ heading: "App-Passwörter"
6
+ entry: "App-Passwörter"
7
+ state:
8
+ none: "Keine"
9
+ lede: "Mit einem App-Passwort meldet sich eine einzelne App, etwa ein Medienserver oder ein NAS, ohne Ihren zweiten Faktor als Sie an. Es gilt nur für die App, für die es erstellt wurde."
10
+ shown_once: "Das ist das App-Passwort für %{name}. Es wird nur einmal angezeigt. Geben Sie es jetzt in %{app} ein."
11
+ made_ago: "erstellt vor %{age}"
12
+ used_ago: "zuletzt verwendet vor %{age}"
13
+ never_used: "nie verwendet"
14
+ revoke: "Widerrufen"
15
+ confirm_revoke: "%{name} widerrufen? Die App kann sich damit sofort nicht mehr anmelden."
16
+ name: "Name"
17
+ name_placeholder: "Jellyfin auf dem Fernseher"
18
+ app: "App"
19
+ make: "App-Passwort erstellen"
20
+ make_again: "Zum Erstellen erneut anmelden"
21
+ hint: "Ihr eigenes Passwort und Ihre Passkeys bleiben unverändert. Das Widerrufen eines App-Passworts ändert sonst nichts."
22
+ none_offered: "Noch keine App akzeptiert App-Passwörter."
23
+ app_passwords:
24
+ again: "Melden Sie sich erneut an, um ein App-Passwort zu erstellen. Ihre letzte Anmeldung liegt eine Weile zurück."
25
+ unknown: "Dieses Konto hat kein solches App-Passwort."
26
+ revoked: "%{name} ist widerrufen."
27
+ refused:
28
+ name: "Geben Sie dem App-Passwort einen Namen mit höchstens 64 Zeichen."
29
+ app: "Diese App akzeptiert keine App-Passwörter."
30
+ too_many: "Sie haben bereits %{most} App-Passwörter. Widerrufen Sie zuerst eines."
31
+ events:
32
+ actions:
33
+ app_password:
34
+ created: "App-Passwort erstellt"
35
+ used: "Mit einem App-Passwort angemeldet"
36
+ revoked: "App-Passwort widerrufen"
37
+ actor_mailer:
38
+ notification:
39
+ said:
40
+ app_password:
41
+ created: "Für Ihr %{tenant}-Konto wurde ein App-Passwort erstellt. Damit meldet sich eine App ohne Ihren zweiten Faktor an."
42
+ revoked: "Ein App-Passwort Ihres %{tenant}-Kontos wurde widerrufen."
@@ -159,6 +159,11 @@ de:
159
159
  updated: "E-Mail-Vorlage geändert"
160
160
  job:
161
161
  failed: "Hintergrundjob für diesen Mandanten fehlgeschlagen"
162
+ health:
163
+ failing: "Zustandsprüfung fehlgeschlagen"
164
+ recovered: "Zustandsprüfung wieder in Ordnung"
165
+ release:
166
+ available: "Neue Version verfügbar"
162
167
  stream:
163
168
  created: "Event-Stream hinzugefügt"
164
169
  updated: "Event-Stream geändert"
@@ -163,3 +163,4 @@ de:
163
163
  forward_auth:
164
164
  unknown: "Unter dieser Adresse ist keine App hinter einem Proxy registriert."
165
165
  not_admitted: "Ihr Konto darf %{app} nicht verwenden."
166
+ denied: "Diese Seite von %{app} ist für alle gesperrt."
@@ -0,0 +1,5 @@
1
+ de:
2
+ events:
3
+ actions:
4
+ group:
5
+ refreshed: "Gruppe aus ihrer Quelle gefüllt"
@@ -0,0 +1,12 @@
1
+ de:
2
+ manage:
3
+ access_requests:
4
+ title: "Zugangsanfragen"
5
+ lede: "Akteure, die von diesem Client abgewiesen wurden und um Zugang gebeten haben. Mit Genehmigen kommen sie in eine der Gruppen, die er zulässt, und sie erhalten in jedem Fall eine E-Mail."
6
+ empty: "Niemand wartet auf Zugang."
7
+ asked: "Gebeten %{day}"
8
+ as: "Gruppe, in die sie kommen"
9
+ grant: "Genehmigen"
10
+ decline: "Ablehnen"
11
+ granted: "%{identifier} ist jetzt in %{label} und kann sich anmelden."
12
+ declined: "Die Anfrage von %{identifier} ist abgelehnt."
@@ -0,0 +1,16 @@
1
+ de:
2
+ manage:
3
+ client:
4
+ sign_in:
5
+ app_passwords: "App-Passwörter akzeptieren"
6
+ app_passwords_help: "Akteure erstellen auf ihrer Kontoseite ein App-Passwort für diesen Client. Es meldet sie über LDAP und Forward-Auth ohne zweiten Faktor an, nur bei diesem Client."
7
+ notices:
8
+ app_passwords_on: "Akteure können jetzt ein App-Passwort dafür erstellen."
9
+ app_passwords_off: "Seine App-Passwörter werden ab jetzt abgelehnt."
10
+ ldap:
11
+ connect:
12
+ app: "App"
13
+ no_app: "Keine App"
14
+ app_help: "Wählen Sie den Client, als der der Dienst registriert ist. App-Passwörter der Akteure für diesen Client binden dann auf einer Verbindung, die sich zuerst mit diesem Zugang gebunden hat."
15
+ credentials:
16
+ app: "Akzeptiert App-Passwörter für %{name}"