diff options
| -rw-r--r-- | app/views/users/_user.html.erb | 133 | ||||
| -rw-r--r-- | config/locales/de.yml | 5 | ||||
| -rw-r--r-- | config/locales/en.yml | 5 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 106 |
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 | ||
| 908 | table.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 | } |
