diff options
| -rw-r--r-- | app/views/otp_challenges/new.html.erb | 8 | ||||
| -rw-r--r-- | app/views/otp_enrollments/show.html.erb | 11 | ||||
| -rw-r--r-- | app/views/sessions/new.html.erb | 6 | ||||
| -rw-r--r-- | app/views/users/edit.html.erb | 39 | ||||
| -rw-r--r-- | config/application.rb | 2 | ||||
| -rw-r--r-- | config/locales/de.yml | 41 | ||||
| -rw-r--r-- | config/locales/en.yml | 41 | ||||
| -rw-r--r-- | test/controllers/users_controller_test.rb | 7 |
8 files changed, 121 insertions, 34 deletions
diff --git a/app/views/otp_challenges/new.html.erb b/app/views/otp_challenges/new.html.erb index d6e18c85..677244c7 100644 --- a/app/views/otp_challenges/new.html.erb +++ b/app/views/otp_challenges/new.html.erb | |||
| @@ -1,13 +1,13 @@ | |||
| 1 | <h1>Second factor</h1> | 1 | <h1><%= t(".title") %></h1> |
| 2 | 2 | ||
| 3 | <div id="admin_layout"> | 3 | <div id="admin_layout"> |
| 4 | <div class="layout_row_label">Code</div> | 4 | <div class="layout_row_label"> <%= t(".code") %> </div> |
| 5 | <div class="layout_row_content"> | 5 | <div class="layout_row_content"> |
| 6 | <%= form_tag otp_challenge_path, :method => :post, :class => "otp_form" do %> | 6 | <%= form_tag otp_challenge_path, :method => :post, :class => "otp_form" do %> |
| 7 | <%= text_field_tag :code, nil, :autofocus => true, | 7 | <%= text_field_tag :code, nil, :autofocus => true, |
| 8 | :autocomplete => "one-time-code", :inputmode => "numeric" %> | 8 | :autocomplete => "one-time-code", :inputmode => "numeric" %> |
| 9 | <%= submit_tag "Log in" %> | 9 | <%= submit_tag t(".log_in") %> |
| 10 | <% end %> | 10 | <% end %> |
| 11 | <span class="field_hint">Enter the six-digit code from your authenticator app.</span> | 11 | <span class="field_hint"> <%= t(".hint") %> </span> |
| 12 | </div> | 12 | </div> |
| 13 | </div> | 13 | </div> |
diff --git a/app/views/otp_enrollments/show.html.erb b/app/views/otp_enrollments/show.html.erb index 12067ae3..e18d7fd1 100644 --- a/app/views/otp_enrollments/show.html.erb +++ b/app/views/otp_enrollments/show.html.erb | |||
| @@ -1,14 +1,14 @@ | |||
| 1 | <h1>Enable second factor</h1> | 1 | <h1><%= t(".title") %></h1> |
| 2 | 2 | ||
| 3 | <div id="admin_layout"> | 3 | <div id="admin_layout"> |
| 4 | <div class="layout_row_label">Scan</div> | 4 | <div class="layout_row_label"><%= t(".scan") %></div> |
| 5 | <div class="layout_row_content"> | 5 | <div class="layout_row_content"> |
| 6 | <div class="otp_qr"> | 6 | <div class="otp_qr"> |
| 7 | <%= raw RQRCode::QRCode.new(current_user.pending_otp_provisioning_uri) | 7 | <%= raw RQRCode::QRCode.new(current_user.pending_otp_provisioning_uri) |
| 8 | .as_svg(:module_size => 4, :viewbox => true, | 8 | .as_svg(:module_size => 4, :viewbox => true, |
| 9 | :color => "000", :fill => "fff") %> | 9 | :color => "000", :fill => "fff") %> |
| 10 | </div> | 10 | </div> |
| 11 | <span class="field_hint">Or enter the secret manually: <code><%= current_user.otp_pending_secret %></code></span> | 11 | <span class="field_hint"><%= t(".manual_hint") %> <code><%= current_user.otp_pending_secret %></code></span> |
| 12 | </div> | 12 | </div> |
| 13 | 13 | ||
| 14 | <div class="layout_row_label">Confirm</div> | 14 | <div class="layout_row_label">Confirm</div> |
| @@ -17,10 +17,9 @@ | |||
| 17 | <%= text_field_tag :code, nil, :autofocus => true, | 17 | <%= text_field_tag :code, nil, :autofocus => true, |
| 18 | :autocomplete => "one-time-code", :inputmode => "numeric" %> | 18 | :autocomplete => "one-time-code", :inputmode => "numeric" %> |
| 19 | <%= button_tag :type => "submit", :class => "action_button" do %> | 19 | <%= button_tag :type => "submit", :class => "action_button" do %> |
| 20 | <%= icon("shield-check", library: "tabler", "aria-hidden": true) %> Confirm | 20 | <%= icon("shield-check", library: "tabler", "aria-hidden": true) %> <%= t(".confirm") %> |
| 21 | <% end %> | 21 | <% end %> |
| 22 | <% end %> | 22 | <% end %> |
| 23 | <span class="field_hint">Enter the six-digit code your app shows | 23 | <span class="field_hint"> <%= t(".app_hint", :issuer => OTP_ISSUER) %></span> |
| 24 | for “<%= OTP_ISSUER %>”.</span> | ||
| 25 | </div> | 24 | </div> |
| 26 | </div> | 25 | </div> |
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 1c2c2368..3f115d58 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb | |||
| @@ -4,12 +4,12 @@ | |||
| 4 | <div class="error_messages"><%= flash[:error] %></div> | 4 | <div class="error_messages"><%= flash[:error] %></div> |
| 5 | <% end %> | 5 | <% end %> |
| 6 | 6 | ||
| 7 | <div class="layout_row_label">Login</div> | 7 | <div class="layout_row_label"> <%= t(".login") %> </div> |
| 8 | <div class="layout_row_content"><%= text_field_tag 'login', @login, :autocomplete => "username" %></div> | 8 | <div class="layout_row_content"><%= text_field_tag 'login', @login, :autocomplete => "username" %></div> |
| 9 | 9 | ||
| 10 | <div class="layout_row_label">Password</div> | 10 | <div class="layout_row_label"> <%= t(".password") %> </div> |
| 11 | <div class="layout_row_content"><%= password_field_tag 'password', nil, :autocomplete => "current-password" %></div> | 11 | <div class="layout_row_content"><%= password_field_tag 'password', nil, :autocomplete => "current-password" %></div> |
| 12 | 12 | ||
| 13 | <div class="layout_row_content"><%= submit_tag 'log in' %></div> | 13 | <div class="layout_row_content"><%= submit_tag t(".log_in") %></div> |
| 14 | <% end -%> | 14 | <% end -%> |
| 15 | </div> | 15 | </div> |
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index a8994701..ea5e7dec 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | <h1><%= @user == current_user ? "My account" : "Edit user “#{@user.login}”" %></h1> | 1 | <h1><%= @user == current_user ? t(".my_account") : t(".edit_user", :login => @user.login) %></h1> |
| 2 | 2 | ||
| 3 | <% if @user.errors.any? %> | 3 | <% if @user.errors.any? %> |
| 4 | <div class="error_messages"> | 4 | <div class="error_messages"> |
| @@ -9,61 +9,60 @@ | |||
| 9 | <div id="admin_layout"> | 9 | <div id="admin_layout"> |
| 10 | <%= form_for @user do |f| %> | 10 | <%= form_for @user do |f| %> |
| 11 | <div id="content"> | 11 | <div id="content"> |
| 12 | <div class="layout_row_label">Login</div> | 12 | <div class="layout_row_label"><%= t(".login") %></div> |
| 13 | <div class="layout_row_content"><%= f.text_field :login, :autocomplete => "username" %></div> | 13 | <div class="layout_row_content"><%= f.text_field :login, :autocomplete => "username" %></div> |
| 14 | 14 | ||
| 15 | <div class="layout_row_label">E-Mail</div> | 15 | <div class="layout_row_label"><%= t(".email") %></div> |
| 16 | <div class="layout_row_content"><%= f.text_field :email %></div> | 16 | <div class="layout_row_content"><%= f.text_field :email %></div> |
| 17 | 17 | ||
| 18 | <div class="layout_row_label">Password</div> | 18 | <div class="layout_row_label"><%= t(".password") %></div> |
| 19 | <div class="layout_row_content"><%= f.password_field :password, :autocomplete => "new-password" %></div> | 19 | <div class="layout_row_content"><%= f.password_field :password, :autocomplete => "new-password" %></div> |
| 20 | 20 | ||
| 21 | <div class="layout_row_label">Confirm</div> | 21 | <div class="layout_row_label"><%= t(".confirm") %></div> |
| 22 | <div class="layout_row_content"><%= f.password_field :password_confirmation, :autocomplete => "new-password" %></div> | 22 | <div class="layout_row_content"><%= f.password_field :password_confirmation, :autocomplete => "new-password" %></div> |
| 23 | 23 | ||
| 24 | <% if current_user.admin? %> | 24 | <% if current_user.admin? %> |
| 25 | <div class="layout_row_label">Admin?</div> | 25 | <div class="layout_row_label"><%= t(".admin") %></div> |
| 26 | <div class="layout_row_content"><%= f.check_box :admin %></div> | 26 | <div class="layout_row_content"><%= f.check_box :admin %></div> |
| 27 | <% end %> | 27 | <% end %> |
| 28 | 28 | ||
| 29 | <div class="layout_row_label"></div> | 29 | <div class="layout_row_label"></div> |
| 30 | <div class="layout_row_content"><%= f.submit "Update" %></div> | 30 | <div class="layout_row_content"><%= submit_tag t(".update") %></div> |
| 31 | <% end %> | 31 | <% end %> |
| 32 | </div> | 32 | </div> |
| 33 | 33 | ||
| 34 | <% if @user == current_user %> | 34 | <% if @user == current_user %> |
| 35 | <div class="layout_row_label">Second factor</div> | 35 | <div class="layout_row_label"><%= t(".second_factor") %></div> |
| 36 | <div class="layout_row_content"> | 36 | <div class="layout_row_content"> |
| 37 | <% if current_user.otp_enrolled? %> | 37 | <% if current_user.otp_enrolled? %> |
| 38 | <p>Enabled.</p> | 38 | <p><%= t(".enabled") %></p> |
| 39 | <%= form_tag otp_enrollment_path, :method => :delete, :class => "otp_form" do %> | 39 | <%= form_tag otp_enrollment_path, :method => :delete, :class => "otp_form" do %> |
| 40 | <%= text_field_tag :login, current_user.login, :autocomplete => "username", :readonly => true, :tabindex => -1, :class => "visually_hidden" %> | 40 | <%= text_field_tag :login, current_user.login, :autocomplete => "username", :readonly => true, :tabindex => -1, :class => "visually_hidden" %> |
| 41 | <%= password_field_tag :current_password, nil, :placeholder => "Current password", :autocomplete => "current-password" %> | 41 | <%= password_field_tag :current_password, nil, :placeholder => t(".current_password"), :autocomplete => "current-password" %> |
| 42 | <%= text_field_tag :code, nil, :placeholder => "Current code", | 42 | <%= text_field_tag :code, nil, :placeholder => t(".current_code"), |
| 43 | :autocomplete => "one-time-code", :inputmode => "numeric" %> | 43 | :autocomplete => "one-time-code", :inputmode => "numeric" %> |
| 44 | <%= button_tag :type => "submit", :class => "action_button" do %> | 44 | <%= button_tag :type => "submit", :class => "action_button" do %> |
| 45 | <%= icon("shield-off", library: "tabler", "aria-hidden": true) %> Disable second factor | 45 | <%= icon("shield-off", library: "tabler", "aria-hidden": true) %> <%= t(".disable_second_factor") %> |
| 46 | <% end %> | 46 | <% end %> |
| 47 | <% end %> | 47 | <% end %> |
| 48 | <% else %> | 48 | <% else %> |
| 49 | <p>Not enrolled.</p> | 49 | <p><%= t(".not_enrolled") %></p> |
| 50 | <%= form_tag otp_enrollment_path, :method => :post, :class => "otp_form" do %> | 50 | <%= form_tag otp_enrollment_path, :method => :post, :class => "otp_form" do %> |
| 51 | <%= text_field_tag :login, current_user.login, :autocomplete => "username", :readonly => true, :tabindex => -1, :class => "visually_hidden" %> | 51 | <%= text_field_tag :login, current_user.login, :autocomplete => "username", :readonly => true, :tabindex => -1, :class => "visually_hidden" %> |
| 52 | <%= password_field_tag :current_password, nil, :placeholder => "Current password", :autocomplete => "current-password"%> | 52 | <%= password_field_tag :current_password, nil, :placeholder => t(".current_password"), :autocomplete => "current-password"%> |
| 53 | <%= button_tag :type => "submit", :class => "action_button" do %> | 53 | <%= button_tag :type => "submit", :class => "action_button" do %> |
| 54 | <%= icon("shield-lock", library: "tabler", "aria-hidden": true) %> Enable second factor | 54 | <%= icon("shield-lock", library: "tabler", "aria-hidden": true) %> <%= t(".enable_second_factor") %> |
| 55 | <% end %> | 55 | <% end %> |
| 56 | <% end %> | 56 | <% end %> |
| 57 | <% end %> | 57 | <% end %> |
| 58 | </div> | 58 | </div> |
| 59 | <% elsif current_user.admin? && @user.otp_enrolled? %> | 59 | <% elsif current_user.admin? && @user.otp_enrolled? %> |
| 60 | <div class="layout_row_label">Second factor</div> | 60 | <div class="layout_row_label"><%= t(".second_factor") %></div> |
| 61 | <div class="layout_row_content"> | 61 | <div class="layout_row_content"> |
| 62 | Enabled. | 62 | <%= t(".enabled") %> |
| 63 | <%= button_to "Reset second factor", reset_otp_user_path(@user), :method => :put, | 63 | <%= button_to t(".reset_second_factor"), reset_otp_user_path(@user), :method => :put, |
| 64 | :form_class => "button_to destructive", | 64 | :form_class => "button_to destructive", |
| 65 | :form => { :data => { :confirm => | 65 | :form => { :data => { :confirm => t(".reset_confirm", :login => @user.login) } } %> |
| 66 | "Reset #{@user.login}'s second factor? They will log in with password only afterwards." } } %> | ||
| 67 | </div> | 66 | </div> |
| 68 | <% end %> | 67 | <% end %> |
| 69 | </div> | 68 | </div> |
diff --git a/config/application.rb b/config/application.rb index 5ec3e8fe..c8f53ac2 100644 --- a/config/application.rb +++ b/config/application.rb | |||
| @@ -18,7 +18,7 @@ module Cccms | |||
| 18 | 18 | ||
| 19 | config.i18n.default_locale = :de | 19 | config.i18n.default_locale = :de |
| 20 | config.i18n.available_locales = [:de, :en] | 20 | config.i18n.available_locales = [:de, :en] |
| 21 | config.i18n.fallbacks = { en: [:en, :de] } | 21 | config.i18n.fallbacks = { en: [:en, :de], de: [:de, :en] } |
| 22 | 22 | ||
| 23 | config.filter_parameters += [:password, :password_confirmation] | 23 | config.filter_parameters += [:password, :password_confirmation] |
| 24 | config.active_storage.variant_processor = :disabled | 24 | config.active_storage.variant_processor = :disabled |
diff --git a/config/locales/de.yml b/config/locales/de.yml index ae955aa6..7d49a872 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -146,3 +146,44 @@ de: | |||
| 146 | licensed_under: "Lizenziert unter %{license}" | 146 | licensed_under: "Lizenziert unter %{license}" |
| 147 | 147 | ||
| 148 | related_documents: "Weitere Dokumente" | 148 | related_documents: "Weitere Dokumente" |
| 149 | |||
| 150 | sessions: | ||
| 151 | new: | ||
| 152 | login: "Anmeldename" | ||
| 153 | password: "Passwort" | ||
| 154 | log_in: "anmelden" | ||
| 155 | |||
| 156 | otp_challenges: | ||
| 157 | new: | ||
| 158 | title: "Zweiter Faktor" | ||
| 159 | code: "Code" | ||
| 160 | log_in: "Anmelden" | ||
| 161 | hint: "Gib den sechsstelligen Code aus deiner Authenticator-App ein." | ||
| 162 | |||
| 163 | otp_enrollments: | ||
| 164 | show: | ||
| 165 | title: "Zweiten Faktor einrichten" | ||
| 166 | scan: "Scannen" | ||
| 167 | manual_hint: "Oder Secret manuell eingeben:" | ||
| 168 | confirm: "Bestätigen" | ||
| 169 | app_hint: "Gib den sechsstelligen Code ein, den deine App für „%{issuer}“ anzeigt." | ||
| 170 | |||
| 171 | users: | ||
| 172 | edit: | ||
| 173 | my_account: "Mein Konto" | ||
| 174 | edit_user: "Benutzer „%{login}“ bearbeiten" | ||
| 175 | login: "Anmeldename" | ||
| 176 | email: "E-Mail" | ||
| 177 | password: "Passwort" | ||
| 178 | confirm: "bestätigen" | ||
| 179 | admin: "Admin?" | ||
| 180 | update: "Aktualisieren" | ||
| 181 | enabled: "Aktiviert." | ||
| 182 | not_enrolled: "Nicht eingerichtet." | ||
| 183 | current_password: "Aktuelles Passwort" | ||
| 184 | current_code: "Aktueller Code" | ||
| 185 | second_factor: "Zweiter Faktor" | ||
| 186 | enable_second_factor: "Zweiten Faktor aktivieren" | ||
| 187 | disable_second_factor: "Zweiten Faktor entfernen" | ||
| 188 | reset_second_factor: "Zweiten Faktor zurücksetzen" | ||
| 189 | reset_confirm: "Zweiten Faktor von %{login} zurücksetzen? Die Anmeldung ist danach nur mit Passwort möglich." | ||
diff --git a/config/locales/en.yml b/config/locales/en.yml index 618f5164..ea94952a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -98,3 +98,44 @@ en: | |||
| 98 | licensed_under: "Licensed under %{license}" | 98 | licensed_under: "Licensed under %{license}" |
| 99 | 99 | ||
| 100 | related_documents: "Related documents" | 100 | related_documents: "Related documents" |
| 101 | |||
| 102 | sessions: | ||
| 103 | new: | ||
| 104 | login: "Login" | ||
| 105 | password: "Password" | ||
| 106 | log_in: "log in" | ||
| 107 | |||
| 108 | otp_challenges: | ||
| 109 | new: | ||
| 110 | title: "Second factor" | ||
| 111 | code: "Code" | ||
| 112 | log_in: "Log in" | ||
| 113 | hint: "Enter the six-digit code from your authenticator app." | ||
| 114 | |||
| 115 | otp_enrollments: | ||
| 116 | show: | ||
| 117 | title: "Enable second factor" | ||
| 118 | scan: "Scan" | ||
| 119 | manual_hint: "Or enter the secret manually:" | ||
| 120 | confirm: "Confirm" | ||
| 121 | app_hint: "Enter the six-digit code your app shows for “%{issuer}”." | ||
| 122 | |||
| 123 | users: | ||
| 124 | edit: | ||
| 125 | my_account: "My account" | ||
| 126 | edit_user: "Edit user “%{login}”" | ||
| 127 | login: "login" | ||
| 128 | email: "e-mail" | ||
| 129 | password: "password" | ||
| 130 | confirm: "confirm" | ||
| 131 | admin: "admin?" | ||
| 132 | update: "Update" | ||
| 133 | enabled: "Enabled." | ||
| 134 | not_enrolled: "Not enrolled." | ||
| 135 | current_password: "Current password" | ||
| 136 | current_code: "Current code" | ||
| 137 | second_factor: "Second factor" | ||
| 138 | enable_second_factor: "Enable second factor" | ||
| 139 | disable_second_factor: "Disable second factor" | ||
| 140 | reset_second_factor: "Reset second factor" | ||
| 141 | reset_confirm: "Reset %{login}'s second factor? They will log in with password only afterwards." | ||
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 946b5414..14f12b52 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb | |||
| @@ -163,6 +163,13 @@ class UsersControllerTest < ActionController::TestCase | |||
| 163 | end | 163 | end |
| 164 | assert_redirected_to users_path | 164 | assert_redirected_to users_path |
| 165 | end | 165 | end |
| 166 | |||
| 167 | test "enrolled user gets a working my account" do | ||
| 168 | users(:quentin).update!(:otp_secret => ROTP::Base32.random) | ||
| 169 | login_as :quentin | ||
| 170 | get :edit, params: { :id => users(:quentin).id } | ||
| 171 | assert_response :success | ||
| 172 | end | ||
| 166 | 173 | ||
| 167 | test "admin user can promote regular users to admins" do | 174 | test "admin user can promote regular users to admins" do |
| 168 | login_as :aaron | 175 | login_as :aaron |
