organizations 0.4.3 → 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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -9
  3. data/.rubocop_todo.yml +625 -0
  4. data/CHANGELOG.md +46 -0
  5. data/README.md +405 -41
  6. data/app/controllers/organizations/application_controller.rb +4 -0
  7. data/app/controllers/organizations/memberships_controller.rb +3 -3
  8. data/app/controllers/organizations/public_controller.rb +26 -0
  9. data/app/mailers/organizations/invitation_mailer.rb +38 -18
  10. data/app/mailers/organizations/verification_mailer.rb +56 -0
  11. data/app/models/organizations/allowlist_entry.rb +6 -0
  12. data/app/models/organizations/domain.rb +6 -0
  13. data/app/models/organizations/join_code.rb +6 -0
  14. data/app/models/organizations/join_request.rb +6 -0
  15. data/app/views/organizations/invitation_mailer/invitation_email.html.erb +16 -13
  16. data/app/views/organizations/invitation_mailer/invitation_email.text.erb +8 -8
  17. data/app/views/organizations/verification_mailer/code_email.html.erb +22 -0
  18. data/app/views/organizations/verification_mailer/code_email.text.erb +7 -0
  19. data/config/locales/en.yml +158 -0
  20. data/config/locales/es.yml +126 -0
  21. data/config/routes.rb +46 -28
  22. data/gemfiles/rails_7.2.gemfile +3 -3
  23. data/gemfiles/rails_8.1.gemfile +3 -3
  24. data/lib/generators/organizations/install/install_generator.rb +3 -0
  25. data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +170 -25
  26. data/lib/generators/organizations/install/templates/initializer.rb +53 -0
  27. data/lib/generators/organizations/upgrade/templates/add_verified_joining_to_organizations.rb.erb +180 -0
  28. data/lib/generators/organizations/upgrade/upgrade_generator.rb +50 -0
  29. data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +51 -0
  30. data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +112 -0
  31. data/lib/generators/organizations/views/templates/organizations/invitations/new.html.erb +10 -0
  32. data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +99 -0
  33. data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +87 -0
  34. data/lib/generators/organizations/views/templates/organizations/organizations/_form.html.erb +39 -0
  35. data/lib/generators/organizations/views/templates/organizations/organizations/edit.html.erb +10 -0
  36. data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +93 -0
  37. data/lib/generators/organizations/views/templates/organizations/organizations/new.html.erb +10 -0
  38. data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +270 -0
  39. data/lib/generators/organizations/views/views_generator.rb +47 -0
  40. data/lib/organizations/callback_context.rb +9 -0
  41. data/lib/organizations/callbacks.rb +26 -18
  42. data/lib/organizations/configuration.rb +275 -1
  43. data/lib/organizations/controller_helpers.rb +38 -12
  44. data/lib/organizations/email_normalizer.rb +65 -0
  45. data/lib/organizations/join_flow.rb +227 -0
  46. data/lib/organizations/join_state.rb +117 -0
  47. data/lib/organizations/metadata_flags.rb +79 -0
  48. data/lib/organizations/models/allowlist_entry.rb +88 -0
  49. data/lib/organizations/models/concerns/has_organizations.rb +100 -12
  50. data/lib/organizations/models/domain.rb +85 -0
  51. data/lib/organizations/models/invitation.rb +105 -17
  52. data/lib/organizations/models/join_code.rb +242 -0
  53. data/lib/organizations/models/join_request.rb +627 -0
  54. data/lib/organizations/models/membership.rb +39 -8
  55. data/lib/organizations/models/organization.rb +317 -19
  56. data/lib/organizations/organization_scoped.rb +135 -0
  57. data/lib/organizations/test_helpers.rb +35 -1
  58. data/lib/organizations/version.rb +1 -1
  59. data/lib/organizations/view_helpers.rb +8 -12
  60. data/lib/organizations.rb +165 -0
  61. metadata +34 -2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
