bible270 1.1.6 → 1.1.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 323ea67db4332521a07129d64ca3b1e4f6405ba0d87352f14db5dcbcbbe675c7
4
- data.tar.gz: b60e969effcc93b68b95c29e8054aae754a41210641146ce526eefd6d28e0229
3
+ metadata.gz: b7014361816b071aaacc54d569f247ff116cc2c5c0f49ea4490e46a2e62375f4
4
+ data.tar.gz: d61942b56c9409563d03e8b76e2f3dea0ba6a73d0119e4d0dade38bdcb177ec2
5
5
  SHA512:
6
- metadata.gz: 799b578048236367edd22c1b75e3c62d9c2b24a8dce6d28174695954a465d97ddffb75164d790b207d7aaca3d225a2a8f1f00e6e2d932cfd90b9824277c53f7d
7
- data.tar.gz: 6c7c718e5e2c3b434090eb38481371ec0a63399769d0d54ded50fbee47de914bcbd5fe49605169b7a81b012f979826333d28b2aa349e8392955bd9567187a49c
6
+ metadata.gz: ee0357bfb87e84ab59b359ba11a274c380834587008263b7229e3d8f3edd04c02a1211045ebabec90545c4384c0404d872913c1caa2461609ec09aa4339bce88
7
+ data.tar.gz: 2e413b639bdccfbc194a44a7e30051d5c304ed2205625c3b3c79749da20f463178c538b6b9db43233843d783e2bfb91fce2e43121f55b7ff798a55798564a1a7
data/CHANGELOG.md CHANGED
@@ -6,7 +6,7 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
6
6
 
7
7
  ### Changed
8
8
 
9
- - show administrators a quiet, linked completed-day count on Community reader rows while keeping progress hidden—and its aggregate query skipped—for ordinary readers and visitors
9
+ - show administrators a quiet, linked completed-day count on Community reader rows while keeping progress hidden—and its aggregate query skipped—for ordinary readers and visitors; the link stays unadorned at rest and uses the site’s gold hover and keyboard-focus treatment
10
10
 
11
11
  **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.1.5...main)**
12
12
 
@@ -552,6 +552,7 @@ This is the initial public release.
552
552
 
553
553
  * Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
554
554
 
555
+ [1.1.6]: https://github.com/avonderluft/bible270/releases/tag/v1.1.6
555
556
  [1.1.5]: https://github.com/avonderluft/bible270/releases/tag/v1.1.5
556
557
  [1.1.4]: https://github.com/avonderluft/bible270/releases/tag/v1.1.4
557
558
  [1.1.3]: https://github.com/avonderluft/bible270/releases/tag/v1.1.3
data/README.md CHANGED
@@ -6,6 +6,8 @@ A mountable **Rails engine** that drops a 270-day (9 month) interactive Bible re
6
6
 
