rsb-auth 0.9.1

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 (94) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +15 -0
  3. data/README.md +76 -0
  4. data/Rakefile +25 -0
  5. data/app/controllers/concerns/rsb/auth/ensure_identity_complete.rb +72 -0
  6. data/app/controllers/concerns/rsb/auth/rate_limitable.rb +20 -0
  7. data/app/controllers/rsb/auth/account/login_methods_controller.rb +85 -0
  8. data/app/controllers/rsb/auth/account/sessions_controller.rb +31 -0
  9. data/app/controllers/rsb/auth/account_controller.rb +99 -0
  10. data/app/controllers/rsb/auth/admin/identities_controller.rb +402 -0
  11. data/app/controllers/rsb/auth/admin/sessions_management_controller.rb +27 -0
  12. data/app/controllers/rsb/auth/application_controller.rb +50 -0
  13. data/app/controllers/rsb/auth/invitations_controller.rb +31 -0
  14. data/app/controllers/rsb/auth/password_resets_controller.rb +46 -0
  15. data/app/controllers/rsb/auth/registrations_controller.rb +104 -0
  16. data/app/controllers/rsb/auth/sessions_controller.rb +109 -0
  17. data/app/controllers/rsb/auth/verifications_controller.rb +29 -0
  18. data/app/helpers/rsb/auth/user_agent_helper.rb +22 -0
  19. data/app/mailers/rsb/auth/application_mailer.rb +10 -0
  20. data/app/mailers/rsb/auth/auth_mailer.rb +33 -0
  21. data/app/models/rsb/auth/application_record.rb +10 -0
  22. data/app/models/rsb/auth/credential/email_password.rb +9 -0
  23. data/app/models/rsb/auth/credential/phone_password.rb +16 -0
  24. data/app/models/rsb/auth/credential/username_password.rb +9 -0
  25. data/app/models/rsb/auth/credential.rb +122 -0
  26. data/app/models/rsb/auth/identity.rb +62 -0
  27. data/app/models/rsb/auth/invitation.rb +55 -0
  28. data/app/models/rsb/auth/password_reset_token.rb +36 -0
  29. data/app/models/rsb/auth/session.rb +44 -0
  30. data/app/services/rsb/auth/account_service.rb +140 -0
  31. data/app/services/rsb/auth/authentication_service.rb +86 -0
  32. data/app/services/rsb/auth/invitation_service.rb +53 -0
  33. data/app/services/rsb/auth/password_reset_service.rb +48 -0
  34. data/app/services/rsb/auth/registration_service.rb +108 -0
  35. data/app/services/rsb/auth/session_service.rb +47 -0
  36. data/app/services/rsb/auth/verification_service.rb +30 -0
  37. data/app/views/layouts/rsb/auth/application.html.erb +76 -0
  38. data/app/views/rsb/auth/account/_identity_fields.html.erb +3 -0
  39. data/app/views/rsb/auth/account/confirm_destroy.html.erb +45 -0
  40. data/app/views/rsb/auth/account/login_methods/show.html.erb +92 -0
  41. data/app/views/rsb/auth/account/show.html.erb +110 -0
  42. data/app/views/rsb/auth/admin/credentials/_email_password.html.erb +34 -0
  43. data/app/views/rsb/auth/admin/credentials/_phone_password.html.erb +34 -0
  44. data/app/views/rsb/auth/admin/credentials/_username_password.html.erb +34 -0
  45. data/app/views/rsb/auth/admin/identities/index.html.erb +76 -0
  46. data/app/views/rsb/auth/admin/identities/new.html.erb +46 -0
  47. data/app/views/rsb/auth/admin/identities/new_credential.html.erb +45 -0
  48. data/app/views/rsb/auth/admin/identities/show.html.erb +180 -0
  49. data/app/views/rsb/auth/admin/sessions_management/index.html.erb +69 -0
  50. data/app/views/rsb/auth/auth_mailer/invitation.html.erb +4 -0
  51. data/app/views/rsb/auth/auth_mailer/password_reset.html.erb +4 -0
  52. data/app/views/rsb/auth/auth_mailer/verification.html.erb +4 -0
  53. data/app/views/rsb/auth/credentials/_email_password_login.html.erb +36 -0
  54. data/app/views/rsb/auth/credentials/_email_password_signup.html.erb +45 -0
  55. data/app/views/rsb/auth/credentials/_icon.html.erb +21 -0
  56. data/app/views/rsb/auth/credentials/_phone_password_login.html.erb +33 -0
  57. data/app/views/rsb/auth/credentials/_phone_password_signup.html.erb +45 -0
  58. data/app/views/rsb/auth/credentials/_selector.html.erb +43 -0
  59. data/app/views/rsb/auth/credentials/_username_password_login.html.erb +33 -0
  60. data/app/views/rsb/auth/credentials/_username_password_signup.html.erb +54 -0
  61. data/app/views/rsb/auth/invitations/show.html.erb +40 -0
  62. data/app/views/rsb/auth/password_resets/edit.html.erb +41 -0
  63. data/app/views/rsb/auth/password_resets/new.html.erb +27 -0
  64. data/app/views/rsb/auth/registrations/new.html.erb +55 -0
  65. data/app/views/rsb/auth/sessions/new.html.erb +47 -0
  66. data/config/locales/account.en.yml +65 -0
  67. data/config/locales/admin.en.yml +26 -0
  68. data/config/locales/credentials.en.yml +11 -0
  69. data/config/locales/seo.en.yml +28 -0
  70. data/config/routes.rb +34 -0
  71. data/db/migrate/20260208100001_create_rsb_auth_identities.rb +12 -0
  72. data/db/migrate/20260208100002_create_rsb_auth_credentials.rb +20 -0
  73. data/db/migrate/20260208100003_create_rsb_auth_sessions.rb +18 -0
  74. data/db/migrate/20260208100004_create_rsb_auth_password_reset_tokens.rb +15 -0
  75. data/db/migrate/20260208100005_add_verification_to_rsb_auth_credentials.rb +9 -0
  76. data/db/migrate/20260208100006_create_rsb_auth_invitations.rb +19 -0
  77. data/db/migrate/20260211100001_add_revoked_at_to_rsb_auth_credentials.rb +16 -0
  78. data/db/migrate/20260212100001_add_deleted_at_to_rsb_auth_identities.rb +10 -0
  79. data/db/migrate/20260214172956_add_recovery_email_to_rsb_auth_credentials.rb +8 -0
  80. data/lib/generators/rsb/auth/install/install_generator.rb +31 -0
  81. data/lib/generators/rsb/auth/views/views_generator.rb +197 -0
  82. data/lib/rsb/auth/configuration.rb +59 -0
  83. data/lib/rsb/auth/credential_conflict_error.rb +21 -0
  84. data/lib/rsb/auth/credential_definition.rb +39 -0
  85. data/lib/rsb/auth/credential_deprecation_bridge.rb +81 -0
  86. data/lib/rsb/auth/credential_registry.rb +96 -0
  87. data/lib/rsb/auth/credential_settings_registrar.rb +118 -0
  88. data/lib/rsb/auth/engine.rb +187 -0
  89. data/lib/rsb/auth/lifecycle_handler.rb +71 -0
  90. data/lib/rsb/auth/settings_schema.rb +74 -0
  91. data/lib/rsb/auth/test_helper.rb +139 -0
  92. data/lib/rsb/auth/version.rb +9 -0
  93. data/lib/rsb/auth.rb +49 -0
  94. metadata +192 -0