1
+ ## [0.5.0] - 2026-07-16
2
+
3
+ **Verified Joining** — users can now join organizations by proving they belong, closing three roadmap items (domain-based joining, request-to-join, roster/bulk import). Fully additive: existing installs change nothing until they enroll a domain, generate a code, or import a roster. Run `rails g organizations:upgrade` + `rails db:migrate` to get the new tables/columns.
4
+
5
+ ### Added
6
+
7
+ - **Join requests** (`Organizations::JoinRequest`) — the mirror image of invitations (user→org). Memberships stay active-only; all pending state lives on the request (`pending → approved | rejected | withdrawn`, `:expired` derived like invitations). `user.request_to_join!(org)`, `org.approve_join_request!/reject_join_request!`, `request.withdraw!`. One open request per (org, user), DB-enforced.
8
+ - **Email domains** (`Organizations::Domain`) — `org.add_domain!("corp.com")`. Exact, dot-boundary-safe matching (subdomains are separate domains; lookalike and multi-@ evasion shapes rejected).
9
+ - **Emailed-code verification** — `request.start_email_verification!(email:)` + `request.verify_email_code!(code)`. Codes are 6 digits, stored as SHA-256 digests only (peppered by row id), single-use, TTL'd (15 min default), attempt-capped (5), resend-throttled. The proven address can differ from the account email — recorded as `verified_email` on the membership, **unique per organization** (one proven inbox = one member).
10
+ - **Join codes** (`Organizations::JoinCode`) — globally-unique shareable PINs: `org.generate_join_code!(label:, requires_verified_domain_email:, auto_approve:, expires_at:, max_uses:)`, `JoinCode.redeem(code, user:)`, `code.revoke!` (rotation = revoke + regenerate). `requires_verified_domain_email` chains the email challenge per-code ("reinforced" level); `auto_approve: false` parks redemptions for manual approval. Race-safe use accounting.
11
+ - **Allowlists / rosters** (`Organizations::AllowlistEntry`) — `org.import_allowlist!(emails, source:, membership_metadata:)` (idempotent). Rostered addresses still complete the email challenge (a leaked roster grants nothing without inbox access); entries are claimed on join.
12
+ - **Account-email trust shortcut** — `org.join_with_account_email!(user)`: when the host user's account email is confirmed (e.g. Devise `:confirmable`) and its domain is enrolled, joining needs no code. Gate with `config.trust_confirmed_account_email`.
13
+ - **Membership provenance** — memberships gain `joined_via` (`invited|code|domain_email|allowlist|manual`), `verified_email(_normalized)`, `verified_at`, plus `Membership#verified?`. Invitation acceptance now stamps provenance too (accepting the emailed token proves the inbox).
14
+ - **`membership_metadata` copy-through** — domains, join codes, allowlist entries, and invitations carry a `membership_metadata` hash merged onto memberships they create (cohort tags like `{ member_kind: "student" }`) — the gem never interprets it. Invitations also gained a `metadata` column (parity with other tables).
15
+ - **Callbacks** — `on_join_request_created`, `on_join_request_approved` (with `decided_by`, nil for auto-approvals), `on_join_request_rejected`.
16
+ - **Config** — `verification_mailer`, `verification_code_ttl`, `verification_max_attempts`, `verification_resend_interval`, `verification_max_sends`, `verification_email_normalizer` (default collapses case/whitespace/+tags), `trust_confirmed_account_email`, `join_request_expiry`, `join_code_generator`.
17
+ - **`Organizations::VerificationMailer`** (+ HTML/text templates), `Organizations::EmailNormalizer`, `organizations:upgrade` generator.
18
+
19
+ **Pre-release hardening wave** (added before the public 0.5.0 cut, driven by the first two production integrations):
20
+
21
+ - **`on_member_joining` — THE membership gate**: a strict, vetoing, pre-persist callback dispatched inside the creating transaction on EVERY join path (`add_member!`, invitation acceptance, join-request approval — which covers codes, domains, allowlists, and the account-email shortcut). Raise `Organizations::MembershipVetoed` (localized default message) to veto; rollback is clean (requests stay pending, invitations stay unaccepted). This is where plan seat limits and member caps belong — the previous "enforce caps before calling approve/redeem in host code" guidance is superseded. Deliberately does NOT fire for owner-at-org-creation, idempotent already-a-member paths, or role changes.
22
+ - **`Organizations::JoinFlow`** — the controller-facing facade: `JoinFlow.attempt(user:, organization:, code:, email:, verification_code:, message:)` returns a `Result` (`outcome` ∈ member/challenge_sent/pending/vetoed/error; stable `reason` symbols from `JoinFlow::REASONS`; localized `message`) instead of raising — no more hand-written rescue ladders in host controllers. Foreign/unknown/revoked/expired codes collapse into one reason (no enumeration oracle) and a foreign code is never consumed.
23
+ - **`Organizations::JoinState`** — the headless join-screen state machine: `:member`/`:verifying`/`:pending`/`:entry`, `resend_seconds` (derived from config — stop mirroring the throttle as a magic number), `error_message`. Plus granular capability predicates `Organization#accepts_domain_joining?`/`#accepts_code_joining?` and the `JoinCode.active` scope. `accepts_join_requests?` refined: codes count only while actually redeemable.
24
+ - **i18n layer** — every user-facing string (errors, validation messages, role/status labels, invitation-flow notices, stock mailer copy) resolves through `config/locales/en.yml` (catalog SSOT) with a full **Spanish (`es`) locale shipped**. Hosts override any key in their own locale files. Developer-facing errors stay English on purpose.
25
+ - **`config.user_class`** — hosts whose account model isn't `User` (e.g. `Account`) are now supported; every gem association resolves through it. `TestHelpers#create_user` follows it.
26
+ - **Extension load hooks** — `ActiveSupport.on_load(:organizations_organization) { include MyExtensions }` (one hook per model + `:organizations_application_controller`/`:organizations_public_controller`), reload-safe under Zeitwerk; supersedes the initializer `class_eval` recipe (which evaporated on the first dev reload) and hand-rolled `to_prepare` blocks. `config.public_controller_helpers = ["ApplicationHelper"]` declares host helper modules for the public engine controller (it inherits `ActionController::Base`, so host layouts otherwise miss app helpers).
27
+ - **`config.engine_routes`** — devise_for-style `only:`/`except:` over engine route groups (`:switching`, `:organizations`, `:memberships`, `:invitations`, `:public_invitations`), replacing order-load-bearing shadow-route hacks in hosts that don't want the full engine UI.
28
+ - **`Organizations::OrganizationScoped`** — the URL-scoped addressing mode for overlay-style hosts (`/org/:slug/admin`): configurable `organization_param`/`organization_finder`, `current_scoped_organization`/`current_scoped_membership`, `require_organization_role :admin`, and a configurable not-found posture (default 404-never-403: unknown orgs, strangers, and under-role members are indistinguishable).
29
+ - **Verification delivery-failure handling** — if the code email fails to enqueue, the gem rolls the throttle bookkeeping back (immediate retry allowed; the failed send doesn't count against `max_sends`) and fires the new `on_verification_delivery_failed` callback so host error trackers see mail outages instead of users silently stranding.
30
+ - **DX bundle** — `Organization.create_with_owner!(owner:, **attrs)` (the ops/provisioning primitive: no session switching, no per-user cap, fires `organization_created`); `JoinRequest.purge_stale!(older_than:)` (GDPR sweep: decided/expired requests hold verification emails with no purpose; approved ones are kept as the join audit trail); `metadata_flag` macro on Organization/Membership (typed boolean accessors with defaults over the metadata bag); user-level errors get canonical top-level homes (`Organizations::CannotLeaveAsLastOwner` — nested paths remain as aliases); `Invitation#acceptance_url` is engine-mount-aware (previously 404'd for non-root mounts); known-user promotion is the default invitation auth redirect (sign-in for existing accounts, sign-up otherwise).
31
+ - **`rails g organizations:views`** — copies the Tailwind reference views (organizations/memberships/invitations) into the host, devise-views style; templates are drift-guarded byte-identical to the `test/dummy` reference app. The dummy now also carries a **verified-joining reference implementation** (four-state join screen + Access admin surface) to copy, and boots as a living 0.5.0 demo.
32
+ - **`TestHelpers#issue_verification_code(join_request, code: "424242")`** — force a known plaintext code onto a challenge in tests; host suites were reverse-engineering the digest recipe by hand.
33
+ - **PostgreSQL correctness for every rescued unique-violation** — all six rescue-RecordNotUnique INSERT sites now run under their own SAVEPOINT (transaction(requires_new: true)): PG aborts the whole transaction on a statement error, so the graceful fallbacks (idempotent membership reuse, degraded verified-email stamp, duplicate-allowlist skip) previously EXPLODED with PG::InFailedSqlTransaction instead of degrading — SQLite forgave the pattern, which is how it stayed green. Proven + guarded by a new ORGS_TEST_DATABASE_URL suite leg and a PostgreSQL CI cell. metadata_flag toggles are now with_lock-serialized; add_member! re-checks membership inside the transaction so a racing duplicate join is resolved idempotently without firing the gate (and without re-dispatching member_joined).
34
+
35
+ ### Changed
36
+
37
+ - `Organizations.configure` is now **atomic**: if the block (or the `validate!` it triggers) raises, the previous configuration is restored instead of leaving half-applied settings behind — a raising initializer no longer poisons later `configure` calls. Restoration is a shallow snapshot: use setters in configure blocks (`config.x = [...]`), never in-place mutation (`config.x << ...`).
38
+ - `organization_role_label`/`organization_invitation_status_label` and the default unauthorized messages resolve through I18n (English output unchanged; the unauthorized-role message now uses the translated label, e.g. "You need Admin access…").
39
+ - The two near-duplicate cannot-promote-to-owner messages were unified into one catalog key.
40
+ - Stock mailers self-register their view path and guard `Rails.application` access properly, so they render outside a full Rails app (found by the first tests to ever render them).
41
+
42
+ ### Notes
43
+
44
+ - BYO-UI as always: the gem ships models/APIs/mailers and state (`JoinFlow`/`JoinState`); routes, controllers and pixels for joining are the host's. Copy the reference implementation from `test/dummy` and **rate-limit your join/redemption/verification endpoints** (see README "Rate limiting your join endpoints").
45
+ - Join-code nuance under the gate: uses are counted at redemption, so a vetoed redemption still consumes a use — `max_uses` is an anti-abuse cap, not a seat count.
46
+
1
47
  ## [0.4.3] - 2026-03-19
2
48
 
3
49
  - Added `can_view_billing?` and `can_manage_billing?` view helpers for billing permission checks