authnz_eleven 0.1.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 (237) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +384 -0
  5. data/lib/authnz_eleven/identity.rb +210 -0
  6. data/lib/authnz_eleven/principal.rb +201 -0
  7. data/lib/authnz_eleven/principals.rb +111 -0
  8. data/lib/authnz_eleven/version.rb +5 -0
  9. data/lib/authnz_eleven/wizard.rb +570 -0
  10. data/lib/authnz_eleven.rb +15 -0
  11. data/lib/generators/authnz_eleven/USAGE +26 -0
  12. data/lib/generators/authnz_eleven/authnz_eleven_generator.rb +3040 -0
  13. data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.LICENSE +22 -0
  14. data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.txt +558 -0
  15. data/lib/generators/authnz_eleven/templates/config/initializers/authentication.rb.tt +275 -0
  16. data/lib/generators/authnz_eleven/templates/config/initializers/omniauth.rb.tt +9 -0
  17. data/lib/generators/authnz_eleven/templates/config/initializers/team_slug.rb.tt +24 -0
  18. data/lib/generators/authnz_eleven/templates/config/initializers/webauthn.rb.tt +14 -0
  19. data/lib/generators/authnz_eleven/templates/config/locales/authentication.en.yml.tt +11 -0
  20. data/lib/generators/authnz_eleven/templates/controllers/active_teams_controller.rb.tt +6 -0
  21. data/lib/generators/authnz_eleven/templates/controllers/admin/dashboard_controller.rb.tt +4 -0
  22. data/lib/generators/authnz_eleven/templates/controllers/admin/sessions_controller.rb.tt +17 -0
  23. data/lib/generators/authnz_eleven/templates/controllers/admin/users_controller.rb.tt +91 -0
  24. data/lib/generators/authnz_eleven/templates/controllers/admin_controller.rb.tt +6 -0
  25. data/lib/generators/authnz_eleven/templates/controllers/base_controller.rb.tt +11 -0
  26. data/lib/generators/authnz_eleven/templates/controllers/concerns/api_token_authentication.rb.tt +21 -0
  27. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/easy_dev_login.rb.tt +47 -0
  28. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/registering.rb.tt +25 -0
  29. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/sudoable.rb.tt +117 -0
  30. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/webauthn_ceremony.rb.tt +51 -0
  31. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication.rb.tt +421 -0
  32. data/lib/generators/authnz_eleven/templates/controllers/concerns/authorization.rb.tt +48 -0
  33. data/lib/generators/authnz_eleven/templates/controllers/concerns/captchable.rb.tt +11 -0
  34. data/lib/generators/authnz_eleven/templates/controllers/impersonations_controller.rb.tt +8 -0
  35. data/lib/generators/authnz_eleven/templates/controllers/invitations_controller.rb.tt +65 -0
  36. data/lib/generators/authnz_eleven/templates/controllers/settings/api_tokens_controller.rb.tt +54 -0
  37. data/lib/generators/authnz_eleven/templates/controllers/settings/authentications/events_controller.rb.tt +5 -0
  38. data/lib/generators/authnz_eleven/templates/controllers/settings/dashboard_controller.rb.tt +4 -0
  39. data/lib/generators/authnz_eleven/templates/controllers/settings/email_verifications_controller.rb.tt +15 -0
  40. data/lib/generators/authnz_eleven/templates/controllers/settings/emails_controller.rb.tt +93 -0
  41. data/lib/generators/authnz_eleven/templates/controllers/settings/invitations_controller.rb.tt +61 -0
  42. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/authenticators_controller.rb.tt +87 -0
  43. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/recovery_codes_controller.rb.tt +27 -0
  44. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/security_keys_controller.rb.tt +86 -0
  45. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/sms_controller.rb.tt +84 -0
  46. data/lib/generators/authnz_eleven/templates/controllers/settings/omniauth_identities_controller.rb.tt +27 -0
  47. data/lib/generators/authnz_eleven/templates/controllers/settings/passkeys_controller.rb.tt +70 -0
  48. data/lib/generators/authnz_eleven/templates/controllers/settings/passwords_controller.rb.tt +74 -0
  49. data/lib/generators/authnz_eleven/templates/controllers/settings/phone_verifications_controller.rb.tt +39 -0
  50. data/lib/generators/authnz_eleven/templates/controllers/settings/phones_controller.rb.tt +88 -0
  51. data/lib/generators/authnz_eleven/templates/controllers/settings/sessions_controller.rb.tt +29 -0
  52. data/lib/generators/authnz_eleven/templates/controllers/settings/users_controller.rb.tt +42 -0
  53. data/lib/generators/authnz_eleven/templates/controllers/settings_controller.rb.tt +5 -0
  54. data/lib/generators/authnz_eleven/templates/controllers/teams_controller.rb.tt +38 -0
  55. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passkeys_controller.rb.tt +36 -0
  56. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passwords_controller.rb.tt +28 -0
  57. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments_controller.rb.tt +14 -0
  58. data/lib/generators/authnz_eleven/templates/controllers/users/easy_dev_logins_controller.rb.tt +19 -0
  59. data/lib/generators/authnz_eleven/templates/controllers/users/email_verifications_controller.rb.tt +36 -0
  60. data/lib/generators/authnz_eleven/templates/controllers/users/magic_link_controller.rb.tt +53 -0
  61. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/recovery_codes_controller.rb.tt +30 -0
  62. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/security_keys_controller.rb.tt +34 -0
  63. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/sms_controller.rb.tt +45 -0
  64. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/totps_controller.rb.tt +42 -0
  65. data/lib/generators/authnz_eleven/templates/controllers/users/omniauth/registrations_controller.rb.tt +26 -0
  66. data/lib/generators/authnz_eleven/templates/controllers/users/omniauth_controller.rb.tt +139 -0
  67. data/lib/generators/authnz_eleven/templates/controllers/users/passkeys/sessions_controller.rb.tt +18 -0
  68. data/lib/generators/authnz_eleven/templates/controllers/users/password_resets_controller.rb.tt +64 -0
  69. data/lib/generators/authnz_eleven/templates/controllers/users/registrations/email_verifications_controller.rb.tt +53 -0
  70. data/lib/generators/authnz_eleven/templates/controllers/users/registrations/phone_verifications_controller.rb.tt +37 -0
  71. data/lib/generators/authnz_eleven/templates/controllers/users/registrations_controller.rb.tt +112 -0
  72. data/lib/generators/authnz_eleven/templates/controllers/users/sessions_controller.rb.tt +52 -0
  73. data/lib/generators/authnz_eleven/templates/controllers/users/sms_sessions_controller.rb.tt +53 -0
  74. data/lib/generators/authnz_eleven/templates/controllers/users/sudos_controller.rb.tt +42 -0
  75. data/lib/generators/authnz_eleven/templates/erb/admin/dashboard/index.html.erb.tt +9 -0
  76. data/lib/generators/authnz_eleven/templates/erb/admin/sessions/index.html.erb.tt +27 -0
  77. data/lib/generators/authnz_eleven/templates/erb/admin/users/index.html.erb.tt +32 -0
  78. data/lib/generators/authnz_eleven/templates/erb/admin/users/show.html.erb.tt +70 -0
  79. data/lib/generators/authnz_eleven/templates/erb/invitations/show.html.erb.tt +9 -0
  80. data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/create.html.erb.tt +7 -0
  81. data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/index.html.erb.tt +29 -0
  82. data/lib/generators/authnz_eleven/templates/erb/settings/authentications/events/index.html.erb.tt +18 -0
  83. data/lib/generators/authnz_eleven/templates/erb/settings/dashboard/index.html.erb.tt +75 -0
  84. data/lib/generators/authnz_eleven/templates/erb/settings/emails/edit.html.erb.tt +68 -0
  85. data/lib/generators/authnz_eleven/templates/erb/settings/invitations/new.html.erb.tt +31 -0
  86. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/authenticators/new.html.erb.tt +57 -0
  87. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/create.html.erb.tt +14 -0
  88. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/index.html.erb.tt +13 -0
  89. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/edit.html.erb.tt +14 -0
  90. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/index.html.erb.tt +20 -0
  91. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/new.html.erb.tt +17 -0
  92. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/sms/new.html.erb.tt +35 -0
  93. data/lib/generators/authnz_eleven/templates/erb/settings/omniauth_identities/index.html.erb.tt +28 -0
  94. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/edit.html.erb.tt +14 -0
  95. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/index.html.erb.tt +21 -0
  96. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/new.html.erb.tt +13 -0
  97. data/lib/generators/authnz_eleven/templates/erb/settings/passwords/edit.html.erb.tt +68 -0
  98. data/lib/generators/authnz_eleven/templates/erb/settings/phone_verifications/new.html.erb.tt +22 -0
  99. data/lib/generators/authnz_eleven/templates/erb/settings/phones/edit.html.erb.tt +58 -0
  100. data/lib/generators/authnz_eleven/templates/erb/settings/sessions/index.html.erb.tt +29 -0
  101. data/lib/generators/authnz_eleven/templates/erb/settings/users/show.html.erb.tt +28 -0
  102. data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_nav.html.erb.tt +24 -0
  103. data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_sudo_challenge.html.erb.tt +43 -0
  104. data/lib/generators/authnz_eleven/templates/erb/shared/_captcha.html.erb.tt +8 -0
  105. data/lib/generators/authnz_eleven/templates/erb/teams/index.html.erb.tt +34 -0
  106. data/lib/generators/authnz_eleven/templates/erb/teams/new.html.erb.tt +22 -0
  107. data/lib/generators/authnz_eleven/templates/erb/teams/show.html.erb.tt +5 -0
  108. data/lib/generators/authnz_eleven/templates/erb/user_mailer/email_change_verification.html.erb.tt +7 -0
  109. data/lib/generators/authnz_eleven/templates/erb/user_mailer/invitation.html.erb.tt +11 -0
  110. data/lib/generators/authnz_eleven/templates/erb/user_mailer/magic_link.html.erb.tt +5 -0
  111. data/lib/generators/authnz_eleven/templates/erb/user_mailer/password_reset.html.erb.tt +6 -0
  112. data/lib/generators/authnz_eleven/templates/erb/user_mailer/registration_email_verification.html.erb.tt +7 -0
  113. data/lib/generators/authnz_eleven/templates/erb/user_mailer/security_notification.html.erb.tt +15 -0
  114. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passkeys/new.html.erb.tt +24 -0
  115. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passwords/new.html.erb.tt +21 -0
  116. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/show.html.erb.tt +21 -0
  117. data/lib/generators/authnz_eleven/templates/erb/users/email_verifications/show.html.erb.tt +9 -0
  118. data/lib/generators/authnz_eleven/templates/erb/users/magic_link/edit.html.erb.tt +9 -0
  119. data/lib/generators/authnz_eleven/templates/erb/users/magic_link/new.html.erb.tt +22 -0
  120. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/recovery_codes/new.html.erb.tt +20 -0
  121. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/security_keys/new.html.erb.tt +17 -0
  122. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/sms/new.html.erb.tt +25 -0
  123. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/totps/new.html.erb.tt +21 -0
  124. data/lib/generators/authnz_eleven/templates/erb/users/omniauth/registrations/new.html.erb.tt +28 -0
  125. data/lib/generators/authnz_eleven/templates/erb/users/password_resets/edit.html.erb.tt +26 -0
  126. data/lib/generators/authnz_eleven/templates/erb/users/password_resets/new.html.erb.tt +20 -0
  127. data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/new.html.erb.tt +11 -0
  128. data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/show.html.erb.tt +34 -0
  129. data/lib/generators/authnz_eleven/templates/erb/users/registrations/new.html.erb.tt +87 -0
  130. data/lib/generators/authnz_eleven/templates/erb/users/registrations/phone_verifications/new.html.erb.tt +46 -0
  131. data/lib/generators/authnz_eleven/templates/erb/users/sessions/new.html.erb.tt +88 -0
  132. data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/edit.html.erb.tt +30 -0
  133. data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/new.html.erb.tt +22 -0
  134. data/lib/generators/authnz_eleven/templates/erb/users/sudos/new.html.erb.tt +9 -0
  135. data/lib/generators/authnz_eleven/templates/javascript/controllers/webauthn_controller.js.tt +142 -0
  136. data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_deferred_job.rb.tt +9 -0
  137. data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_mail_delivery_job.rb.tt +3 -0
  138. data/lib/generators/authnz_eleven/templates/jobs/sms_delivery_job.rb.tt +11 -0
  139. data/lib/generators/authnz_eleven/templates/lib/captcha/null.rb.tt +9 -0
  140. data/lib/generators/authnz_eleven/templates/lib/captcha/turnstile.rb.tt +39 -0
  141. data/lib/generators/authnz_eleven/templates/lib/captcha.rb.tt +15 -0
  142. data/lib/generators/authnz_eleven/templates/lib/sms/log.rb.tt +8 -0
  143. data/lib/generators/authnz_eleven/templates/lib/sms/twilio.rb.tt +70 -0
  144. data/lib/generators/authnz_eleven/templates/lib/sms.rb.tt +15 -0
  145. data/lib/generators/authnz_eleven/templates/mailers/user_mailer.rb.tt +98 -0
  146. data/lib/generators/authnz_eleven/templates/migrations/add_guest_to_users_migration.rb.tt +6 -0
  147. data/lib/generators/authnz_eleven/templates/migrations/create_api_tokens_migration.rb.tt +14 -0
  148. data/lib/generators/authnz_eleven/templates/migrations/create_events_migration.rb.tt +26 -0
  149. data/lib/generators/authnz_eleven/templates/migrations/create_invitations_migration.rb.tt +20 -0
  150. data/lib/generators/authnz_eleven/templates/migrations/create_memberships_migration.rb.tt +12 -0
  151. data/lib/generators/authnz_eleven/templates/migrations/create_omniauth_identities_migration.rb.tt +16 -0
  152. data/lib/generators/authnz_eleven/templates/migrations/create_password_histories_migration.rb.tt +11 -0
  153. data/lib/generators/authnz_eleven/templates/migrations/create_pending_registrations_migration.rb.tt +31 -0
  154. data/lib/generators/authnz_eleven/templates/migrations/create_recovery_codes_migration.rb.tt +11 -0
  155. data/lib/generators/authnz_eleven/templates/migrations/create_sessions_migration.rb.tt +19 -0
  156. data/lib/generators/authnz_eleven/templates/migrations/create_teams_migration.rb.tt +9 -0
  157. data/lib/generators/authnz_eleven/templates/migrations/create_users_migration.rb.tt +70 -0
  158. data/lib/generators/authnz_eleven/templates/migrations/create_webauthn_credentials_migration.rb.tt +16 -0
  159. data/lib/generators/authnz_eleven/templates/models/api_token.rb.tt +24 -0
  160. data/lib/generators/authnz_eleven/templates/models/concerns/adminable.rb.tt +15 -0
  161. data/lib/generators/authnz_eleven/templates/models/concerns/authenticatable.rb.tt +151 -0
  162. data/lib/generators/authnz_eleven/templates/models/concerns/bannable.rb.tt +24 -0
  163. data/lib/generators/authnz_eleven/templates/models/concerns/deadboltable.rb.tt +39 -0
  164. data/lib/generators/authnz_eleven/templates/models/concerns/email_verifiable.rb.tt +52 -0
  165. data/lib/generators/authnz_eleven/templates/models/concerns/eventable.rb.tt +22 -0
  166. data/lib/generators/authnz_eleven/templates/models/concerns/guestable.rb.tt +30 -0
  167. data/lib/generators/authnz_eleven/templates/models/concerns/invitable.rb.tt +12 -0
  168. data/lib/generators/authnz_eleven/templates/models/concerns/magic_linkable.rb.tt +34 -0
  169. data/lib/generators/authnz_eleven/templates/models/concerns/password_historical.rb.tt +28 -0
  170. data/lib/generators/authnz_eleven/templates/models/concerns/password_policy.rb.tt +38 -0
  171. data/lib/generators/authnz_eleven/templates/models/concerns/password_rotatable.rb.tt +15 -0
  172. data/lib/generators/authnz_eleven/templates/models/concerns/phone_verifiable.rb.tt +50 -0
  173. data/lib/generators/authnz_eleven/templates/models/concerns/principals.rb.tt +67 -0
  174. data/lib/generators/authnz_eleven/templates/models/concerns/recoverable.rb.tt +19 -0
  175. data/lib/generators/authnz_eleven/templates/models/concerns/second_factor.rb.tt +52 -0
  176. data/lib/generators/authnz_eleven/templates/models/concerns/sms_challengeable.rb.tt +38 -0
  177. data/lib/generators/authnz_eleven/templates/models/concerns/team_member.rb.tt +20 -0
  178. data/lib/generators/authnz_eleven/templates/models/current.rb.tt +28 -0
  179. data/lib/generators/authnz_eleven/templates/models/event.rb.tt +32 -0
  180. data/lib/generators/authnz_eleven/templates/models/invitation.rb.tt +122 -0
  181. data/lib/generators/authnz_eleven/templates/models/membership.rb.tt +16 -0
  182. data/lib/generators/authnz_eleven/templates/models/omniauth_identity.rb.tt +12 -0
  183. data/lib/generators/authnz_eleven/templates/models/password_history.rb.tt +3 -0
  184. data/lib/generators/authnz_eleven/templates/models/pending_registration.rb.tt +216 -0
  185. data/lib/generators/authnz_eleven/templates/models/recovery_code.rb.tt +41 -0
  186. data/lib/generators/authnz_eleven/templates/models/session.rb.tt +46 -0
  187. data/lib/generators/authnz_eleven/templates/models/team.rb.tt +12 -0
  188. data/lib/generators/authnz_eleven/templates/models/user.rb.tt +59 -0
  189. data/lib/generators/authnz_eleven/templates/models/webauthn_credential.rb.tt +12 -0
  190. data/lib/generators/authnz_eleven/templates/tasks/authnz_eleven.rake.tt +41 -0
  191. data/lib/generators/authnz_eleven/templates/test_unit/controllers/account_existence_parity_test.rb.tt +158 -0
  192. data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/access_test.rb.tt +139 -0
  193. data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/multi_factor_authentication_test.rb.tt +65 -0
  194. data/lib/generators/authnz_eleven/templates/test_unit/controllers/api_token_authentication_test.rb.tt +45 -0
  195. data/lib/generators/authnz_eleven/templates/test_unit/controllers/credential_enrollment_test.rb.tt +84 -0
  196. data/lib/generators/authnz_eleven/templates/test_unit/controllers/guest_access_test.rb.tt +113 -0
  197. data/lib/generators/authnz_eleven/templates/test_unit/controllers/invitations_controller_test.rb.tt +230 -0
  198. data/lib/generators/authnz_eleven/templates/test_unit/controllers/passkey_ceremony_test.rb.tt +126 -0
  199. data/lib/generators/authnz_eleven/templates/test_unit/controllers/password_rotation_test.rb.tt +67 -0
  200. data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_key_ceremony_test.rb.tt +218 -0
  201. data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_notifications_test.rb.tt +39 -0
  202. data/lib/generators/authnz_eleven/templates/test_unit/controllers/session_lifecycle_test.rb.tt +102 -0
  203. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/api_tokens_controller_test.rb.tt +61 -0
  204. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/authentications/events_controller_test.rb.tt +17 -0
  205. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/email_verifications_controller_test.rb.tt +39 -0
  206. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/invitations_controller_test.rb.tt +157 -0
  207. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/management_test.rb.tt +256 -0
  208. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/multi_factor_authentication/management_test.rb.tt +222 -0
  209. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/omniauth_identities_controller_test.rb.tt +57 -0
  210. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/phone_verifications_controller_test.rb.tt +146 -0
  211. data/lib/generators/authnz_eleven/templates/test_unit/controllers/teams_controller_test.rb.tt +83 -0
  212. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/email_verifications_controller_test.rb.tt +74 -0
  213. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/magic_link_controller_test.rb.tt +90 -0
  214. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/multi_factor_authentication/challenge_test.rb.tt +143 -0
  215. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth/registrations_controller_test.rb.tt +138 -0
  216. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth_controller_test.rb.tt +97 -0
  217. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/password_resets_controller_test.rb.tt +58 -0
  218. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/email_verifications_controller_test.rb.tt +102 -0
  219. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/phone_verifications_controller_test.rb.tt +69 -0
  220. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations_controller_test.rb.tt +180 -0
  221. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sessions_controller_test.rb.tt +124 -0
  222. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sms_sessions_controller_test.rb.tt +111 -0
  223. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sudos_controller_test.rb.tt +110 -0
  224. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/memberships.yml.tt +13 -0
  225. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/teams.yml.tt +2 -0
  226. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/users.yml.tt +68 -0
  227. data/lib/generators/authnz_eleven/templates/test_unit/mailers/user_mailer_test.rb.tt +88 -0
  228. data/lib/generators/authnz_eleven/templates/test_unit/models/api_token_test.rb.tt +38 -0
  229. data/lib/generators/authnz_eleven/templates/test_unit/models/password_history_test.rb.tt +51 -0
  230. data/lib/generators/authnz_eleven/templates/test_unit/models/session_test.rb.tt +16 -0
  231. data/lib/generators/authnz_eleven/templates/test_unit/models/sms_challengeable_test.rb.tt +108 -0
  232. data/lib/generators/authnz_eleven/templates/test_unit/models/user/guestable_test.rb.tt +78 -0
  233. data/lib/generators/authnz_eleven/templates/test_unit/models/user/recovery_codes_test.rb.tt +32 -0
  234. data/lib/generators/authnz_eleven/templates/test_unit/models/user/sign_in_methods_test.rb.tt +56 -0
  235. data/lib/generators/authnz_eleven/templates/test_unit/models/user_test.rb.tt +145 -0
  236. data/lib/generators/authnz_eleven/wizard_generator.rb +52 -0
  237. metadata +309 -0
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuthnzEleven
4
+ # One piece of identifying information on the generated account. A Principal
5
+ # derives every name and expression the templates need for one identifying
6
+ # attribute (email, username, phone), so the per-type differences live here
7
+ # instead of being scattered across the templates.
8
+ #
9
+ # Each selected principal is an explicit part of the generated account shape.
10
+ class Principal
11
+ # type: :email | :username | :phone
12
+ attr_reader :type
13
+
14
+ def initialize(type:, required: true, login: false, permanent: false)
15
+ @type = type.to_sym
16
+ @required = required
17
+ @login = login
18
+ @permanent = permanent
19
+ end
20
+
21
+ def required? = @required
22
+ def optional? = !@required
23
+ # a key the sign-in form accepts
24
+ def login? = @login
25
+
26
+ # Fixed once the account holds it: no self-serve change surface is generated
27
+ # for it. The column stays writable, so an admin or a console still can.
28
+ def permanent? = @permanent
29
+ def changeable? = !@permanent
30
+
31
+ # A channel can receive a message (verification, reset, magic link, OTP).
32
+ def channel? = type != :username
33
+
34
+ # "email" / "username" / "phone"
35
+ def column = type.to_s
36
+ # Form label text. Every principal type is a single word, so a plain
37
+ # capitalize matches humanize without pulling ActiveSupport into the gem's
38
+ # otherwise dependency-free load path (the host Rails app provides it at
39
+ # generation time, but the value object is unit-tested standalone).
40
+ def label = type.to_s.capitalize
41
+
42
+ # Form helper + autocomplete token for generated views.
43
+ def field_helper
44
+ { email: "email_field", phone: "telephone_field", username: "text_field" }.fetch(type)
45
+ end
46
+
47
+ def autocomplete
48
+ { email: "email", phone: "tel", username: "username" }.fetch(type)
49
+ end
50
+
51
+ # Model-layer expressions, emitted verbatim into user.rb.tt. Keeping them
52
+ # here (not scattered in the template) keeps the template readable and the
53
+ # per-type differences in one place.
54
+ def validation
55
+ case type
56
+ # disposable_domain, not disposable: the plain option also resolves the
57
+ # domain's MX servers, putting a DNS lookup inside a validation. This one
58
+ # is a lookup against the gem's bundled list, in-process and offline.
59
+ when :email then '"valid_email_2/email": { disposable_domain: true }'
60
+ when :phone then %() # validated by an inline Phonelib validate block
61
+ # Letters (both cases), digits, underscore; 3–20 chars; at least one letter;
62
+ # no "@". The ≥1-letter rule is what makes the sign-in dispatch a total,
63
+ # unambiguous partition: "@" → email, else a letter → username, else
64
+ # → phone — a username always has a letter, a phone never does. Relaxing it
65
+ # to allow all-digit names reopens the username/phone ambiguity. Case is
66
+ # preserved for display; uniqueness folds case via the functional index below.
67
+ when :username then %q(format: { with: /\A(?=.*[a-zA-Z])[a-zA-Z0-9_]{3,20}\z/ })
68
+ end
69
+ end
70
+
71
+ # Everything after `validates :<column>, ` on the model's validation line.
72
+ # A required principal gets `presence: true, <validation>`; an optional one
73
+ # drops presence and allows nil, so a blank submission (normalized to nil
74
+ # below) skips validation and rides the partial unique index.
75
+ def validates_arguments
76
+ rule = validation.to_s.strip
77
+ [
78
+ ("presence: true" if required?),
79
+ (rule unless rule.empty?),
80
+ ("allow_nil: true" if optional?)
81
+ ].compact.join(", ")
82
+ end
83
+
84
+ # Phone can't ride the generic `validates :col, <format>` line: its validity
85
+ # is a Phonelib.valid? check (a custom validator, not a `format:` matcher), and
86
+ # when guests exist it must skip them (guests carry a nil phone). The template
87
+ # emits its own lines instead.
88
+ def dedicated_validation? = type == :phone
89
+
90
+ # Sign-in dispatch: when one form field accepts several login keys, the
91
+ # lookup is routed to a column by a *structural* test of the submitted string —
92
+ # an email has "@", a username has a letter (guaranteed by the format), a phone
93
+ # has neither. #login_dispatch_test is that test; the lowest-priority present
94
+ # key needs none (it's the fallback). Email must be tested before username
95
+ # because an email address also contains letters.
96
+ def login_dispatch_test
97
+ case type
98
+ when :email then %(identifier.include?("@"))
99
+ when :username then %(identifier.match?(/[a-zA-Z]/))
100
+ when :phone then nil
101
+ end
102
+ end
103
+
104
+ # The no-password lookup for this key, as a Ruby expression over a stripped
105
+ # `identifier` local — used by the model's find_by_login. Email and phone pass
106
+ # the raw identifier and lean on `normalizes` being applied inside finders
107
+ # (downcase / E.164); username needs an explicit LOWER() match because its
108
+ # normalization is strip-only (case is folded for uniqueness, not on write).
109
+ def find_by_login_lookup
110
+ case type
111
+ when :email then %(find_by(email: identifier))
112
+ when :username then 'find_by("LOWER(username) = ?", identifier.downcase)'
113
+ when :phone then %(find_by(phone: identifier))
114
+ end
115
+ end
116
+
117
+ # The same lookup, but verifying a password — used by the model's
118
+ # authenticate_with_password. Channel keys go through authenticate_by, which
119
+ # equalizes timing whether or not a row was found. It only hashes a decoy on a
120
+ # miss, though, so a found row without a password would skip the hash entirely:
121
+ # with_password makes that row a miss too. Username can't, because its lookup
122
+ # needs LOWER() to fold case, so that arm finds first and authenticates second.
123
+ # The asymmetry is safe: a username is a public identifier, so it is honest
124
+ # about existence anyway.
125
+ def authenticate_lookup
126
+ case type
127
+ when :email then %(with_password.authenticate_by(email: identifier, password:))
128
+ when :username
129
+ 'find_by("LOWER(username) = ?", identifier.downcase)&.then { |user| user if user.authenticate(password) }'
130
+ when :phone then %(with_password.authenticate_by(phone: identifier, password:))
131
+ end
132
+ end
133
+
134
+ # +config_reference+ is the generated app's config constant ("UserAuth"), which
135
+ # the phone branch reads a default country from. It's threaded in rather than
136
+ # derived here because it belongs to the identity, not to the principal — see
137
+ # Identity#config_reference, the same value the initializer template writes.
138
+ def normalization(config_reference:)
139
+ base =
140
+ case type
141
+ when :email then "it.strip.downcase"
142
+ # Strip only — the chosen case is stored and displayed verbatim.
143
+ when :username then "it.strip"
144
+ # A number typed without a leading "+" is read as though dialled inside
145
+ # config.phone.default_country. Passing that country to parse as an
146
+ # ARGUMENT is what suppresses Phonelib's fallback of reading a bare
147
+ # number's leading digits AS a country code — which would turn a mistyped
148
+ # local number into a valid foreign one ("6494461709" comes back a good NZ
149
+ # number). Setting Phonelib.default_country globally does NOT do this.
150
+ #
151
+ # #to_s returns E.164 when the number parses and the original string
152
+ # otherwise, so an uninterpretable number reaches the validator looking the
153
+ # way the user typed it, and is rejected there.
154
+ when :phone then "Phonelib.parse(it, #{config_reference}.phone.default_country).to_s"
155
+ end
156
+ # An optional principal must collapse a blank submission to nil so the
157
+ # partial unique index (WHERE <col> IS NOT NULL) and allow_nil both apply.
158
+ # Required principals normalize without it.
159
+ base += ".presence" if optional?
160
+ "-> { #{base} }"
161
+ end
162
+
163
+ # Whether flows may reveal that a value is taken. Usernames are public
164
+ # identifiers: sign-up MUST honestly reject a taken username. Channel
165
+ # principals stay coy.
166
+ def publicly_unique? = type == :username
167
+
168
+ # The `t.string` line for create_users_migration. Principals whose uniqueness
169
+ # can't ride an inline `index:` option (username — see
170
+ # #functional_unique_index?) emit a bare column here and their index
171
+ # separately.
172
+ #
173
+ # +nullable+ defaults to requiredness but is overridden for a *required* phone
174
+ # in a --guestable build, where guests carry a nil number so the constraint has
175
+ # to live in the model (unless: :guest?) rather than in NOT NULL.
176
+ def migration_field(nullable: optional?)
177
+ case type
178
+ when :username then "t.string :username, null: false"
179
+ when :email
180
+ # Optional email drops NOT NULL and rides a partial unique index, so
181
+ # multiple rows may hold NULL while set values stay unique.
182
+ if required?
183
+ "t.string :email, null: false, index: { unique: true }"
184
+ else
185
+ %(t.string :email, index: { unique: true, where: "email IS NOT NULL" })
186
+ end
187
+ else
188
+ null_option = " null: false," unless nullable
189
+ where = %(, where: "#{column} IS NOT NULL") if nullable
190
+ "t.string :#{column},#{null_option} index: { unique: true#{where} }"
191
+ end
192
+ end
193
+
194
+ # Username uniqueness is case-insensitive but its stored value keeps its
195
+ # display case, so it can't use migration_field's inline unique index. It
196
+ # rides a functional unique index on LOWER(column) instead, emitted
197
+ # separately. SQLite and PostgreSQL take it as written; MySQL wants the
198
+ # expression in a second pair of parens.
199
+ def functional_unique_index? = type == :username
200
+ end
201
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "principal"
4
+
5
+ module AuthnzEleven
6
+ # The ordered set of principals for one generator run. Where Identity names
7
+ # things (class, table, routes), Principals shapes the account's identifying
8
+ # columns and the flows that ride on them.
9
+ #
10
+ # Email and phone are opt-in principals; a bare flag makes the selected
11
+ # principal required. Every valid build has at least one principal.
12
+ class Principals
13
+ include Enumerable
14
+
15
+ REQUIREDNESS = %w[required optional].freeze
16
+
17
+ # Build the set from the generator's parsed options. --username prepends a
18
+ # required login principal, and --email / --phone add channel principals.
19
+ def self.from_options(options)
20
+ # Ordered username, email, phone so #display prefers them in that order and
21
+ # the sign-in dispatch tests the most specific keys first. Each channel
22
+ # helper returns nil when its flag omits it.
23
+ list = [
24
+ (Principal.new(type: :username, required: true, login: true) if options[:username]),
25
+ email_principal(options[:email]),
26
+ phone_principal(options[:phone])
27
+ ].compact
28
+ new(list)
29
+ end
30
+
31
+ # A --email / --phone value is a requiredness word, optionally followed by the
32
+ # word "permanent" — the only modifier a principal takes:
33
+ # "required,permanent" -> [ "required", [ "permanent" ] ]. The generator
34
+ # validates the words; this only splits them.
35
+ def self.split_mode(value)
36
+ requiredness, *modifiers = value.to_s.split(",").map { it.strip.downcase }.reject(&:empty?)
37
+ [requiredness.to_s, modifiers]
38
+ end
39
+
40
+ def self.permanent?(modifiers) = modifiers == %w[permanent]
41
+
42
+ # The email principal for the given --email mode, or nil when email is off.
43
+ def self.email_principal(mode)
44
+ requiredness, modifiers = split_mode(mode)
45
+ return unless %w[required optional].include?(requiredness)
46
+
47
+ Principal.new(type: :email, required: requiredness == "required", login: true,
48
+ permanent: permanent?(modifiers))
49
+ end
50
+
51
+ # The phone principal for the given --phone mode, or nil when phone is off.
52
+ # Phone is a login key too (the sign-in form dispatches to it).
53
+ def self.phone_principal(mode)
54
+ requiredness, modifiers = split_mode(mode)
55
+ return unless %w[required optional].include?(requiredness)
56
+
57
+ Principal.new(type: :phone, required: requiredness == "required", login: true,
58
+ permanent: permanent?(modifiers))
59
+ end
60
+
61
+ def initialize(list)
62
+ @list = list
63
+ end
64
+
65
+ def each(&) = @list.each(&)
66
+
67
+ def email = find { |p| p.type == :email }
68
+ def username = find { |p| p.type == :username }
69
+ def phone = find { |p| p.type == :phone }
70
+
71
+ def login_principals = select(&:login?)
72
+
73
+ # Login keys in *dispatch* priority for the sign-in form's shape test:
74
+ # email first (its "@" is the most specific test), then username (a letter),
75
+ # then phone (the fallback). Distinct from #login_principals' list order, which
76
+ # follows display preference (username first).
77
+ def login_dispatch_principals
78
+ login_principals.sort_by { |p| %i[email username phone].index(p.type) }
79
+ end
80
+
81
+ def channels = select(&:channel?)
82
+ def required = select(&:required?)
83
+
84
+ # The principal shown in UI/admin/WebAuthn: the first login principal
85
+ # (username when present, else email, else phone).
86
+ def display
87
+ login_principals.min_by { |p| %i[username email phone].index(p.type) }
88
+ end
89
+
90
+ # #display names one column for the whole build, which is what a table header
91
+ # or a WebAuthn handle wants. Showing a person their own identifier is a
92
+ # record-level question instead: --contactable's Reddit shape
93
+ # (--email=optional --phone=optional) promises each account holds *a* channel
94
+ # without saying which, so the build-time pick is nil on half the rows.
95
+ #
96
+ # Only an OPTIONAL principal can be nil at the front of that precedence, so the
97
+ # fallback stops at the first required one: a required username or email is
98
+ # NOT NULL (guests get a synthesized one), which makes everything after it
99
+ # unreachable. A required phone is the exception — nullable under --guestable
100
+ # and while verification is outstanding — but phone sorts last and so never
101
+ # leads. Most builds therefore emit a bare column read.
102
+ def display_expression(receiver)
103
+ ordered = login_principals.sort_by { |p| %i[username email phone].index(p.type) }
104
+ reachable = ordered.take_while(&:optional?) + ordered.drop_while(&:optional?).take(1)
105
+ reachable.map { |p| "#{receiver}.#{p.column}" }.join(" || ")
106
+ end
107
+
108
+ # Whether the sign-in form must dispatch between several login keys.
109
+ def multi_login? = login_principals.size > 1
110
+ end
111
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AuthnzEleven
4
+ VERSION = "0.1.0"
5
+ end