@@ -0,0 +1,92 @@
1
+ <div class="min-h-screen flex items-center justify-center px-4 py-12">
2
+ <div class="max-w-md w-full space-y-8">
3
+ <div>
4
+ <h2 class="text-3xl font-extrabold text-gray-900"><%= @credential.type.demodulize.titleize %></h2>
5
+ <p class="mt-2 text-sm text-gray-600"><%= @credential.identifier %></p>
6
+ <% if @credential.recovery_email.present? %>
7
+ <p class="mt-1 text-sm text-gray-600">
8
+ <strong>Recovery email:</strong>
9
+ <%= @credential.recovery_email %>
10
+ </p>
11
+ <% end %>
12
+ </div>
13
+
14
+ <%# Verification status %>
15
+ <div class="p-4 bg-white border border-gray-200 rounded-md">
16
+ <h3 class="text-sm font-medium text-gray-900"><%= t("rsb.auth.account.verification_status") %></h3>
17
+ <div class="mt-2">
18
+ <% if @credential.verified? %>
19
+ <div class="flex items-center space-x-2">
20
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><%= t("rsb.auth.account.verified") %></span>
21
+ <span class="text-xs text-gray-500">
22
+ <%= l(@credential.verified_at, format: :long) %>
23
+ </span>
24
+ </div>
25
+ <% else %>
26
+ <div class="flex items-center justify-between">
27
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><%= t("rsb.auth.account.unverified") %></span>
28
+ <%= button_to t("rsb.auth.account.resend_verification"), resend_verification_account_login_method_path(@credential), method: :post, class: "text-sm text-indigo-600 hover:text-indigo-500" %>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+ </div>
33
+
34
+ <%# Change password form %>
35
+ <div class="border-t border-gray-200 pt-8">
36
+ <h3 class="text-lg font-medium text-gray-900"><%= t("rsb.auth.account.change_password_title") %></h3>
37
+
38
+ <% if @password_errors.present? %>
39
+ <div class="mt-4 rounded-md bg-red-50 p-4">
40
+ <div class="ml-3">
41
+ <div class="text-sm text-red-700">
42
+ <ul class="list-disc pl-5 space-y-1">
43
+ <% @password_errors.each do |error| %>
44
+ <li><%= error %></li>
45
+ <% end %>
46
+ </ul>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <% end %>
51
+
52
+ <%= form_with url: password_account_login_method_path(@credential), method: :patch, class: "mt-4 space-y-4" do |f| %>
53
+ <div>
54
+ <label for="current_password" class="block text-sm font-medium text-gray-700"><%= t("rsb.auth.account.current_password") %></label>
55
+ <input id="current_password" name="current_password" type="password" autocomplete="current-password" required
56
+ class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
57
+ </div>
58
+ <div>
59
+ <label for="new_password" class="block text-sm font-medium text-gray-700"><%= t("rsb.auth.account.new_password") %></label>
60
+ <input id="new_password" name="new_password" type="password" autocomplete="new-password" required
61
+ class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
62
+ </div>
63
+ <div>
64
+ <label for="new_password_confirmation" class="block text-sm font-medium text-gray-700"><%= t("rsb.auth.account.confirm_new_password") %></label>
65
+ <input id="new_password_confirmation" name="new_password_confirmation" type="password" autocomplete="new-password" required
66
+ class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
67
+ </div>
68
+ <div>
69
+ <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
70
+ <%= t("rsb.auth.account.change_password_title") %>
71
+ </button>
72
+ </div>
73
+ <% end %>
74
+ </div>
75
+
76
+ <%# Remove login method %>
77
+ <% if @can_remove %>
78
+ <div class="border-t border-red-200 pt-8">
79
+ <h3 class="text-lg font-medium text-red-600"><%= t("rsb.auth.account.remove_login_method") %></h3>
80
+ <p class="mt-2 text-sm text-gray-600"><%= t("rsb.auth.account.remove_login_method_warning") %></p>
81
+ <div class="mt-4">
82
+ <%= button_to t("rsb.auth.account.remove_login_method"), account_login_method_path(@credential), method: :delete, data: { turbo_confirm: t("rsb.auth.account.remove_confirm") }, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
83
+ </div>
84
+ </div>
85
+ <% end %>
86
+
87
+ <%# Back link %>
88
+ <div class="pt-4">
89
+ <%= link_to t("rsb.auth.account.back_to_account"), account_path, class: "text-sm text-indigo-600 hover:text-indigo-500" %>
90
+ </div>
91
+ </div>
92
+ </div>
@@ -0,0 +1,110 @@
1
+ <div class="min-h-screen flex items-center justify-center px-4 py-12">
2
+ <div class="max-w-2xl w-full space-y-8">
3
+ <div>
4
+ <h2 class="text-3xl font-extrabold text-gray-900"><%= t("rsb.auth.account.title") %></h2>
5
+ <p class="mt-2 text-sm text-gray-600"><%= t("rsb.auth.account.subtitle") %></p>
6
+ </div>
7
+
8
+ <%# Section 1: Login Methods %>
9
+ <div class="border-t border-gray-200 pt-8">
10
+ <h3 class="text-lg font-medium text-gray-900"><%= t("rsb.auth.account.login_methods_title") %></h3>
11
+ <div class="mt-4 space-y-3">
12
+ <% @login_methods.each do |credential| %>
13
+ <%= link_to account_login_method_path(credential), class: "block p-4 bg-white border border-gray-200 rounded-md hover:border-indigo-300 hover:shadow-sm transition" do %>
14
+ <div class="flex items-center justify-between">
15
+ <div>
16
+ <p class="text-sm font-medium text-gray-900"><%= credential.identifier %></p>
17
+ <p class="text-xs text-gray-500"><%= credential.type.demodulize.titleize %></p>
18
+ </div>
19
+ <div class="flex items-center space-x-2">
20
+ <% if credential.verified? %>
21
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><%= t("rsb.auth.account.verified") %></span>
22
+ <% else %>
23
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><%= t("rsb.auth.account.unverified") %></span>
24
+ <% end %>
25
+ <svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
27
+ </svg>
28
+ </div>
29
+ </div>
30
+ <% end %>
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+
35
+ <%# Section 2: Profile Fields (identity fields partial) %>
36
+ <% if @errors.present? %>
37
+ <div class="rounded-md bg-red-50 p-4">
38
+ <div class="ml-3">
39
+ <h3 class="text-sm font-medium text-red-800">There were errors with your submission:</h3>
40
+ <div class="mt-2 text-sm text-red-700">
41
+ <ul class="list-disc pl-5 space-y-1">
42
+ <% @errors.each do |error| %>
43
+ <li><%= error %></li>
44
+ <% end %>
45
+ </ul>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <% end %>
50
+
51
+ <%= form_with model: @identity, url: account_path, method: :patch, class: "space-y-6" do |f| %>
52
+ <% identity_fields_html = capture { render "rsb/auth/account/identity_fields", f: f, identity: @identity } %>
53
+ <%= identity_fields_html %>
54
+
55
+ <% if f.object.errors.any? || identity_fields_html.strip.present? %>
56
+ <div>
57
+ <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
58
+ <%= t("rsb.auth.account.save") %>
59
+ </button>
60
+ </div>
61
+ <% end %>
62
+ <% end %>
63
+
64
+ <%# Section 3: Active Sessions %>
65
+ <div class="border-t border-gray-200 pt-8">
66
+ <h3 class="text-lg font-medium text-gray-900"><%= t("rsb.auth.account.sessions_title") %></h3>
67
+ <div class="mt-4 space-y-3">
68
+ <% @sessions.each do |session| %>
69
+ <% ua = parse_user_agent(session.user_agent) %>
70
+ <div class="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-md">
71
+ <div>
72
+ <p class="text-sm font-medium text-gray-900"><%= ua[:browser] %> / <%= ua[:os] %></p>
73
+ <p class="text-xs text-gray-500">
74
+ <%= session.ip_address %> &middot;
75
+ <% if session.last_active_at %>
76
+ <%= time_ago_in_words(session.last_active_at) %> ago
77
+ <% end %>
78
+ </p>
79
+ </div>
80
+ <div>
81
+ <% if session == @current_session %>
82
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800"><%= t("rsb.auth.account.current_session") %></span>
83
+ <% else %>
84
+ <%= button_to t("rsb.auth.account.revoke_session"), account_session_path(session), method: :delete, class: "inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
85
+ <% end %>
86
+ </div>
87
+ </div>
88
+ <% end %>
89
+ </div>
90
+ <% if @sessions.size > 1 %>
91
+ <div class="mt-4">
92
+ <%= button_to t("rsb.auth.account.revoke_all_sessions"), destroy_all_account_sessions_path, method: :delete, class: "text-sm text-indigo-600 hover:text-indigo-500" %>
93
+ </div>
94
+ <% end %>
95
+ </div>
96
+
97
+ <%# Section 4: Delete Account (danger zone) %>
98
+ <% if @deletion_enabled %>
99
+ <div class="border-t border-red-200 pt-8">
100
+ <h3 class="text-lg font-medium text-red-600"><%= t("rsb.auth.account.danger_zone") %></h3>
101
+ <div class="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
102
+ <p class="text-sm text-red-700"><%= t("rsb.auth.account.delete_warning") %></p>
103
+ <div class="mt-4">
104
+ <%= link_to t("rsb.auth.account.delete_account"), confirm_destroy_account_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ <% end %>
109
+ </div>
110
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="space-y-4">
2
+ <div>
3
+ <label for="identifier" class="block text-sm font-medium mb-1">Email</label>
4
+ <input type="email" name="identifier" id="identifier"
5
+ value="<%= credential&.identifier %>"
6
+ required
7
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary"
8
+ placeholder="user@example.com">
9
+ <% if credential&.errors&.include?(:identifier) %>
10
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:identifier].join(", ") %></p>
11
+ <% end %>
12
+ </div>
13
+
14
+ <div>
15
+ <label for="password" class="block text-sm font-medium mb-1">Password</label>
16
+ <input type="password" name="password" id="password"
17
+ required
18
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary"
19
+ placeholder="Minimum 8 characters">
20
+ <% if credential&.errors&.include?(:password) %>
21
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:password].join(", ") %></p>
22
+ <% end %>
23
+ </div>
24
+
25
+ <div>
26
+ <label for="password_confirmation" class="block text-sm font-medium mb-1">Confirm Password</label>
27
+ <input type="password" name="password_confirmation" id="password_confirmation"
28
+ required
29
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary">
30
+ <% if credential&.errors&.include?(:password_confirmation) %>
31
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:password_confirmation].join(", ") %></p>
32
+ <% end %>
33
+ </div>
34
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="space-y-4">
2
+ <div>
3
+ <label for="identifier" class="block text-sm font-medium mb-1">Phone Number</label>
4
+ <input type="tel" name="identifier" id="identifier"
5
+ value="<%= credential&.identifier %>"
6
+ required
7
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary"
8
+ placeholder="+1234567890">
9
+ <% if credential&.errors&.include?(:identifier) %>
10
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:identifier].join(", ") %></p>
11
+ <% end %>
12
+ </div>
13
+
14
+ <div>
15
+ <label for="password" class="block text-sm font-medium mb-1">Password</label>
16
+ <input type="password" name="password" id="password"
17
+ required
18
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary"
19
+ placeholder="Minimum 8 characters">
20
+ <% if credential&.errors&.include?(:password) %>
21
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:password].join(", ") %></p>
22
+ <% end %>
23
+ </div>
24
+
25
+ <div>
26
+ <label for="password_confirmation" class="block text-sm font-medium mb-1">Confirm Password</label>
27
+ <input type="password" name="password_confirmation" id="password_confirmation"
28
+ required
29
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary">
30
+ <% if credential&.errors&.include?(:password_confirmation) %>
31
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:password_confirmation].join(", ") %></p>
32
+ <% end %>
33
+ </div>
34
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="space-y-4">
2
+ <div>
3
+ <label for="identifier" class="block text-sm font-medium mb-1">Username</label>
4
+ <input type="text" name="identifier" id="identifier"
5
+ value="<%= credential&.identifier %>"
6
+ required
7
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary"
8
+ placeholder="3-30 characters, letters/numbers/dots">
9
+ <% if credential&.errors&.include?(:identifier) %>
10
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:identifier].join(", ") %></p>
11
+ <% end %>
12
+ </div>
13
+
14
+ <div>
15
+ <label for="password" class="block text-sm font-medium mb-1">Password</label>
16
+ <input type="password" name="password" id="password"
17
+ required
18
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary"
19
+ placeholder="Minimum 8 characters">
20
+ <% if credential&.errors&.include?(:password) %>
21
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:password].join(", ") %></p>
22
+ <% end %>
23
+ </div>
24
+
25
+ <div>
26
+ <label for="password_confirmation" class="block text-sm font-medium mb-1">Confirm Password</label>
27
+ <input type="password" name="password_confirmation" id="password_confirmation"
28
+ required
29
+ class="w-full px-3 py-2 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:ring-2 focus:ring-rsb-primary">
30
+ <% if credential&.errors&.include?(:password_confirmation) %>
31
+ <p class="mt-1 text-xs text-rsb-danger-text"><%= credential.errors[:password_confirmation].join(", ") %></p>
32
+ <% end %>
33
+ </div>
34
+ </div>
@@ -0,0 +1,76 @@
1
+ <div class="flex justify-between items-center mb-6">
2
+ <h1 class="text-2xl font-bold">Identities</h1>
3
+ <% if rsb_admin_can?("identities", "new") %>
4
+ <a href="/admin/identities/new"
5
+ class="inline-flex items-center gap-1 px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover transition-colors">
6
+ New Identity
7
+ </a>
8
+ <% end %>
9
+ </div>
10
+
11
+ <% if @filters.any? %>
12
+ <%= render rsb_admin_partial("resources/filters"),
13
+ filters: @filters,
14
+ values: @filter_values,
15
+ resource_key: "identities" %>
16
+ <% end %>
17
+
18
+ <div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm overflow-hidden">
19
+ <% if @identities.any? %>
20
+ <div class="overflow-x-auto">
21
+ <table class="w-full">
22
+ <thead>
23
+ <tr>
24
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Status</th>
25
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Primary Identifier</th>
26
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Credentials</th>
27
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">Created</th>
28
+ <th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border"></th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <% @identities.each do |identity| %>
33
+ <tr class="hover:bg-rsb-bg border-b border-rsb-border last:border-b-0">
34
+ <td class="px-4 py-3 text-sm">
35
+ <% if identity.active? %>
36
+ <span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-success-bg text-rsb-success-text text-xs font-medium">Active</span>
37
+ <% elsif identity.suspended? %>
38
+ <span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-warning-bg text-rsb-warning-text text-xs font-medium">Suspended</span>
39
+ <% else %>
40
+ <span class="inline-block px-2.5 py-0.5 rounded-rsb bg-rsb-danger-bg text-rsb-danger-text text-xs font-medium">Deactivated</span>
41
+ <% end %>
42
+ </td>
43
+ <td class="px-4 py-3 text-sm"><%= identity.primary_identifier || "-" %></td>
44
+ <td class="px-4 py-3 text-sm"><%= identity.credentials.size %></td>
45
+ <td class="px-4 py-3 text-sm"><%= identity.created_at.strftime("%b %d, %Y") %></td>
46
+ <td class="px-4 py-3 text-sm text-right">
47
+ <a href="/admin/identities/<%= identity.id %>"
48
+ class="inline-flex items-center gap-1 px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">View</a>
49
+ </td>
50
+ </tr>
51
+ <% end %>
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+
56
+ <%# Pagination %>
57
+ <% query = @filter_values.any? ? @filter_values.map { |k,v| "q[#{k}]=#{ERB::Util.url_encode(v)}" }.join("&") : nil %>
58
+ <div class="flex justify-between items-center px-4 py-3 border-t border-rsb-border">
59
+ <span class="text-sm text-rsb-muted">Page <%= @current_page + 1 %></span>
60
+ <div class="flex gap-2">
61
+ <% if @current_page > 0 %>
62
+ <a href="/admin/identities?page=<%= @current_page - 1 %><%= "&#{query}" if query %>"
63
+ class="inline-flex items-center px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">Previous</a>
64
+ <% end %>
65
+ <% if @identities.size == @per_page %>
66
+ <a href="/admin/identities?page=<%= @current_page + 1 %><%= "&#{query}" if query %>"
67
+ class="inline-flex items-center px-3 py-1.5 border border-rsb-border text-rsb-text rounded-rsb text-xs font-medium hover:bg-rsb-bg transition-colors">Next</a>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+ <% else %>
72
+ <div class="p-8 text-center">
73
+ <p class="text-rsb-muted text-sm">No identities found. User registrations will appear here.</p>
74
+ </div>
75
+ <% end %>
76
+ </div>
@@ -0,0 +1,46 @@
1
+ <div class="flex justify-between items-center mb-6">
2
+ <h1 class="text-2xl font-bold">New Identity</h1>
3
+ <a href="/admin/identities"
4
+ class="inline-flex items-center gap-1 px-4 py-2 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg transition-colors">Back</a>
5
+ </div>
6
+
7
+ <div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
8
+ <% if @credential_types.any? %>
9
+ <%# Credential Type Selector %>
10
+ <div class="mb-6">
11
+ <label class="block text-sm font-medium mb-2">Credential Type</label>
12
+ <div class="flex gap-2 flex-wrap">
13
+ <% @credential_types.each do |ctype| %>
14
+ <a href="/admin/identities/new?type=<%= ctype.key %>"
15
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 border rounded-rsb text-sm font-medium transition-colors
16
+ <%= ctype.key == @selected_type&.key ? 'bg-rsb-primary text-rsb-primary-text border-rsb-primary' : 'border-rsb-border text-rsb-text hover:bg-rsb-bg' %>">
17
+ <%= ctype.label %>
18
+ </a>
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @selected_type %>
24
+ <form action="/admin/identities" method="post">
25
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
26
+ <%= hidden_field_tag :credential_type, @selected_type.key %>
27
+
28
+ <%# Render the selected type's admin form partial %>
29
+ <%= render partial: @selected_type.admin_form_partial, locals: { credential: @credential } %>
30
+
31
+ <div class="mt-6 flex gap-2">
32
+ <button type="submit"
33
+ class="px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover transition-colors">
34
+ Create Identity
35
+ </button>
36
+ <a href="/admin/identities"
37
+ class="px-4 py-2 border border-rsb-border rounded-rsb text-sm font-medium hover:bg-rsb-bg transition-colors">
38
+ Cancel
39
+ </a>
40
+ </div>
41
+ </form>
42
+ <% end %>
43
+ <% else %>
44
+ <p class="text-rsb-muted text-sm">No credential types with admin form support are available. Register credential types with <code>admin_form_partial</code> to enable identity creation.</p>
45
+ <% end %>
46
+ </div>
@@ -0,0 +1,45 @@
1
+ <div class="flex justify-between items-center mb-6">
2
+ <h1 class="text-2xl font-bold">Add Credential — Identity #<%= @identity.id %></h1>
3
+ <a href="/admin/identities/<%= @identity.id %>"
4
+ class="inline-flex items-center gap-1 px-4 py-2 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg transition-colors">Back</a>
5
+ </div>
6
+
7
+ <div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
8
+ <% if @credential_types.any? %>
9
+ <%# Credential Type Selector %>
10
+ <div class="mb-6">
11
+ <label class="block text-sm font-medium mb-2">Credential Type</label>
12
+ <div class="flex gap-2 flex-wrap">
13
+ <% @credential_types.each do |ctype| %>
14
+ <a href="/admin/identities/<%= @identity.id %>/new_credential?type=<%= ctype.key %>"
15
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 border rounded-rsb text-sm font-medium transition-colors
16
+ <%= ctype.key == @selected_type&.key ? 'bg-rsb-primary text-rsb-primary-text border-rsb-primary' : 'border-rsb-border text-rsb-text hover:bg-rsb-bg' %>">
17
+ <%= ctype.label %>
18
+ </a>
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @selected_type %>
24
+ <form action="/admin/identities/<%= @identity.id %>/add_credential" method="post">
25
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
26
+ <%= hidden_field_tag :credential_type, @selected_type.key %>
27
+
28
+ <%= render partial: @selected_type.admin_form_partial, locals: { credential: @credential } %>
29
+
30
+ <div class="mt-6 flex gap-2">
31
+ <button type="submit"
32
+ class="px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover transition-colors">
33
+ Add Credential
34
+ </button>
35
+ <a href="/admin/identities/<%= @identity.id %>"
36
+ class="px-4 py-2 border border-rsb-border rounded-rsb text-sm font-medium hover:bg-rsb-bg transition-colors">
37
+ Cancel
38
+ </a>
39
+ </div>
40
+ </form>
41
+ <% end %>
42
+ <% else %>
43
+ <p class="text-rsb-muted text-sm"><%= I18n.t("rsb.auth.admin.identities.no_available_types") %></p>
44
+ <% end %>
45
+ </div>