organizations 0.4.2 → 0.5.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 +4 -4
- data/.rubocop.yml +8 -9
- data/.rubocop_todo.yml +625 -0
- data/CHANGELOG.md +53 -0
- data/README.md +414 -46
- data/app/controllers/organizations/application_controller.rb +4 -0
- data/app/controllers/organizations/memberships_controller.rb +3 -3
- data/app/controllers/organizations/public_controller.rb +26 -0
- data/app/mailers/organizations/invitation_mailer.rb +38 -18
- data/app/mailers/organizations/verification_mailer.rb +56 -0
- data/app/models/organizations/allowlist_entry.rb +6 -0
- data/app/models/organizations/domain.rb +6 -0
- data/app/models/organizations/join_code.rb +6 -0
- data/app/models/organizations/join_request.rb +6 -0
- data/app/views/organizations/invitation_mailer/invitation_email.html.erb +16 -13
- data/app/views/organizations/invitation_mailer/invitation_email.text.erb +8 -8
- data/app/views/organizations/verification_mailer/code_email.html.erb +22 -0
- data/app/views/organizations/verification_mailer/code_email.text.erb +7 -0
- data/config/locales/en.yml +158 -0
- data/config/locales/es.yml +126 -0
- data/config/routes.rb +46 -28
- data/gemfiles/rails_7.2.gemfile +3 -3
- data/gemfiles/rails_8.1.gemfile +3 -3
- data/lib/generators/organizations/install/install_generator.rb +3 -0
- data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +170 -25
- data/lib/generators/organizations/install/templates/initializer.rb +53 -0
- data/lib/generators/organizations/upgrade/templates/add_verified_joining_to_organizations.rb.erb +180 -0
- data/lib/generators/organizations/upgrade/upgrade_generator.rb +50 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +51 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +112 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/new.html.erb +10 -0
- data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +99 -0
- data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +87 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/_form.html.erb +39 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/edit.html.erb +10 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +93 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/new.html.erb +10 -0
- data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +270 -0
- data/lib/generators/organizations/views/views_generator.rb +47 -0
- data/lib/organizations/callback_context.rb +9 -0
- data/lib/organizations/callbacks.rb +26 -18
- data/lib/organizations/configuration.rb +275 -1
- data/lib/organizations/controller_helpers.rb +38 -12
- data/lib/organizations/email_normalizer.rb +65 -0
- data/lib/organizations/join_flow.rb +227 -0
- data/lib/organizations/join_state.rb +117 -0
- data/lib/organizations/metadata_flags.rb +79 -0
- data/lib/organizations/models/allowlist_entry.rb +88 -0
- data/lib/organizations/models/concerns/has_organizations.rb +100 -12
- data/lib/organizations/models/domain.rb +85 -0
- data/lib/organizations/models/invitation.rb +105 -17
- data/lib/organizations/models/join_code.rb +242 -0
- data/lib/organizations/models/join_request.rb +627 -0
- data/lib/organizations/models/membership.rb +39 -8
- data/lib/organizations/models/organization.rb +317 -19
- data/lib/organizations/organization_scoped.rb +135 -0
- data/lib/organizations/test_helpers.rb +35 -1
- data/lib/organizations/version.rb +1 -1
- data/lib/organizations/view_helpers.rb +27 -19
- data/lib/organizations.rb +165 -0
- metadata +34 -2
|
@@ -142,16 +142,16 @@ module Organizations
|
|
|
142
142
|
# Can't change your own role
|
|
143
143
|
if @membership.user_id == current_user.id
|
|
144
144
|
raise Organizations::NotAuthorized.new(
|
|
145
|
-
"
|
|
145
|
+
Organizations.t(:"errors.cannot_change_own_role"),
|
|
146
146
|
permission: :edit_member_roles,
|
|
147
147
|
organization: current_organization,
|
|
148
148
|
user: current_user
|
|
149
149
|
)
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
# Validate the new role is valid
|
|
152
|
+
# Validate the new role is valid (user-reachable via form params, so localized)
|
|
153
153
|
unless Roles.valid_role?(new_role)
|
|
154
|
-
raise Organizations::Error, "
|
|
154
|
+
raise Organizations::Error, Organizations.t(:"errors.invalid_role", role: new_role)
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
# Note: Owner promotion/demotion rules are enforced by Organization#change_role_of!
|
|
@@ -28,6 +28,24 @@ module Organizations
|
|
|
28
28
|
# Minimal helpers needed for public routes
|
|
29
29
|
helper_method :current_user if respond_to?(:helper_method)
|
|
30
30
|
|
|
31
|
+
# Host helper decoration (config.public_controller_helpers): this
|
|
32
|
+
# controller deliberately does NOT inherit the host ApplicationController
|
|
33
|
+
# (to dodge auth filters), which also means host layouts rendered by
|
|
34
|
+
# public pages (invitation acceptance in the app's devise/marketing
|
|
35
|
+
# layout) are missing the app's helper modules. Both known production
|
|
36
|
+
# hosts hand-patched exactly this in an initializer — declare it instead:
|
|
37
|
+
#
|
|
38
|
+
# config.public_controller_helpers = ["ApplicationHelper", "PageHelper"]
|
|
39
|
+
#
|
|
40
|
+
# Strings are constantized HERE, at controller-class load time (after
|
|
41
|
+
# initializers, reload-safe), so the initializer never has to reference
|
|
42
|
+
# autoloadable constants directly.
|
|
43
|
+
if respond_to?(:helper)
|
|
44
|
+
Organizations.configuration.public_controller_helpers.each do |helper_module|
|
|
45
|
+
helper(helper_module.is_a?(Module) ? helper_module : helper_module.to_s.constantize)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
31
49
|
private
|
|
32
50
|
|
|
33
51
|
# Returns the current user from the host application (if any).
|
|
@@ -59,3 +77,11 @@ module Organizations
|
|
|
59
77
|
helper_method :main_app if respond_to?(:helper_method)
|
|
60
78
|
end
|
|
61
79
|
end
|
|
80
|
+
|
|
81
|
+
# Host extension seam — see the load-hooks note in
|
|
82
|
+
# lib/organizations/models/organization.rb. Typical use: decorating this
|
|
83
|
+
# controller with host helper modules its layout needs (it inherits
|
|
84
|
+
# ActionController::Base, NOT the host ApplicationController, so app helpers
|
|
85
|
+
# aren't present by default). Prefer config.public_controller_helpers for
|
|
86
|
+
# that common case; this hook is for arbitrary decoration.
|
|
87
|
+
ActiveSupport.run_load_hooks(:organizations_public_controller, Organizations::PublicController)
|
|
@@ -11,6 +11,13 @@ module Organizations
|
|
|
11
11
|
# InvitationMailer.invitation_email(invitation).deliver_later
|
|
12
12
|
#
|
|
13
13
|
class InvitationMailer < ActionMailer::Base
|
|
14
|
+
# Self-register the gem's app/views so this mailer renders OUTSIDE a full
|
|
15
|
+
# Rails app too (plain-ActiveRecord consumers, this gem's own test env).
|
|
16
|
+
# Under Rails the engine already provides the path — appending a
|
|
17
|
+
# duplicate is harmless (lowest precedence), and host template overrides
|
|
18
|
+
# in the app's own app/views still win.
|
|
19
|
+
append_view_path File.expand_path("../../views", __dir__)
|
|
20
|
+
|
|
14
21
|
default from: -> { default_from_address }
|
|
15
22
|
|
|
16
23
|
# Invitation email
|
|
@@ -20,18 +27,23 @@ module Organizations
|
|
|
20
27
|
@invitation = invitation
|
|
21
28
|
@organization = invitation.organization
|
|
22
29
|
@inviter = invitation.invited_by
|
|
30
|
+
@inviter_name = inviter_name
|
|
23
31
|
@accept_url = invitation_accept_url(invitation)
|
|
32
|
+
# Pre-formatted in the mailer (not the templates) so the strftime
|
|
33
|
+
# fallback lives in exactly one place — see #format_expiry.
|
|
34
|
+
@expires_on = @invitation.expires_at ? format_expiry(@invitation.expires_at) : nil
|
|
24
35
|
|
|
25
36
|
mail(
|
|
26
37
|
to: invitation.email,
|
|
27
|
-
subject: "
|
|
38
|
+
subject: Organizations.t(:"mailers.invitation.subject",
|
|
39
|
+
inviter: @inviter_name, organization: @organization.name)
|
|
28
40
|
)
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
private
|
|
32
44
|
|
|
33
45
|
def inviter_name
|
|
34
|
-
return "
|
|
46
|
+
return Organizations.t(:"mailers.from_team") unless @inviter
|
|
35
47
|
|
|
36
48
|
if @inviter.respond_to?(:name) && @inviter.name.present?
|
|
37
49
|
@inviter.name
|
|
@@ -40,25 +52,33 @@ module Organizations
|
|
|
40
52
|
end
|
|
41
53
|
end
|
|
42
54
|
|
|
55
|
+
# Localize the expiry timestamp when the host has date/time translations
|
|
56
|
+
# (rails-i18n or its own time.formats.long); otherwise fall back to the
|
|
57
|
+
# historical English strftime. Bare i18n (this gem's own test env, plain
|
|
58
|
+
# Ruby consumers) has no time formats, and I18n.l raises in that case —
|
|
59
|
+
# never let a missing date format break invitation delivery.
|
|
60
|
+
def format_expiry(time)
|
|
61
|
+
I18n.l(time, format: :long)
|
|
62
|
+
rescue I18n::MissingTranslationData, I18n::ArgumentError
|
|
63
|
+
time.strftime("%B %d, %Y at %I:%M %p %Z")
|
|
64
|
+
end
|
|
65
|
+
|
|
43
66
|
def invitation_accept_url(invitation)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else
|
|
56
|
-
"/invitations/#{invitation.token}"
|
|
57
|
-
end
|
|
67
|
+
# ONE implementation for acceptance URLs: Invitation#acceptance_url
|
|
68
|
+
# (mount-point aware via Organizations.engine_mount_path). The previous
|
|
69
|
+
# engine-url_helpers attempt here was mount-UNAWARE — raw engine route
|
|
70
|
+
# helpers don't know where the host mounted the engine, so links broke
|
|
71
|
+
# for any non-root mount.
|
|
72
|
+
invitation.acceptance_url(base_url: full_rails_app? ? default_host : "")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# SSOT for the bare-`Rails`-module guard — see Organizations.full_rails_app?.
|
|
76
|
+
def full_rails_app?
|
|
77
|
+
Organizations.full_rails_app?
|
|
58
78
|
end
|
|
59
79
|
|
|
60
80
|
def default_from_address
|
|
61
|
-
if
|
|
81
|
+
if full_rails_app? && Rails.application.config.action_mailer&.default_options
|
|
62
82
|
Rails.application.config.action_mailer.default_options[:from] || "noreply@example.com"
|
|
63
83
|
else
|
|
64
84
|
"noreply@example.com"
|
|
@@ -66,7 +86,7 @@ module Organizations
|
|
|
66
86
|
end
|
|
67
87
|
|
|
68
88
|
def default_host
|
|
69
|
-
if
|
|
89
|
+
if full_rails_app? && Rails.application.config.action_mailer&.default_url_options
|
|
70
90
|
options = Rails.application.config.action_mailer.default_url_options
|
|
71
91
|
protocol = options[:protocol] || "https"
|
|
72
92
|
host = options[:host] || "localhost"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Organizations
|
|
4
|
+
# Mailer for emailed verification codes (verified joining).
|
|
5
|
+
# Can be customized via Organizations.configuration.verification_mailer —
|
|
6
|
+
# custom mailers must implement `code_email(join_request, code)`.
|
|
7
|
+
#
|
|
8
|
+
# SECURITY: `code` is the plaintext one-time code. It exists only in this
|
|
9
|
+
# delivery path — the database stores a digest (see JoinRequest).
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# VerificationMailer.code_email(join_request, "492817").deliver_later
|
|
13
|
+
#
|
|
14
|
+
class VerificationMailer < ActionMailer::Base
|
|
15
|
+
# Self-register the gem's app/views — see InvitationMailer for rationale.
|
|
16
|
+
append_view_path File.expand_path("../../views", __dir__)
|
|
17
|
+
|
|
18
|
+
default from: -> { default_from_address }
|
|
19
|
+
|
|
20
|
+
# Verification code email
|
|
21
|
+
# @param join_request [Organizations::JoinRequest]
|
|
22
|
+
# @param code [String] the plaintext 6-digit code
|
|
23
|
+
# @return [Mail::Message]
|
|
24
|
+
def code_email(join_request, code)
|
|
25
|
+
@join_request = join_request
|
|
26
|
+
@organization = join_request.organization
|
|
27
|
+
@code = code
|
|
28
|
+
@expires_in_minutes = ttl_minutes
|
|
29
|
+
|
|
30
|
+
mail(
|
|
31
|
+
to: join_request.verification_email,
|
|
32
|
+
subject: Organizations.t(:"mailers.verification.subject",
|
|
33
|
+
code: @code, organization: @organization.name)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def ttl_minutes
|
|
40
|
+
ttl = Organizations.configuration.verification_code_ttl
|
|
41
|
+
(ttl.to_i / 60.0).ceil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def default_from_address
|
|
45
|
+
# Deliberately identical to InvitationMailer#default_from_address —
|
|
46
|
+
# keep the two in sync. The bare-`Rails`-module guard lives in ONE
|
|
47
|
+
# place: Organizations.full_rails_app?.
|
|
48
|
+
if Organizations.full_rails_app? &&
|
|
49
|
+
Rails.application.config.action_mailer&.default_options
|
|
50
|
+
Rails.application.config.action_mailer.default_options[:from] || "noreply@example.com"
|
|
51
|
+
else
|
|
52
|
+
"noreply@example.com"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reloadable entrypoint for Organizations::AllowlistEntry in Rails apps.
|
|
4
|
+
# This file lives in app/models so Zeitwerk manages it, making the class
|
|
5
|
+
# reload-safe. It delegates to the canonical implementation in lib/.
|
|
6
|
+
load File.expand_path("../../../lib/organizations/models/allowlist_entry.rb", __dir__)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reloadable entrypoint for Organizations::Domain in Rails apps.
|
|
4
|
+
# This file lives in app/models so Zeitwerk manages it, making the class
|
|
5
|
+
# reload-safe. It delegates to the canonical implementation in lib/.
|
|
6
|
+
load File.expand_path("../../../lib/organizations/models/domain.rb", __dir__)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reloadable entrypoint for Organizations::JoinCode in Rails apps.
|
|
4
|
+
# This file lives in app/models so Zeitwerk manages it, making the class
|
|
5
|
+
# reload-safe. It delegates to the canonical implementation in lib/.
|
|
6
|
+
load File.expand_path("../../../lib/organizations/models/join_code.rb", __dir__)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reloadable entrypoint for Organizations::JoinRequest in Rails apps.
|
|
4
|
+
# This file lives in app/models so Zeitwerk manages it, making the class
|
|
5
|
+
# reload-safe. It delegates to the canonical implementation in lib/.
|
|
6
|
+
load File.expand_path("../../../lib/organizations/models/join_request.rb", __dir__)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title
|
|
6
|
+
<title><%= Organizations.t(:"mailers.invitation.title", organization: @organization.name) %></title>
|
|
7
7
|
<style>
|
|
8
8
|
body {
|
|
9
9
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -53,45 +53,48 @@
|
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div class="header">
|
|
56
|
-
<h1
|
|
56
|
+
<h1><%= Organizations.t(:"mailers.invitation.header") %></h1>
|
|
57
57
|
</div>
|
|
58
58
|
|
|
59
59
|
<div class="content">
|
|
60
|
-
<p
|
|
60
|
+
<p><%= Organizations.t(:"mailers.invitation.greeting") %></p>
|
|
61
61
|
|
|
62
62
|
<p>
|
|
63
|
+
<%# The *_html keys go through the view `t` helper: its _html-suffix
|
|
64
|
+
convention marks the translation html_safe while HTML-escaping every
|
|
65
|
+
interpolated value (inviter/org names are user input). Source:
|
|
66
|
+
https://guides.rubyonrails.org/i18n.html#using-safe-html-translations %>
|
|
63
67
|
<% if @inviter %>
|
|
64
|
-
|
|
65
|
-
has invited you to join <strong><%= @organization.name %></strong>.
|
|
68
|
+
<%= t("organizations.mailers.invitation.invited_by_html", inviter: @inviter_name, organization: @organization.name) %>
|
|
66
69
|
<% else %>
|
|
67
|
-
|
|
70
|
+
<%= t("organizations.mailers.invitation.invited_generic_html", organization: @organization.name) %>
|
|
68
71
|
<% end %>
|
|
69
72
|
</p>
|
|
70
73
|
|
|
71
74
|
<p>
|
|
72
|
-
|
|
73
|
-
<span class="role-badge"><%= @invitation.role.to_s.capitalize %></span>
|
|
75
|
+
<%= Organizations.t(:"mailers.invitation.joining_as_label") %>
|
|
76
|
+
<span class="role-badge"><%= Organizations.t(:"roles.#{@invitation.role}", default: @invitation.role.to_s.capitalize) %></span>
|
|
74
77
|
</p>
|
|
75
78
|
|
|
76
79
|
<p style="text-align: center;">
|
|
77
|
-
<a href="<%= @accept_url %>" class="button"
|
|
80
|
+
<a href="<%= @accept_url %>" class="button"><%= Organizations.t(:"mailers.invitation.accept_button") %></a>
|
|
78
81
|
</p>
|
|
79
82
|
|
|
80
83
|
<p>
|
|
81
|
-
|
|
84
|
+
<%= Organizations.t(:"mailers.invitation.copy_link") %><br>
|
|
82
85
|
<a href="<%= @accept_url %>"><%= @accept_url %></a>
|
|
83
86
|
</p>
|
|
84
87
|
|
|
85
|
-
<% if @
|
|
88
|
+
<% if @expires_on %>
|
|
86
89
|
<p style="color: #666; font-size: 14px;">
|
|
87
|
-
|
|
90
|
+
<%= Organizations.t(:"mailers.invitation.expires_on", date: @expires_on) %>
|
|
88
91
|
</p>
|
|
89
92
|
<% end %>
|
|
90
93
|
</div>
|
|
91
94
|
|
|
92
95
|
<div class="footer">
|
|
93
96
|
<p>
|
|
94
|
-
|
|
97
|
+
<%= Organizations.t(:"mailers.invitation.ignore") %>
|
|
95
98
|
</p>
|
|
96
99
|
</div>
|
|
97
100
|
</body>
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
<%= Organizations.t(:"mailers.invitation.title", organization: @organization.name) %>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<%= Organizations.t(:"mailers.invitation.greeting") %>
|
|
4
4
|
|
|
5
|
-
<% if @inviter %><%=
|
|
5
|
+
<% if @inviter %><%= Organizations.t(:"mailers.invitation.invited_by", inviter: @inviter_name, organization: @organization.name) %><% else %><%= Organizations.t(:"mailers.invitation.invited_generic", organization: @organization.name) %><% end %>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<%= Organizations.t(:"mailers.invitation.joining_as", role: Organizations.t(:"roles.#{@invitation.role}", default: @invitation.role.to_s.capitalize)) %>
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<%= Organizations.t(:"mailers.invitation.accept_cta") %>
|
|
10
10
|
<%= @accept_url %>
|
|
11
11
|
|
|
12
|
-
<% if @
|
|
13
|
-
|
|
12
|
+
<% if @expires_on %>
|
|
13
|
+
<%= Organizations.t(:"mailers.invitation.expires_on", date: @expires_on) %>
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
<%= Organizations.t(:"mailers.invitation.ignore") %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
|
5
|
+
</head>
|
|
6
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #111827; margin: 0; padding: 24px;">
|
|
7
|
+
<%# _html key via the view `t` helper: html_safe translation, escaped
|
|
8
|
+
interpolations (the org name is user input). Source:
|
|
9
|
+
https://guides.rubyonrails.org/i18n.html#using-safe-html-translations %>
|
|
10
|
+
<p><%= t("organizations.mailers.verification.lead_html", organization: @organization.name) %></p>
|
|
11
|
+
|
|
12
|
+
<p style="font-size: 32px; font-weight: 700; letter-spacing: 6px; background: #f3f4f6; border-radius: 8px; display: inline-block; padding: 12px 24px;">
|
|
13
|
+
<%= @code %>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p><%= Organizations.t(:"mailers.verification.expires", minutes: @expires_in_minutes) %></p>
|
|
17
|
+
|
|
18
|
+
<p style="color: #6b7280; font-size: 14px;">
|
|
19
|
+
<%= Organizations.t(:"mailers.verification.ignore", organization: @organization.name) %>
|
|
20
|
+
</p>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= Organizations.t(:"mailers.verification.lead", organization: @organization.name) %>
|
|
2
|
+
|
|
3
|
+
<%= @code %>
|
|
4
|
+
|
|
5
|
+
<%= Organizations.t(:"mailers.verification.expires", minutes: @expires_in_minutes) %>
|
|
6
|
+
|
|
7
|
+
<%= Organizations.t(:"mailers.verification.ignore", organization: @organization.name) %>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Organizations gem — English (default) locale.
|
|
2
|
+
#
|
|
3
|
+
# This file is the SINGLE SOURCE OF TRUTH for every user-facing string the
|
|
4
|
+
# gem produces: error messages raised by the models, role/status labels used
|
|
5
|
+
# by the view helpers, model validation messages, and the stock mailer copy.
|
|
6
|
+
#
|
|
7
|
+
# Hosts override any of these the standard Rails way: define the same keys in
|
|
8
|
+
# the app's own locale files (app files load after engine files, so they win).
|
|
9
|
+
# See README "Internationalization (i18n)".
|
|
10
|
+
#
|
|
11
|
+
# ⚠️ Developer-facing errors (ArgumentError, ConfigurationError, wrong-API
|
|
12
|
+
# usage like "user is required") are deliberately NOT translated — they are
|
|
13
|
+
# for programmers, and English keeps them greppable/searchable.
|
|
14
|
+
#
|
|
15
|
+
# ⚠️ Keys under `errors:` are 1:1 with raise sites in lib/organizations/**.
|
|
16
|
+
# If you add a raise site, add its key HERE (and to es.yml) — a missing key
|
|
17
|
+
# renders as "Translation missing: ..." which is a loud, findable bug.
|
|
18
|
+
en:
|
|
19
|
+
organizations:
|
|
20
|
+
# Role labels (used by organization_role_label / organization_role_info)
|
|
21
|
+
roles:
|
|
22
|
+
owner: "Owner"
|
|
23
|
+
admin: "Admin"
|
|
24
|
+
member: "Member"
|
|
25
|
+
viewer: "Viewer"
|
|
26
|
+
|
|
27
|
+
# Invitation status labels (organization_invitation_status_label)
|
|
28
|
+
invitation_status:
|
|
29
|
+
pending: "Pending"
|
|
30
|
+
accepted: "Accepted"
|
|
31
|
+
expired: "Expired"
|
|
32
|
+
|
|
33
|
+
# Join request statuses — lowercase on purpose: interpolated mid-sentence
|
|
34
|
+
# into errors.join_request_already_decided ("...has already been %{status}").
|
|
35
|
+
join_request_status:
|
|
36
|
+
pending: "pending"
|
|
37
|
+
approved: "approved"
|
|
38
|
+
rejected: "rejected"
|
|
39
|
+
withdrawn: "withdrawn"
|
|
40
|
+
expired: "expired"
|
|
41
|
+
|
|
42
|
+
errors:
|
|
43
|
+
# — Join codes —
|
|
44
|
+
join_code_invalid: "This code is not valid"
|
|
45
|
+
join_code_exhausted: "This code has reached its usage limit"
|
|
46
|
+
|
|
47
|
+
# — Join requests —
|
|
48
|
+
join_request_expired: "This join request has expired"
|
|
49
|
+
join_request_already_decided: "This join request has already been %{status}"
|
|
50
|
+
join_request_already_member: "You are already a member of this organization"
|
|
51
|
+
join_request_create_failed: "Could not create join request"
|
|
52
|
+
join_request_membership_gone: "Request was approved but the membership no longer exists"
|
|
53
|
+
|
|
54
|
+
# — Membership veto gate (on_member_joining raising MembershipVetoed) —
|
|
55
|
+
# Default shown when the host's veto raises without its own message.
|
|
56
|
+
membership_vetoed: "You can't join this organization right now"
|
|
57
|
+
|
|
58
|
+
# — Email verification challenge —
|
|
59
|
+
verification_email_invalid: "This is not a valid email address"
|
|
60
|
+
verification_email_not_eligible: "This email address is not eligible to join this organization"
|
|
61
|
+
verification_email_already_claimed: "This email address is already associated with a member of this organization"
|
|
62
|
+
verification_code_invalid: "The code is incorrect"
|
|
63
|
+
verification_code_missing: "No verification code is active for this request"
|
|
64
|
+
verification_code_expired: "This code has expired — request a new one"
|
|
65
|
+
verification_attempts_exceeded: "Too many incorrect attempts — request a new code"
|
|
66
|
+
verification_sends_exceeded: "Too many codes requested for this request"
|
|
67
|
+
verification_resend_throttled: "Please wait before requesting another code"
|
|
68
|
+
account_email_trust_disabled: "Account-email trust is disabled (see config.trust_confirmed_account_email)"
|
|
69
|
+
account_email_unconfirmed: "The account email has not been confirmed"
|
|
70
|
+
not_accepting_requests: "This organization is not accepting join requests right now"
|
|
71
|
+
|
|
72
|
+
# — Invitations —
|
|
73
|
+
invitation_expired: "This invitation has expired"
|
|
74
|
+
invitation_already_accepted: "This invitation has already been accepted"
|
|
75
|
+
invitation_cannot_resend_accepted: "Cannot resend an accepted invitation"
|
|
76
|
+
invitation_email_mismatch: "This invitation was sent to a different email address"
|
|
77
|
+
invitation_accept_as_owner: "Cannot accept invitation as owner. Invite as admin, then use transfer_ownership_to! after joining."
|
|
78
|
+
invitation_already_member: "User is already a member of this organization"
|
|
79
|
+
invite_as_owner: "Cannot invite as owner. Invite as admin, then use transfer_ownership_to! after they join."
|
|
80
|
+
invite_not_a_member: "Only organization members can send invitations"
|
|
81
|
+
invite_not_authorized: "You don't have permission to invite members"
|
|
82
|
+
|
|
83
|
+
# — Ownership / role invariants (reachable from admin UIs) —
|
|
84
|
+
cannot_remove_owner: "Cannot remove the organization owner. Transfer ownership first."
|
|
85
|
+
cannot_add_as_owner: "Cannot add member as owner. Use transfer_ownership_to! instead."
|
|
86
|
+
cannot_promote_to_owner: "Cannot promote to owner. Use transfer_ownership_to! instead."
|
|
87
|
+
cannot_demote_owner_directly: "Cannot demote owner directly. Use transfer_ownership_to! instead."
|
|
88
|
+
cannot_demote_owner: "Cannot demote owner. Transfer ownership first."
|
|
89
|
+
promote_not_higher: "Cannot promote to %{new_role} - it's not a higher role than %{role}"
|
|
90
|
+
demote_not_lower: "Cannot demote to %{new_role} - it's not a lower role than %{role}"
|
|
91
|
+
transfer_no_owner: "Cannot transfer ownership because organization has no owner membership"
|
|
92
|
+
transfer_to_non_member: "Cannot transfer ownership to a non-member"
|
|
93
|
+
transfer_to_non_admin: "Cannot transfer ownership to non-admin. Promote them to admin first."
|
|
94
|
+
|
|
95
|
+
# — Leaving / user-level —
|
|
96
|
+
cannot_leave_as_last_owner: "Cannot leave organization as the only owner. Transfer ownership first."
|
|
97
|
+
cannot_leave_last_organization: "Cannot leave your only organization"
|
|
98
|
+
organization_limit_reached: "Maximum number of organizations (%{max}) reached"
|
|
99
|
+
no_current_organization_to_leave: "No current organization to leave"
|
|
100
|
+
cannot_delete_user_owns_organizations: "Cannot delete a user who still owns organizations. Transfer ownership or delete those organizations first."
|
|
101
|
+
|
|
102
|
+
# — Controller guards (default unauthorized copy) —
|
|
103
|
+
unauthorized_role: "You need %{role} access to perform this action"
|
|
104
|
+
unauthorized_permission: "You don't have permission to %{permission}"
|
|
105
|
+
unauthorized: "You are not authorized to perform this action"
|
|
106
|
+
not_a_member: "You are not a member of this organization"
|
|
107
|
+
organization_required: "Organization required"
|
|
108
|
+
cannot_change_own_role: "You cannot change your own role"
|
|
109
|
+
invalid_role: "Invalid role: %{role}"
|
|
110
|
+
|
|
111
|
+
# Flash notices produced by the controller-helper invitation flow.
|
|
112
|
+
notices:
|
|
113
|
+
welcome: "Welcome to %{organization}!"
|
|
114
|
+
already_member: "You're already a member of %{organization}."
|
|
115
|
+
select_or_create_organization: "Please select or create an organization."
|
|
116
|
+
|
|
117
|
+
# Model validation messages (errors.add second argument). These read as
|
|
118
|
+
# "<Attribute> <message>" in full_messages — keep them lowercase fragments.
|
|
119
|
+
attributes:
|
|
120
|
+
membership_taken: "is already a member of this organization"
|
|
121
|
+
owner_taken: "owner already exists for this organization"
|
|
122
|
+
pending_request_taken: "already has a pending request for this organization"
|
|
123
|
+
invitation_taken: "has already been invited to this organization"
|
|
124
|
+
allowlist_taken: "is already on this organization's allowlist"
|
|
125
|
+
domain_invalid: "is not a valid domain name"
|
|
126
|
+
|
|
127
|
+
# Stock mailer copy. Hosts that swap in their own mailer
|
|
128
|
+
# (config.invitation_mailer / config.verification_mailer) ignore all of
|
|
129
|
+
# this; hosts that keep the stock mailers can retheme by overriding keys.
|
|
130
|
+
mailers:
|
|
131
|
+
from_team: "The team"
|
|
132
|
+
invitation:
|
|
133
|
+
subject: "%{inviter} invited you to join %{organization}"
|
|
134
|
+
header: "You're invited!"
|
|
135
|
+
title: "You're invited to join %{organization}!"
|
|
136
|
+
greeting: "Hi there,"
|
|
137
|
+
invited_by: "%{inviter} has invited you to join %{organization}."
|
|
138
|
+
# _html variants: rendered through the view `t` helper, whose *_html
|
|
139
|
+
# convention marks the string html_safe and HTML-escapes every
|
|
140
|
+
# interpolated argument (Rails i18n guide §4.1.2 "HTML translations",
|
|
141
|
+
# https://guides.rubyonrails.org/i18n.html) — keep markup here, never
|
|
142
|
+
# in the interpolated values.
|
|
143
|
+
invited_by_html: "<strong>%{inviter}</strong> has invited you to join <strong>%{organization}</strong>."
|
|
144
|
+
invited_generic: "You've been invited to join %{organization}."
|
|
145
|
+
invited_generic_html: "You've been invited to join <strong>%{organization}</strong>."
|
|
146
|
+
joining_as: "You'll be joining as: %{role}"
|
|
147
|
+
joining_as_label: "You'll be joining as:"
|
|
148
|
+
accept_cta: "Accept your invitation here:"
|
|
149
|
+
accept_button: "Accept Invitation"
|
|
150
|
+
copy_link: "Or copy and paste this link into your browser:"
|
|
151
|
+
expires_on: "This invitation will expire on %{date}."
|
|
152
|
+
ignore: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
153
|
+
verification:
|
|
154
|
+
subject: "%{code} is your %{organization} verification code"
|
|
155
|
+
lead: "Use this code to verify your email address and join %{organization}:"
|
|
156
|
+
lead_html: "Use this code to verify your email address and join <strong>%{organization}</strong>:"
|
|
157
|
+
expires: "The code expires in %{minutes} minutes."
|
|
158
|
+
ignore: "If you didn't request this code, you can safely ignore this email — nobody can join %{organization} with your address without it."
|