summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-01 17:52:46 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-01 17:52:46 +0200
commit4175f26cc8b22ff5a07929fa2436c561022cccd2 (patch)
treea73509f797bc609c4492340a97c45276f7274af1
parent1384756b81c719f899359efc9125e80fae8614c0 (diff)
tidy up user list, make otp flag more visible
-rw-r--r--app/views/users/_user.html.erb133
-rw-r--r--config/locales/de.yml5
-rw-r--r--config/locales/en.yml5
-rw-r--r--public/stylesheets/admin.css106
4 files changed, 167 insertions, 82 deletions
diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb
index c9b21f0a..26d6cab2 100644
--- a/app/views/users/_user.html.erb
+++ b/app/views/users/_user.html.erb
@@ -1,70 +1,95 @@
1<% users.each do |user| %> 1<% users.each do |user| %>
2<tr> 2<tr>
3 <td class="user_login"><%= user.login %></td> 3 <td class="user_login">
4 <td class="user_roles"> 4 <% if user.otp_enrolled? %>
5 <% if user.roles.any? %> 5 <span role="img" title="<%= t(".otp_enrolled") %>" aria-label="<%= t(".otp_enrolled") %>">
6 <%= user.role_labels.join(", ") %> 6 <%= icon("shield-check", library: "tabler", "aria-hidden": true) %>
7 </span>
8 <% elsif user.otp_pending_secret.present? %>
9 <span role="img" class="otp_missing" title="<%= t(".otp_pending") %>" aria-label="<%= t(".otp_pending") %>">
10 <%= icon("shield-half", library: "tabler", "aria-hidden": true) %>
11 </span>
7 <% else %> 12 <% else %>
8 <span class="field_hint"><%= t(".no_roles") %></span> 13 <span role="img" class="otp_missing" title="<%= t(".otp_missing") %>" aria-label="<%= t(".otp_missing") %>">
9 <% end %> 14 <%= icon("shield-off", library: "tabler", "aria-hidden": true) %>
10 </td> 15 </span>
11 <td class="user_action">
12 <%= link_to user_path(user), "aria-label" => t("admin.common.show"),
13 title: t("admin.common.show") do %>
14 <%= icon("eye", library: "tabler", "aria-hidden": true) %>
15 <% end %> 16 <% end %>
17 <%= user.login %>
16 </td> 18 </td>
17 <td class="user_action"> 19 <td class="user_roles">
18 <% if current_user.admin? || current_user == user %> 20 <% if user.roles.any? %>
19 <%= link_to edit_user_path(user), "aria-label" => t("admin.common.edit"), 21 <% user.role_labels.each do |label| %>
20 title: t("admin.common.edit") do %> 22 <span class="user_role"><%= label %></span>
21 <%= icon("edit", library: "tabler", "aria-hidden": true) %>
22 <% end %> 23 <% end %>
24 <% else %>
25 <span class="field_hint"><%= t(".no_roles") %></span>
23 <% end %> 26 <% end %>
24 </td> 27 </td>
25 <td class="user_action"> 28 <td class="user_actions">
26 <% if current_user.admin? && current_user != user %> 29 <div class="user_action_grid">
27 <% if user.alumni? %> 30 <span class="user_action">
28 <%= button_to reactivate_user_path(user), method: :put, 31 <%= link_to user_path(user), "aria-label" => t("admin.common.show"),
29 form: { class: 'button_to state_changing' }, 32 title: t("admin.common.show") do %>
30 "aria-label" => t(".reactivate"), title: t(".reactivate") do %> 33 <%= icon("eye", library: "tabler", "aria-hidden": true) %>
31 <%= icon("user-check", library: "tabler", "aria-hidden": true) %>
32 <% end %> 34 <% end %>
33 <% else %> 35 </span>
34 <%= button_to deactivate_user_path(user), method: :put, 36
35 form: { data: { confirm: t(".confirm_deactivate", :login => user.login) }, 37 <span class="user_action">
36 class: 'button_to destructive' }, 38 <% if current_user.admin? || current_user == user %>
37 "aria-label" => t(".deactivate"), title: t(".deactivate") do %> 39 <%= link_to edit_user_path(user), "aria-label" => t("admin.common.edit"),
38 <%= icon("user-off", library: "tabler", "aria-hidden": true) %> 40 title: t("admin.common.edit") do %>
41 <%= icon("edit", library: "tabler", "aria-hidden": true) %>
42 <% end %>
39 <% end %> 43 <% end %>
40 <% end %> 44 </span>
41 <% end %> 45
42 </td> 46 <span class="user_action">
43 <td class="user_action"> 47 <% if current_user.admin? && current_user != user %>
44 <% if current_user.redaktion? && !user.alumni? %> 48 <% if user.alumni? %>
45 <% if user.redaktion? %> 49 <%= button_to reactivate_user_path(user), method: :put,
46 <% unless user == current_user %> 50 form: { class: 'button_to state_changing' },
47 <%= button_to revoke_redaktion_user_path(user), method: :put, 51 "aria-label" => t(".reactivate"), title: t(".reactivate") do %>
48 form: { data: { confirm: t(".confirm_revoke_redaktion", :login => user.login) }, 52 <%= icon("user-check", library: "tabler", "aria-hidden": true) %>
49 class: 'button_to destructive' }, 53 <% end %>
50 "aria-label" => t(".revoke_redaktion"), title: t(".revoke_redaktion") do %> 54 <% else %>
51 <%= icon("user-minus", library: "tabler", "aria-hidden": true) %> 55 <%= button_to deactivate_user_path(user), method: :put,
56 form: { data: { confirm: t(".confirm_deactivate", :login => user.login) },
57 class: 'button_to destructive' },
58 "aria-label" => t(".deactivate"), title: t(".deactivate") do %>
59 <%= icon("user-off", library: "tabler", "aria-hidden": true) %>
60 <% end %>
52 <% end %> 61 <% end %>
53 <% end %> 62 <% end %>
54 <% elsif user.otp_enrolled? %> 63 </span>
55 <%= button_to grant_redaktion_user_path(user), method: :put, 64
56 form: { data: { confirm: t(".confirm_grant_redaktion", :login => user.login) }, 65 <span class="user_action">
57 class: 'button_to state_changing' }, 66 <% if current_user.redaktion? && !user.alumni? %>
58 "aria-label" => t(".grant_redaktion"), title: t(".grant_redaktion") do %> 67 <% if user.redaktion? %>
59 <%= icon("user-plus", library: "tabler", "aria-hidden": true) %> 68 <% unless user == current_user %>
69 <%= button_to revoke_redaktion_user_path(user), method: :put,
70 form: { data: { confirm: t(".confirm_revoke_redaktion", :login => user.login) },
71 class: 'button_to destructive' },
72 "aria-label" => t(".revoke_redaktion"), title: t(".revoke_redaktion") do %>
73 <%= icon("user-minus", library: "tabler", "aria-hidden": true) %>
74 <% end %>
75 <% end %>
76 <% elsif user.otp_enrolled? %>
77 <%= button_to grant_redaktion_user_path(user), method: :put,
78 form: { data: { confirm: t(".confirm_grant_redaktion", :login => user.login) },
79 class: 'button_to state_changing' },
80 "aria-label" => t(".grant_redaktion"), title: t(".grant_redaktion") do %>
81 <%= icon("user-plus", library: "tabler", "aria-hidden": true) %>
82 <% end %>
83 <% else %>
84 <button type="button" class="disabled_action" disabled
85 title="<%= t(".grant_redaktion_blocked") %>"
86 aria-label="<%= t(".grant_redaktion_blocked") %>">
87 <%= icon("user-plus", library: "tabler", "aria-hidden": true) %>
88 </button>
89 <% end %>
60 <% end %> 90 <% end %>
61 <% else %> 91 </span>
62 <span class="field_hint" title="<%= t(".needs_otp") %>" 92 </div>
63 aria-label="<%= t(".needs_otp") %>">
64 <%= icon("shield-off", library: "tabler", "aria-hidden": true) %>
65 </span>
66 <% end %>
67 <% end %>
68 </td> 93 </td>
69</tr> 94</tr>
70<% end %> 95<% end %>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 67117945..fcceb023 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -298,7 +298,10 @@ de:
298 revoke_redaktion: "Aus der Redaktion entfernen" 298 revoke_redaktion: "Aus der Redaktion entfernen"
299 confirm_grant_redaktion: "%{login} in die Redaktion aufnehmen? Damit darf %{login} in den geschützten Bereichen veröffentlichen." 299 confirm_grant_redaktion: "%{login} in die Redaktion aufnehmen? Damit darf %{login} in den geschützten Bereichen veröffentlichen."
300 confirm_revoke_redaktion: "%{login} aus der Redaktion entfernen?" 300 confirm_revoke_redaktion: "%{login} aus der Redaktion entfernen?"
301 needs_otp: "zweiter Faktor fehlt" 301 otp_enrolled: "Zweiter Faktor eingerichtet"
302 otp_pending: "Zweiter Faktor begonnen, aber nicht bestätigt"
303 otp_missing: "Kein zweiter Faktor"
304 grant_redaktion_blocked: "In die Redaktion aufnehmen — erst möglich, wenn das Konto einen zweiten Faktor eingerichtet hat"
302 index: 305 index:
303 title: "Benutzerkonten" 306 title: "Benutzerkonten"
304 create_editor: "Editor-Konto anlegen" 307 create_editor: "Editor-Konto anlegen"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 95eb7e94..81d9f4fb 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -242,7 +242,10 @@ en:
242 revoke_redaktion: "Remove from Redaktion" 242 revoke_redaktion: "Remove from Redaktion"
243 confirm_grant_redaktion: "Add %{login} to Redaktion? They will be able to publish in the protected sections." 243 confirm_grant_redaktion: "Add %{login} to Redaktion? They will be able to publish in the protected sections."
244 confirm_revoke_redaktion: "Remove %{login} from Redaktion?" 244 confirm_revoke_redaktion: "Remove %{login} from Redaktion?"
245 needs_otp: "no second factor" 245 otp_enrolled: "Second factor enrolled"
246 otp_pending: "Second factor started but not confirmed"
247 otp_missing: "No second factor"
248 grant_redaktion_blocked: "Add to Redaktion — only possible once the account has enrolled a second factor"
246 249
247 index: 250 index:
248 title: "User accounts" 251 title: "User accounts"
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index a54c47a3..f415379a 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -837,41 +837,99 @@ table.revisions_table tr:hover {
837 margin: 0; 837 margin: 0;
838} 838}
839 839
840.user_group_heading { 840.user_table {
841 margin-top: 1.5rem; 841 width: 100%;
842} 842 max-width: 44rem;
843 843 margin-bottom: 1.5rem;
844/* Retired accounts: present for attribution, not for action. */
845.user_group_alumni,
846.user_table_alumni {
847 color: var(--text-muted);
848} 844}
849 845
850.user_table td.user_action { 846.user_table td.user_actions {
851 width: 1px; 847 width: 1px;
852 white-space: nowrap; 848 white-space: nowrap;
853 text-align: center;
854} 849}
855 850
856.user_table td.user_action a { 851.user_table .user_action_grid {
857 text-decoration: none; 852 display: grid;
853 grid-template-columns: repeat(2, auto);
854 gap: 0.4rem;
855 justify-content: end;
856}
857
858.user_table .user_action {
859 display: inline-flex;
860 min-height: 2rem;
858} 861}
859 862
860.user_table td.user_action svg { 863.user_table .user_action svg {
861 width: 1.25rem; 864 width: 1.25rem;
862 height: 1.25rem; 865 height: 1.25rem;
863} 866}
864 867
865.user_table td.user_action .field_hint svg { 868.user_table .user_action form.button_to {
869 display: inline-flex;
870 margin: 0;
871}
872
873.user_table .user_action a,
874.user_table .user_action button[type="submit"],
875.user_table .user_action .disabled_action {
876 display: inline-flex;
877 align-items: center;
878 justify-content: center;
879 padding: 0.3rem;
880 border: 1px solid transparent;
881 border-radius: 2px;
882 background-color: var(--surface-raised);
883 color: var(--text);
884 text-decoration: none;
885}
886
887.user_table .user_action a:hover {
888 color: var(--surface);
889 background-color: var(--text);
890}
891
892.user_table .user_action form.button_to.destructive button:hover {
893 color: var(--on-solid);
894 background-color: var(--danger-solid);
895}
896
897.user_table .user_action form.button_to.state_changing button:hover {
898 color: var(--on-solid);
899 background-color: var(--action-solid);
900}
901
902.user_table .user_action .disabled_action {
866 color: var(--text-muted); 903 color: var(--text-muted);
867 vertical-align: middle; 904 background-color: transparent;
905 border-color: var(--border);
906 cursor: not-allowed;
868} 907}
869 908
870/* Four groups stacked, and the last one needs clearance from the wrapper. */ 909.user_table .user_role {
871.user_table { 910 display: block;
872 width: 100%; 911}
873 max-width: 44rem; 912
874 margin-bottom: 1.5rem; 913.user_table .user_login svg {
914 color: var(--text-muted);
915 vertical-align: -0.15em;
916 margin-right: 0.5rem;
917 width: 1rem;
918 height: 1rem;
919}
920
921.user_table .user_login .otp_missing svg {
922 color: var(--accent);
923}
924
925.user_group_heading {
926 margin-top: 1.5rem;
927}
928
929/* Retired accounts: present for attribution, not for action. */
930.user_group_alumni,
931.user_table_alumni {
932 color: var(--text-muted);
875} 933}
876 934
877.role_choice { 935.role_choice {
@@ -905,10 +963,6 @@ table.revisions_table tr:hover {
905 } 963 }
906} 964}
907 965
908table.user_table td.user_login {
909 padding-right: 30px;
910}
911
912/* ============================================================ 966/* ============================================================
913 Node display (nodes#show / nodes#new) 967 Node display (nodes#show / nodes#new)
914 ============================================================ */ 968 ============================================================ */
@@ -947,7 +1001,7 @@ div.layout_row_content {
947 padding: 0.5rem 0.75rem; 1001 padding: 0.5rem 0.75rem;
948} 1002}
949 1003
950.info_group .disabled_action { 1004.disabled_action {
951 display: inline-flex; 1005 display: inline-flex;
952 align-items: center; 1006 align-items: center;
953 gap: 0.35rem; 1007 gap: 0.35rem;
@@ -960,7 +1014,7 @@ div.layout_row_content {
960 line-height: 1.25rem; 1014 line-height: 1.25rem;
961} 1015}
962 1016
963.info_group .disabled_action svg { 1017.disabled_action svg {
964 width: 1.25rem; 1018 width: 1.25rem;
965 height: 1.25rem; 1019 height: 1.25rem;
966} 1020}