7
7
  Initially created for use by students and faculty of [Kingdom Movement School of Ministry](https://www.kmsm.life/), to read through all of Scripture together during the school year. Thus it is potentially useful for any Bible School, Seminary, or Discipleship school working on a 9 month schedule.
8
8
 
9
+ For a guided tour of the implementation, see the [architecture walkthrough](docs/architecture.md).
10
+
9
11
  ## The plan
10
12
 
11
13
  Three readings a day, sized by **verse count** so each day takes roughly the same time.
@@ -26,6 +26,8 @@ module Bible270
26
26
 
27
27
  layout :bible270_layout
28
28
 
29
+ rescue_from ActionController::InvalidAuthenticityToken, with: :recover_from_stale_page
30
+
29
31
  helper_method :current_reader, :signed_in?, :b270_config, :enrollment_closed?
30
32
 
31
33
  private
@@ -101,6 +103,17 @@ module Bible270
101
103
  current_reader.present?
102
104
  end
103
105
 
106
+ # A suspended mobile tab can outlive its Rails session cookie. The page still
107
+ # contains the old session's CSRF token, so Rails rejects its next form before
108
+ # current_reader has a chance to restore the session from the remember cookie.
109
+ # Never execute that unverified request; return to a fresh page for a safe retry.
110
+ def recover_from_stale_page(error)
111
+ Rails.logger.info("[bible270] refreshing a page after #{error.class}")
112
+ redirect_back fallback_location: root_path, allow_other_host: false,
113
+ alert: 'This page had been open for a while, so it was refreshed. Please try again.',
114
+ status: :see_other
115
+ end
116
+
104
117
  # Guard participation (checking off / commenting). Viewing is always open.
105
118
  def require_reader!
106
119
  return true if signed_in?
@@ -17,6 +17,16 @@ module Bible270
17
17
  @origin = safe_origin(params[:origin])
18
18
  end
19
19
 
20
+ # Refresh a resumed page's session and CSRF token without reloading its content.
21
+ # Calling current_reader first lets the long-lived remember cookie rebuild a
22
+ # browser session that the phone may have discarded while the tab was asleep.
23
+ def refresh
24
+ current_reader
25
+ response.headers['X-CSRF-Token'] = form_authenticity_token
26
+ response.headers['Cache-Control'] = 'no-store'
27
+ head :no_content
28
+ end
29
+
20
30
  def create
21
31
  auth = request.env['omniauth.auth']
22
32
  redirect_to(sign_in_path, alert: "Sign in didn't complete. Please try again.") and return unless auth
@@ -1,5 +1,6 @@
1
- <script nonce="<%= content_security_policy_nonce %>">
1
+ <script nonce="<%= content_security_policy_nonce %>" data-refresh-path="<%= refresh_session_path %>">
2
2
  (() => {
3
+ const refreshSessionPath = document.currentScript.dataset.refreshPath;
3
4
  if (window.Bible270InteractionUI) {
4
5
  window.Bible270InteractionUI.reset();
5
6
  return;
@@ -30,6 +31,42 @@
30
31
  document.querySelectorAll("form[data-b270-submit='true']").forEach(resetForm);
31
32
  }
32
33
 
34
+ const SESSION_REFRESH_AFTER = 5 * 60 * 1000;
35
+ let sessionRefreshedAt = Date.now();
36
+ let sessionRefresh = null;
37
+ let hiddenAt = null;
38
+
39
+ function updateAuthenticityToken(token) {
40
+ const meta = document.querySelector("meta[name='csrf-token']");
41
+ if (meta) meta.content = token;
42
+ document.querySelectorAll("input[name='authenticity_token']").forEach((input) => {
43
+ input.value = token;
44
+ });
45
+ }
46
+
47
+ function refreshSession() {
48
+ if (sessionRefresh) return sessionRefresh;
49
+
50
+ sessionRefresh = fetch(refreshSessionPath, {
51
+ credentials: "same-origin",
52
+ cache: "no-store",
53
+ headers: { Accept: "text/plain" }
54
+ }).then((response) => {
55
+ const token = response.headers.get("X-CSRF-Token");
56
+ if (!response.ok || !token) throw new Error("Could not refresh the session");
57
+
58
+ updateAuthenticityToken(token);
59
+ sessionRefreshedAt = Date.now();
60
+ }).finally(() => {
61
+ sessionRefresh = null;
62
+ });
63
+ return sessionRefresh;
64
+ }
65
+
66
+ function sessionRefreshNeeded() {
67
+ return Date.now() - sessionRefreshedAt >= SESSION_REFRESH_AFTER;
68
+ }
69
+
33
70
  let scriptureWindow = null;
34
71
  const replacesBackgroundTabs = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
35
72
  (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
@@ -62,6 +99,24 @@
62
99
  document.addEventListener("submit", (event) => {
63
100
  const form = event.target.closest("form[data-b270-submit='true']");
64
101
  if (!form) return;
102
+
103
+ if (sessionRefresh || sessionRefreshNeeded()) {
104
+ event.preventDefault();
105
+ if (form.dataset.b270AwaitingSession === "true") return;
106
+
107
+ form.dataset.b270AwaitingSession = "true";
108
+ const submitter = event.submitter;
109
+ refreshSession().then(() => {
110
+ delete form.dataset.b270AwaitingSession;
111
+ form.requestSubmit(submitter || undefined);
112
+ }).catch(() => {
113
+ delete form.dataset.b270AwaitingSession;
114
+ sessionRefreshedAt = Date.now();
115
+ form.requestSubmit(submitter || undefined);
116
+ });
117
+ return;
118
+ }
119
+
65
120
  if (form.dataset.b270Submitting === "true") {
66
121
  event.preventDefault();
67
122
  return;
@@ -89,7 +144,18 @@
89
144
  }
90
145
  });
91
146
 
92
- window.addEventListener("pageshow", reset);
147
+ window.addEventListener("pageshow", (event) => {
148
+ reset();
149
+ if (event.persisted) refreshSession().catch(() => {});
150
+ });
151
+ document.addEventListener("visibilitychange", () => {
152
+ if (document.visibilityState === "hidden") {
153
+ hiddenAt = Date.now();
154
+ } else if (hiddenAt && Date.now() - hiddenAt >= SESSION_REFRESH_AFTER) {
155
+ refreshSession().catch(() => {});
156
+ hiddenAt = null;
157
+ }
158
+ });
93
159
  document.addEventListener("turbo:load", reset);
94
160
 
95
161
  window.Bible270InteractionUI = { reset };
data/config/routes.rb CHANGED
@@ -50,6 +50,7 @@ Bible270::Engine.routes.draw do
50
50
  # Built-in OmniAuth sign-in. The request phase (POST <mount>/auth/:provider)
51
51
  # is handled by the OmniAuth middleware in the host app, not by these routes.
52
52
  get 'sign_in', to: 'sessions#new', as: :sign_in
53
+ get 'session/refresh', to: 'sessions#refresh', as: :refresh_session
53
54
 
54
55
  # Email (magic link) sign-in, for readers without a social account.
55
56
  post 'sign_in/email', to: 'sessions#email_link', as: :email_sign_in_link
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.1.6'
4
+ VERSION = '1.1.7'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft