summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/elevations_controller.rb11
-rw-r--r--app/controllers/otp_challenges_controller.rb7
-rw-r--r--app/views/layouts/_elevation_banner.html.erb47
-rw-r--r--app/views/layouts/admin.html.erb1
-rw-r--r--config/locales/de.yml10
-rw-r--r--config/locales/en.yml10
-rw-r--r--config/routes.rb4
-rw-r--r--lib/authenticated_system.rb20
-rw-r--r--public/stylesheets/admin.css40
9 files changed, 147 insertions, 3 deletions
diff --git a/app/controllers/elevations_controller.rb b/app/controllers/elevations_controller.rb
index 804b8f95..229f33d8 100644
--- a/app/controllers/elevations_controller.rb
+++ b/app/controllers/elevations_controller.rb
@@ -41,4 +41,15 @@ class ElevationsController < ApplicationController
41 flash[:notice] = t("flash.elevation.dropped") 41 flash[:notice] = t("flash.elevation.dropped")
42 redirect_to admin_path 42 redirect_to admin_path
43 end 43 end
44
45 def renew
46 if renew_elevation!
47 flash[:notice] = t("flash.elevation.renewed",
48 :minutes => AuthenticatedSystem::ELEVATION_MAX_AGE.in_minutes.to_i,
49 :left => elevation_extensions_left)
50 else
51 flash[:error] = t("flash.elevation.cannot_renew")
52 end
53 redirect_back(:fallback_location => admin_path, :allow_other_host => false)
54 end
44end 55end
diff --git a/app/controllers/otp_challenges_controller.rb b/app/controllers/otp_challenges_controller.rb
index 87586241..2526d1fb 100644
--- a/app/controllers/otp_challenges_controller.rb
+++ b/app/controllers/otp_challenges_controller.rb
@@ -31,7 +31,12 @@ class OtpChallengesController < ApplicationController
31 # an admin who logs in and goes straight to user management 31 # an admin who logs in and goes straight to user management
32 # is already elevated 32 # is already elevated
33 elevate! if user.is_admin? 33 elevate! if user.is_admin?
34 flash[:notice] = t("flash.common.logged_in") 34 flash[:notice] = if user.is_admin?
35 t("flash.elevation.granted_at_login",
36 :minutes => AuthenticatedSystem::ELEVATION_MAX_AGE.in_minutes.to_i)
37 else
38 t("flash.common.logged_in")
39 end
35 redirect_to safe_return_to(return_to, :default => admin_path) 40 redirect_to safe_return_to(return_to, :default => admin_path)
36 else 41 else
37 flash.now[:error] = t("flash.otp.code_mismatch") 42 flash.now[:error] = t("flash.otp.code_mismatch")
diff --git a/app/views/layouts/_elevation_banner.html.erb b/app/views/layouts/_elevation_banner.html.erb
new file mode 100644
index 00000000..a7d15a2e
--- /dev/null
+++ b/app/views/layouts/_elevation_banner.html.erb
@@ -0,0 +1,47 @@
1<% if elevated? %>
2 <div id="elevation_banner"
3 data-expires-at="<%= elevation_expires_at.to_i %>"
4 data-label-minutes="<%= t(".minutes_short") %>"
5 data-label-less-than-a-minute="<%= t(".less_than_a_minute") %>"
6 data-label-expired="<%= t(".expired") %>">
7 <span class="elevation_state">
8 <%= icon("shield-check", library: "tabler", "aria-hidden": true) %>
9 <%= t(".active") %>
10 <span id="elevation_remaining"></span>
11 </span>
12 <span class="elevation_actions">
13 <% if elevation_extensions_left > 0 %>
14 <%= button_to t(".renew"), renew_elevation_path, method: :post,
15 form: { class: "button_to state_changing" } %>
16 <% end %>
17 <%= button_to t(".drop"), elevation_path, method: :delete,
18 form: { class: "button_to destructive" } %>
19 </span>
20 </div>
21
22 <%= javascript_tag nonce: true do %>
23 (function () {
24 var banner = document.getElementById("elevation_banner");
25 if (!banner) return;
26 var out = document.getElementById("elevation_remaining");
27 var expires = parseInt(banner.dataset.expiresAt, 10) * 1000;
28
29 function tick() {
30 var ms = expires - Date.now();
31 if (ms <= 0) {
32 out.textContent = banner.dataset.labelExpired;
33 banner.classList.remove("elevation_soon");
34 banner.classList.add("elevation_expired");
35 return;
36 }
37 var mins = Math.floor(ms / 60000);
38 out.textContent = mins < 1
39 ? banner.dataset.labelLessThanAMinute
40 : banner.dataset.labelMinutes.replace("%{count}", mins);
41 if (mins < 5) banner.classList.add("elevation_soon");
42 setTimeout(tick, 15000);
43 }
44 tick();
45 })();
46 <% end %>
47<% end %>
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb
index 43f09b96..a84c8927 100644
--- a/app/views/layouts/admin.html.erb
+++ b/app/views/layouts/admin.html.erb
@@ -43,6 +43,7 @@
43 </div> 43 </div>
44 </div> 44 </div>
45 <div class="admin_content_spacer"></div> 45 <div class="admin_content_spacer"></div>
46 <%= render "layouts/elevation_banner" %>
46 <%= render "layouts/flash" %> 47 <%= render "layouts/flash" %>
47 <div id="content"> 48 <div id="content">
48 <%= yield %> 49 <%= yield %>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 7edffe2d..6b5c97cc 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -630,6 +630,9 @@ de:
630 removed: "%{lang}-Übersetzung aus dem Entwurf entfernt. Veröffentlichen, um das dauerhaft zu machen." 630 removed: "%{lang}-Übersetzung aus dem Entwurf entfernt. Veröffentlichen, um das dauerhaft zu machen."
631 elevation: 631 elevation:
632 dropped: "Administrative Rechte abgelegt." 632 dropped: "Administrative Rechte abgelegt."
633 renewed: "Administrative Rechte um %{minutes} Minuten verlängert. Noch %{left} Verlängerungen möglich."
634 cannot_renew: "Verlängern nicht möglich. Rechte müssen mit einem aktuellen Code neu angefordert werden."
635 granted_at_login: "Angemeldet. Administrative Rechte gelten %{minutes} Minuten."
633 636
634 assets: 637 assets:
635 index: 638 index:
@@ -767,3 +770,10 @@ de:
767 log_out: "Abmelden" 770 log_out: "Abmelden"
768 social_meta: 771 social_meta:
769 site_description: "Der Chaos Computer Club ist eine galaktische Gemeinschaft von Lebewesen für Informationsfreiheit und Technikfolgenabschätzung." 772 site_description: "Der Chaos Computer Club ist eine galaktische Gemeinschaft von Lebewesen für Informationsfreiheit und Technikfolgenabschätzung."
773 elevation_banner:
774 active: "Administrative Rechte aktiv, "
775 minutes_short: "noch %{count} Min."
776 less_than_a_minute: "weniger als 1 Min."
777 expired: "abgelaufen"
778 renew: "Verlängern"
779 drop: "Ablegen"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9a8041bf..e4c7ecc4 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -589,6 +589,9 @@ en:
589 removed: "%{lang} translation removed from the draft. Publish to make this permanent." 589 removed: "%{lang} translation removed from the draft. Publish to make this permanent."
590 elevation: 590 elevation:
591 dropped: "Administrative rights dropped." 591 dropped: "Administrative rights dropped."
592 renewed: "Administrative rights extended by %{minutes} minutes. %{left} extensions left."
593 cannot_renew: "Cannot extend. Request the rights again with a current code."
594 granted_at_login: "Signed in. Administrative rights last %{minutes} minutes."
592 595
593 assets: 596 assets:
594 index: 597 index:
@@ -710,3 +713,10 @@ en:
710 log_out: "Log out" 713 log_out: "Log out"
711 social_meta: 714 social_meta:
712 site_description: "The Chaos Computer Club is a galactic community of life forms campaigning for freedom of information and the assessment of the impact of technology." 715 site_description: "The Chaos Computer Club is a galactic community of life forms campaigning for freedom of information and the assessment of the impact of technology."
716 elevation_banner:
717 active: "Administrative rights active —"
718 minutes_short: "%{count} min left"
719 less_than_a_minute: "less than 1 min"
720 expired: "expired"
721 renew: "Extend"
722 drop: "Drop"
diff --git a/config/routes.rb b/config/routes.rb
index 4c37e70f..58e1e632 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -106,7 +106,9 @@ Cccms::Application.routes.draw do
106 end 106 end
107 resource :otp_enrollment, :only => [:show, :create, :update, :destroy] 107 resource :otp_enrollment, :only => [:show, :create, :update, :destroy]
108 resource :otp_challenge, :only => [:new, :create] 108 resource :otp_challenge, :only => [:new, :create]
109 resource :elevation, :only => [:new, :create, :destroy] 109 resource :elevation, :only => [:new, :create, :destroy] do
110 post :renew
111 end
110 112
111 resources :menu_items, :except => :show do 113 resources :menu_items, :except => :show do
112 member do 114 member do
diff --git a/lib/authenticated_system.rb b/lib/authenticated_system.rb
index 9a351dde..04d8051f 100644
--- a/lib/authenticated_system.rb
+++ b/lib/authenticated_system.rb
@@ -1,6 +1,7 @@
1module AuthenticatedSystem 1module AuthenticatedSystem
2 SESSION_MAX_AGE = 7.days 2 SESSION_MAX_AGE = 7.days
3 ELEVATION_MAX_AGE = 30.minutes 3 ELEVATION_MAX_AGE = 30.minutes
4 MAX_ELEVATION_EXTENSIONS = 3
4 5
5 protected 6 protected
6 # Returns true or false if the user is logged in. 7 # Returns true or false if the user is logged in.
@@ -35,10 +36,26 @@ module AuthenticatedSystem
35 36
36 def elevate! 37 def elevate!
37 session[:elevated_at] = Time.now.to_i 38 session[:elevated_at] = Time.now.to_i
39 session[:elevation_extensions] = 0
40 end
41
42 def elevation_extensions_left
43 return 0 unless elevated?
44 MAX_ELEVATION_EXTENSIONS - session[:elevation_extensions].to_i
45 end
46
47 def renew_elevation!
48 return false unless elevated?
49 return false unless elevation_extensions_left > 0
50
51 session[:elevation_extensions] = session[:elevation_extensions].to_i + 1
52 session[:elevated_at] = Time.now.to_i
53 true
38 end 54 end
39 55
40 def drop_elevation! 56 def drop_elevation!
41 session.delete(:elevated_at) 57 session.delete(:elevated_at)
58 session.delete(:elevation_extensions)
42 end 59 end
43 60
44 # Check if the user is authorized 61 # Check if the user is authorized
@@ -113,7 +130,7 @@ module AuthenticatedSystem
113 # available as ActionView helper methods. 130 # available as ActionView helper methods.
114 def self.included(base) 131 def self.included(base)
115 base.send :helper_method, :current_user, :logged_in?, :authorized?, 132 base.send :helper_method, :current_user, :logged_in?, :authorized?,
116 :elevated?, :elevation_expires_at if base.respond_to? :helper_method 133 :elevated?, :elevation_expires_at, :elevation_extensions_left if base.respond_to? :helper_method
117 end 134 end
118 135
119 # 136 #
@@ -147,6 +164,7 @@ module AuthenticatedSystem
147 session[:user_id] = nil # keeps the session but kill our variable 164 session[:user_id] = nil # keeps the session but kill our variable
148 session.delete(:elevated_at) 165 session.delete(:elevated_at)
149 session.delete(:elevation_attempts) 166 session.delete(:elevation_attempts)
167 session.delete(:elevation_extensions)
150 end 168 end
151 169
152 # The session should only be reset at the tail end of a form POST -- 170 # The session should only be reset at the tail end of a form POST --
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 7f286705..be8725d3 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -276,6 +276,46 @@ span.warning a {
276 padding-left: 1.25rem; 276 padding-left: 1.25rem;
277} 277}
278 278
279#elevation_banner {
280 display: flex;
281 flex-wrap: wrap;
282 align-items: center;
283 gap: 0.5rem 1rem;
284 margin-bottom: 1rem;
285 padding: 0.5rem 0.75rem;
286 border: 1px solid var(--accent);
287 border-radius: var(--radius);
288 background-color: var(--accent-surface);
289}
290
291#elevation_banner .elevation_state {
292 display: inline-flex;
293 align-items: center;
294 gap: 0.35rem;
295}
296
297#elevation_banner .elevation_actions {
298 display: inline-flex;
299 align-items: center;
300 gap: 0.5rem;
301 margin-left: auto;
302}
303
304#elevation_banner svg {
305 width: 1.25rem;
306 height: 1.25rem;
307}
308
309#elevation_banner #elevation_remaining {
310 color: var(--text-muted);
311}
312
313#elevation_banner.elevation_soon #elevation_remaining,
314#elevation_banner.elevation_expired #elevation_remaining {
315 color: var(--accent);
316 font-weight: bold;
317}
318
279/* ============================================================ 319/* ============================================================
280 Pagination 320 Pagination
281 ============================================================ */ 321 ============================================================ */