studio-engine 0.18.0 → 0.19.0
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 +4 -4
- data/CHANGELOG.md +34 -0
- data/app/controllers/concerns/studio/magic_link_issuing.rb +39 -0
- data/app/controllers/magic_links_controller.rb +5 -14
- data/app/controllers/studio/local_emails_controller.rb +1 -1
- data/app/controllers/studio/local_reviews_controller.rb +50 -0
- data/app/mailers/user_mailer.rb +6 -9
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +16 -0
- metadata +5 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f49b9ef9699144b785efbe7d4839261bba26035970a6c411f5cd54d0606ea97c
|
|
4
|
+
data.tar.gz: 3705abb5871fcfaa48b7c55613b0bc430b275c8bf9eb4c1625e43fcf14cfed32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 584cbbcdeeb388b1a6b0115a790fb6fa3d1c4ce4c69f42e57f7f0985edb4f6338578db04dcfeb5c60bde65aada69509db87c07151a52aab9f9348739a332cdca
|
|
7
|
+
data.tar.gz: dbbe745f30a632c21d528e336dd323a21224e71d39f97caf6fc43f68f958d1cb78da1a8660d735393847bdfeb67a79d15c1f12c199446f665c0267ff521c19fd
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
|
|
4
4
|
|
|
5
|
+
## 0.19.0 — 2026-07-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`GET /_studio/local_review` — the local half of the board's WAITING APPROVAL
|
|
10
|
+
button.** A magic link signs the recipient into the app that MINTED it: the
|
|
11
|
+
token lives in that app's store, and `return_to` is sanitized to a same-origin
|
|
12
|
+
PATH. So a link minted by the production task board could only ever land on
|
|
13
|
+
production — the operator clicked "review this locally" and arrived, signed in,
|
|
14
|
+
on `mcritchie.studio`. The board now redirects here instead, to the local
|
|
15
|
+
server named by the task's `local_url`, which mints in its OWN database and
|
|
16
|
+
lands the operator signed-in on the page under review
|
|
17
|
+
(`?email=<who>&return_to=<path>`).
|
|
18
|
+
|
|
19
|
+
Like the local email inbox it sits beside, this is a developer-desk tool that
|
|
20
|
+
hands out sign-in material without authenticating anyone, so it is bound to the
|
|
21
|
+
same floor: the route is drawn outside production only, and every request is
|
|
22
|
+
re-checked against `Studio.local_tool_enabled?` — production or non-loopback
|
|
23
|
+
gets a bare 404.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`Studio.local_tool_enabled?(request_local:)`** — one spelling of the
|
|
28
|
+
developer-desk floor (not production, loopback only), now shared by
|
|
29
|
+
`Studio::LocalEmailsController` and `Studio::LocalReviewsController` so a tool
|
|
30
|
+
added later cannot quietly ship a weaker gate.
|
|
31
|
+
|
|
32
|
+
- **`Studio::MagicLinkIssuing`** — the mint (`issue_magic_link`) and the URL that
|
|
33
|
+
consumes it (`magic_link_url_for`) are two halves of one decision
|
|
34
|
+
(`Studio.magic_link_store`), extracted from `MagicLinksController` and
|
|
35
|
+
`UserMailer` into a shared concern. Handing out the wrong URL for the store
|
|
36
|
+
yields "invalid or expired" on a link that was valid; they can no longer drift
|
|
37
|
+
apart. No behavior change for either existing caller.
|
|
38
|
+
|
|
5
39
|
## 0.18.0 — 2026-07-27
|
|
6
40
|
|
|
7
41
|
### Added
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# Minting a magic link, and building the URL that consumes it — the two halves
|
|
5
|
+
# of one decision (Studio.magic_link_store), kept in one place so they can
|
|
6
|
+
# never drift apart. A token minted in the :database store is only consumable
|
|
7
|
+
# at /l/<token>; a :signed token only at /magic_link/<token>. Handing out the
|
|
8
|
+
# wrong URL for the store yields an "invalid or expired link" on a link that
|
|
9
|
+
# was valid — the failure mode this concern exists to prevent.
|
|
10
|
+
#
|
|
11
|
+
# Included by every issuer: MagicLinksController (the request-a-link flow),
|
|
12
|
+
# UserMailer (the emailed link), and Studio::LocalReviewsController (the
|
|
13
|
+
# dev-only local-review link). An app that overrides MagicLinksController
|
|
14
|
+
# wholesale brings its own issuing and is unaffected.
|
|
15
|
+
module MagicLinkIssuing
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Mint a token in the configured store. Default :signed keeps the legacy
|
|
21
|
+
# stateless MessageVerifier link; :database mints a Studio::Link row (the
|
|
22
|
+
# short, unified scheme). `return_to` is sanitized by both stores.
|
|
23
|
+
def issue_magic_link(email, return_to)
|
|
24
|
+
if Studio.magic_link_store == :database
|
|
25
|
+
Studio::Link.create_magic_link(email: email, return_to: return_to).token
|
|
26
|
+
else
|
|
27
|
+
MagicLink.generate(email: email, return_to: return_to)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The URL that CONSUMES the token this app mints: the short /l/<token> for
|
|
32
|
+
# the :database scheme, the legacy /magic_link/<token> for :signed (and for
|
|
33
|
+
# a :database app that keeps its own /magic_link route, e.g. turf-monster,
|
|
34
|
+
# whose /l is already its landing-page namespace).
|
|
35
|
+
def magic_link_url_for(token)
|
|
36
|
+
Studio.magic_link_via_l_route? ? link_url(token: token) : magic_link_url(token: token)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# sign_in_existing / sign_up_new building blocks.
|
|
16
16
|
class MagicLinksController < ApplicationController
|
|
17
17
|
include Studio::LinkConsumption
|
|
18
|
+
include Studio::MagicLinkIssuing
|
|
18
19
|
|
|
19
20
|
skip_before_action :require_authentication
|
|
20
21
|
layout false, only: :confirm
|
|
@@ -57,18 +58,8 @@ class MagicLinksController < ApplicationController
|
|
|
57
58
|
redirect_to login_path, alert: "That sign-in link is invalid or has expired. Request a fresh one below."
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# Studio::Link row (URL: /l/<token>) — the short, unified scheme. The mailer
|
|
65
|
-
# builds the matching URL (see UserMailer#magic_link). sign_in_existing /
|
|
66
|
-
# sign_up_new / safe_path come from Studio::LinkConsumption.
|
|
67
|
-
def issue_magic_link(email, return_to)
|
|
68
|
-
if Studio.magic_link_store == :database
|
|
69
|
-
Studio::Link.create_magic_link(email: email, return_to: return_to).token
|
|
70
|
-
else
|
|
71
|
-
MagicLink.generate(email: email, return_to: return_to)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
61
|
+
# issue_magic_link (mint in the configured store) comes from
|
|
62
|
+
# Studio::MagicLinkIssuing, shared with UserMailer — which builds the URL that
|
|
63
|
+
# consumes it — so the mint and its landing URL cannot drift apart.
|
|
64
|
+
# sign_in_existing / sign_up_new / safe_path come from Studio::LinkConsumption.
|
|
74
65
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# GET /_studio/local_review?email=<who>&return_to=<path> — the LOCAL half of
|
|
5
|
+
# the task board's WAITING APPROVAL button.
|
|
6
|
+
#
|
|
7
|
+
# Why it must live here, on the local app. A magic link signs the recipient
|
|
8
|
+
# into the app that MINTED it: the token is a row (or a signed blob) in that
|
|
9
|
+
# app's own store, and Studio::LinkToken.sanitize_path keeps `return_to` a
|
|
10
|
+
# same-origin PATH. So a link minted by the production board can only ever
|
|
11
|
+
# land on production — no matter what host the path belongs to. The board
|
|
12
|
+
# therefore stops minting and REDIRECTS here, to the local server named by the
|
|
13
|
+
# task's local_url, which mints in its own database and lands the operator
|
|
14
|
+
# signed-in on the page under review.
|
|
15
|
+
#
|
|
16
|
+
# It mints for whatever email it is handed, unauthenticated — so it is bound
|
|
17
|
+
# to the same floor as the local email inbox (Studio::LocalEmailsController):
|
|
18
|
+
# never in production, and only for a loopback request. That is the same
|
|
19
|
+
# exposure the inbox already carries, which hands every captured sign-in link
|
|
20
|
+
# to any local reader. It is a development desk convenience; it is not a
|
|
21
|
+
# sign-in path.
|
|
22
|
+
class LocalReviewsController < ApplicationController
|
|
23
|
+
include Studio::MagicLinkIssuing
|
|
24
|
+
|
|
25
|
+
skip_before_action :require_authentication, raise: false
|
|
26
|
+
|
|
27
|
+
before_action :require_local_development!
|
|
28
|
+
|
|
29
|
+
def show
|
|
30
|
+
email = Studio::LinkToken.normalize_email(params[:email])
|
|
31
|
+
return redirect_to(login_path, alert: MISSING_EMAIL) unless email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
32
|
+
|
|
33
|
+
# return_to is passed through raw: BOTH stores sanitize it to a same-origin
|
|
34
|
+
# path on the way in (Studio::Link.create_magic_link and MagicLink.generate
|
|
35
|
+
# each call Studio::LinkToken.sanitize_path). Re-sanitizing here would be a
|
|
36
|
+
# second spelling of a rule that already has one owner — and a mutation run
|
|
37
|
+
# confirmed it guards nothing the store does not already guard.
|
|
38
|
+
token = issue_magic_link(email, params[:return_to])
|
|
39
|
+
redirect_to magic_link_url_for(token), allow_other_host: false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
MISSING_EMAIL = "Add ?email=<your address> to mint a local review link."
|
|
45
|
+
|
|
46
|
+
def require_local_development!
|
|
47
|
+
head :not_found unless Studio.local_tool_enabled?(request_local: request.local?)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/app/mailers/user_mailer.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
class UserMailer < ApplicationMailer
|
|
2
|
+
# magic_link_url_for — the URL that consumes the token, matched to
|
|
3
|
+
# Studio.magic_link_store. Shared with the issuers that MINT the token
|
|
4
|
+
# (MagicLinksController, Studio::LocalReviewsController) so the two halves
|
|
5
|
+
# cannot drift apart.
|
|
6
|
+
include Studio::MagicLinkIssuing
|
|
7
|
+
|
|
2
8
|
# Branded shell (banner + card) for engine-sent UserMailer emails. An app with
|
|
3
9
|
# its own UserMailer + branded_mailer layout (e.g. turf-monster) overrides both.
|
|
4
10
|
layout "branded_mailer"
|
|
@@ -18,13 +24,4 @@ class UserMailer < ApplicationMailer
|
|
|
18
24
|
@banner_alt = "Your #{@app_name} sign-in link"
|
|
19
25
|
mail(to: email, subject: "Your #{@app_name} sign-in link")
|
|
20
26
|
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
# Match the emailed URL to Studio.magic_link_store: the short /l/<token> for
|
|
25
|
-
# the :database scheme, the legacy /magic_link/<token> for :signed. The
|
|
26
|
-
# request side (MagicLinksController#issue_magic_link) mints the matching token.
|
|
27
|
-
def magic_link_url_for(token)
|
|
28
|
-
Studio.magic_link_via_l_route? ? link_url(token: token) : magic_link_url(token: token)
|
|
29
|
-
end
|
|
30
27
|
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -205,6 +205,18 @@ module Studio
|
|
|
205
205
|
magic_link_store == :database && draw_link_routes
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
# The floor every developer-desk tool sits on: the local email inbox
|
|
209
|
+
# (Studio::LocalEmailsController) and the local-review mint
|
|
210
|
+
# (Studio::LocalReviewsController). Both hand out sign-in material without
|
|
211
|
+
# authenticating anyone, so both are OFF in production and OFF for any request
|
|
212
|
+
# that did not come from the loopback interface. One spelling, so a tool added
|
|
213
|
+
# later cannot quietly ship a weaker gate. Pass request.local? in.
|
|
214
|
+
def self.local_tool_enabled?(request_local:)
|
|
215
|
+
return false if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
|
|
216
|
+
|
|
217
|
+
!!request_local
|
|
218
|
+
end
|
|
219
|
+
|
|
208
220
|
def self.local_email_capture?
|
|
209
221
|
return false if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
|
|
210
222
|
return !!local_email_capture unless local_email_capture.nil?
|
|
@@ -299,8 +311,12 @@ module Studio
|
|
|
299
311
|
get "auth/:provider/callback", to: "omniauth_callbacks#create"
|
|
300
312
|
get "auth/failure", to: "omniauth_callbacks#failure"
|
|
301
313
|
|
|
314
|
+
# Developer-desk tools. Drawn outside production, and each controller
|
|
315
|
+
# re-checks Studio.local_tool_enabled? per request (loopback only) — the
|
|
316
|
+
# route being absent is the outer gate, not the only one.
|
|
302
317
|
unless defined?(Rails) && Rails.env.production?
|
|
303
318
|
get "_studio/local_emails", to: "studio/local_emails#index", as: :studio_local_emails
|
|
319
|
+
get "_studio/local_review", to: "studio/local_reviews#show", as: :studio_local_review
|
|
304
320
|
end
|
|
305
321
|
|
|
306
322
|
# Passwordless email (magic link). Helpers: magic_link_request_path (POST
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -179,6 +178,7 @@ files:
|
|
|
179
178
|
- app/controllers/concerns/studio/error_handling.rb
|
|
180
179
|
- app/controllers/concerns/studio/impersonation.rb
|
|
181
180
|
- app/controllers/concerns/studio/link_consumption.rb
|
|
181
|
+
- app/controllers/concerns/studio/magic_link_issuing.rb
|
|
182
182
|
- app/controllers/error_logs_controller.rb
|
|
183
183
|
- app/controllers/magic_links_controller.rb
|
|
184
184
|
- app/controllers/navbar_controller.rb
|
|
@@ -190,6 +190,7 @@ files:
|
|
|
190
190
|
- app/controllers/studio/email_images_controller.rb
|
|
191
191
|
- app/controllers/studio/links_controller.rb
|
|
192
192
|
- app/controllers/studio/local_emails_controller.rb
|
|
193
|
+
- app/controllers/studio/local_reviews_controller.rb
|
|
193
194
|
- app/controllers/studio/models_controller.rb
|
|
194
195
|
- app/controllers/style_controller.rb
|
|
195
196
|
- app/controllers/theme_settings_controller.rb
|
|
@@ -325,7 +326,6 @@ metadata:
|
|
|
325
326
|
source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
|
|
326
327
|
bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
|
|
327
328
|
changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
|
|
328
|
-
post_install_message:
|
|
329
329
|
rdoc_options: []
|
|
330
330
|
require_paths:
|
|
331
331
|
- lib
|
|
@@ -340,8 +340,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
340
340
|
- !ruby/object:Gem::Version
|
|
341
341
|
version: '0'
|
|
342
342
|
requirements: []
|
|
343
|
-
rubygems_version:
|
|
344
|
-
signing_key:
|
|
343
|
+
rubygems_version: 4.0.9
|
|
345
344
|
specification_version: 4
|
|
346
345
|
summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
|
|
347
346
|
image caching
|