studio-engine 0.33.0 → 0.36.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 +69 -0
- data/app/controllers/studio/local_reviews_controller.rb +85 -1
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +30 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c692fc135af1ba83c08b27398c875bc547ae07bbcc492540ffd83250fccf680
|
|
4
|
+
data.tar.gz: 891c561fe2bef1386137af4fbbc22547f38613efe8bd373bf6ec31894086feef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9cb8bb03d5adeec99336b5fab4d28b29c57be8686ca2afef58932ce866c321fe44f6ab0123609e6845151f938ab28790685bda6b9b21815ef3a3594d6c589921
|
|
7
|
+
data.tar.gz: 2c3be4aadf2a88d7838dab26b27c4a22604268138bba818f0b82890774f7bc5b81424d53a645d3fcc5ebf59a8f10197a9b37bd9cd44246c988d8dca3dfd2cd62
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
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.36.0 — 2026-08-10
|
|
6
|
+
|
|
7
|
+
**The local-review link now signs the reviewer in as someone who can SEE the
|
|
8
|
+
page.** `/_studio/local_review` — the local half of the task board's WAITING
|
|
9
|
+
APPROVAL button — provisions the account before it mints, at the new
|
|
10
|
+
`Studio.local_review_role` (default `"admin"`).
|
|
11
|
+
|
|
12
|
+
The board hands this endpoint the operator's **production** email address. A
|
|
13
|
+
fresh worktree database has never seen it, so consuming the link took
|
|
14
|
+
`Studio::LinkConsumption#sign_up_new` and created the account at the default
|
|
15
|
+
role, `viewer`. `require_admin` on the page under review then redirected it to
|
|
16
|
+
`/`. The sign-in **succeeded** every time, which is what kept this quiet: the
|
|
17
|
+
button worked, the token was valid, and the operator simply arrived on the home
|
|
18
|
+
page having never seen the thing he was asked to review. A seeded admin
|
|
19
|
+
(`alex@mcritchie.studio`) worked fine, so only his real address ever hit it.
|
|
20
|
+
|
|
21
|
+
The endpoint now find-or-creates the reviewer and ensures the role BEFORE
|
|
22
|
+
minting, so the consume takes `sign_in_existing` onto an account that already
|
|
23
|
+
has rights. An existing account is promoted, never duplicated; an already-correct
|
|
24
|
+
one is not rewritten.
|
|
25
|
+
|
|
26
|
+
New public config:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
Studio.configure do |config|
|
|
30
|
+
config.local_review_role = "admin" # default; nil provisions without a role
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Set it to `nil` for an app whose review pages are not admin-gated — the account
|
|
35
|
+
is still provisioned (that is what avoids `sign_up_new`), but its role is left
|
|
36
|
+
to the host's own `configure_new_user`.
|
|
37
|
+
|
|
38
|
+
**The endpoint also answers "who is sitting at this desk?" when the caller names
|
|
39
|
+
nobody.** `?email=` is now optional, and an explicit one still wins. With none,
|
|
40
|
+
the reviewer resolves to `Studio.local_review_email`, and failing that to the
|
|
41
|
+
**first user already holding `Studio.local_review_role`, by id** (that query
|
|
42
|
+
falls back to `"admin"` when the setting is `nil`):
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
config.local_review_email = "someone@example.com" # nil (default) = derive
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This exists so the board's WAITING APPROVAL CTA can be a **public, sign-in-free
|
|
49
|
+
redirect**. Requiring a board session to click it is what broke one-click review
|
|
50
|
+
in the first place; sending an email in that public URL would publish the
|
|
51
|
+
operator's address. The local stack is the machine the reviewer is sitting at,
|
|
52
|
+
so it is the right place to decide. A desk with nobody at that role mints
|
|
53
|
+
nothing and says so, rather than guessing: the derive only ever picks someone
|
|
54
|
+
who ALREADY holds the role, so it never promotes a stranger into it. (The
|
|
55
|
+
`?email=` path does promote the account it is handed — that is the point of it.)
|
|
56
|
+
|
|
57
|
+
**The floor is unchanged, and now asserted rather than assumed.** The endpoint
|
|
58
|
+
grants a role, so both gates in front of it are tested directly: the
|
|
59
|
+
developer-desk routes are drawn only outside production (proved by drawing
|
|
60
|
+
`Studio.routes` into a throwaway route set under a production env), and a
|
|
61
|
+
non-loopback request 404s **before** provisioning or minting anything.
|
|
62
|
+
Provisioning is best-effort: a host whose `User` rejects the write gets a logged
|
|
63
|
+
warning and a working link, never a 500.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **`Studio.local_review_role`** — the role `/_studio/local_review` stamps on the
|
|
68
|
+
account it provisions before minting. Defaults to `"admin"`; `nil` provisions
|
|
69
|
+
the account without touching its role.
|
|
70
|
+
- **`Studio.local_review_email`** — who the local-review mint signs in when the
|
|
71
|
+
caller sends no `?email=`. `nil` (the default) derives the first admin in this
|
|
72
|
+
database, by id.
|
|
73
|
+
|
|
5
74
|
## 0.33.0 — 2026-08-10
|
|
6
75
|
|
|
7
76
|
Two changes ship together in this release: local log files are now capped, and
|
|
@@ -19,6 +19,15 @@ module Studio
|
|
|
19
19
|
# exposure the inbox already carries, which hands every captured sign-in link
|
|
20
20
|
# to any local reader. It is a development desk convenience; it is not a
|
|
21
21
|
# sign-in path.
|
|
22
|
+
#
|
|
23
|
+
# It also PROVISIONS the account before minting, at Studio.local_review_role
|
|
24
|
+
# (default "admin"). The email the board hands over is the operator's
|
|
25
|
+
# PRODUCTION address, and a fresh worktree database has never seen it — so
|
|
26
|
+
# without this the consume takes Studio::LinkConsumption#sign_up_new, creates
|
|
27
|
+
# him at the default role ("viewer"), and `require_admin` on the page under
|
|
28
|
+
# review redirects him to "/". The sign-in SUCCEEDS and he never sees the
|
|
29
|
+
# page, which is what made the failure so quiet: a seeded admin works, so only
|
|
30
|
+
# the operator's real address ever hit it.
|
|
22
31
|
class LocalReviewsController < ApplicationController
|
|
23
32
|
include Studio::MagicLinkIssuing
|
|
24
33
|
|
|
@@ -27,9 +36,13 @@ module Studio
|
|
|
27
36
|
before_action :require_local_development!
|
|
28
37
|
|
|
29
38
|
def show
|
|
30
|
-
email =
|
|
39
|
+
email = reviewer_email
|
|
31
40
|
return redirect_to(login_path, alert: MISSING_EMAIL) unless email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
32
41
|
|
|
42
|
+
# Provision BEFORE minting, so the consume finds an existing account and
|
|
43
|
+
# takes sign_in_existing rather than sign_up_new-at-the-default-role.
|
|
44
|
+
provision_reviewer(email)
|
|
45
|
+
|
|
33
46
|
# return_to is passed through raw: the store sanitizes it to a same-origin
|
|
34
47
|
# path on the way in (Studio::Link.create_magic_link calls
|
|
35
48
|
# Studio::LinkToken.sanitize_path). Re-sanitizing here would be a second
|
|
@@ -43,6 +56,77 @@ module Studio
|
|
|
43
56
|
|
|
44
57
|
MISSING_EMAIL = "Add ?email=<your address> to mint a local review link."
|
|
45
58
|
|
|
59
|
+
# WHO to sign in, in priority order:
|
|
60
|
+
#
|
|
61
|
+
# 1. `?email=` — an explicit caller still wins.
|
|
62
|
+
# 2. `Studio.local_review_email` — the app's declared desk operator.
|
|
63
|
+
# 3. the first admin in this database — the seeded operator, by id.
|
|
64
|
+
#
|
|
65
|
+
# The board deliberately stops at (1) being ABSENT. Its WAITING APPROVAL CTA
|
|
66
|
+
# is a public, sign-in-free redirect, so an email in that URL would be an
|
|
67
|
+
# address published on a public page for anyone to read. The local stack is
|
|
68
|
+
# the right place to answer "who is at this desk": it is the machine the
|
|
69
|
+
# reviewer is sitting at, and it already knows its own operator.
|
|
70
|
+
def reviewer_email
|
|
71
|
+
Studio::LinkToken.normalize_email(
|
|
72
|
+
params[:email].presence || Studio.local_review_email.presence || seeded_admin_email
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The database's own first admin. Ordered by id so a re-seeded desk resolves
|
|
77
|
+
# to the same person every time rather than to whoever was touched last.
|
|
78
|
+
# Rescued because this runs before the mint on every click: a host with no
|
|
79
|
+
# role column must fall through to MISSING_EMAIL, not a 500.
|
|
80
|
+
def seeded_admin_email
|
|
81
|
+
return nil unless User.respond_to?(:column_names) && User.column_names.include?("role")
|
|
82
|
+
|
|
83
|
+
User.where(role: Studio.local_review_role.presence || "admin").order(:id).first&.email
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
Rails.logger.warn("[Studio::LocalReviewsController] no default reviewer: #{e.class}: #{e.message}")
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Find-or-create the reviewer and ensure Studio.local_review_role, so the
|
|
90
|
+
# page under review actually RENDERS for whoever follows the link.
|
|
91
|
+
#
|
|
92
|
+
# Best-effort by design. Provisioning is an upgrade to the mint, not a
|
|
93
|
+
# precondition of it: a host with an extra User validation must not turn the
|
|
94
|
+
# review button into a 500 — it should still hand back a working sign-in
|
|
95
|
+
# link, exactly as it did before this method existed. So a failure is logged
|
|
96
|
+
# loudly and the mint proceeds. The end-to-end check that the operator
|
|
97
|
+
# actually lands ON the page is the real gate; this is the thing that makes
|
|
98
|
+
# it pass, not the thing that proves it.
|
|
99
|
+
# `User` is referenced bare, with no defined?/const_defined? guard: those do
|
|
100
|
+
# not agree with each other about a Zeitwerk autoload that has not fired
|
|
101
|
+
# yet, and a guard that reads "absent" on a cold boot would skip
|
|
102
|
+
# provisioning silently — the failure mode being fixed. Studio::LinkConsumption
|
|
103
|
+
# already requires every consuming app to have User, so a genuinely missing
|
|
104
|
+
# one is a NameError the rescue below logs like any other host mismatch.
|
|
105
|
+
def provision_reviewer(email)
|
|
106
|
+
user = User.find_by(email: email) || build_reviewer(email)
|
|
107
|
+
role = Studio.local_review_role.presence
|
|
108
|
+
user.role = role if role && user.respond_to?(:role=) && user.role != role
|
|
109
|
+
user.save! if user.new_record? || user.changed?
|
|
110
|
+
user
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
# No re-raise: see "best-effort" above. Logged at warn because a silent
|
|
113
|
+
# miss here reappears as the exact symptom this endpoint exists to remove
|
|
114
|
+
# — a successful sign-in that lands on the wrong page.
|
|
115
|
+
Rails.logger.warn(
|
|
116
|
+
"[Studio::LocalReviewsController] could not provision #{email}: #{e.class}: #{e.message}"
|
|
117
|
+
)
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A brand-new reviewer goes through the host's own new-user hook first, so
|
|
122
|
+
# the account this endpoint creates is shaped like every other account in
|
|
123
|
+
# the app (usernames, defaults, associations) — then the role is stamped on
|
|
124
|
+
# top. Mirrors Studio::LinkConsumption#sign_up_new, which is the path this
|
|
125
|
+
# provisioning is standing in for.
|
|
126
|
+
def build_reviewer(email)
|
|
127
|
+
User.new(email: email).tap { |user| Studio.configure_new_user.call(user) }
|
|
128
|
+
end
|
|
129
|
+
|
|
46
130
|
def require_local_development!
|
|
47
131
|
head :not_found unless Studio.local_tool_enabled?(request_local: request.local?)
|
|
48
132
|
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -158,6 +158,36 @@ module Studio
|
|
|
158
158
|
# is truthy, otherwise disabled. Production always disables capture.
|
|
159
159
|
mattr_accessor :local_email_capture, default: nil
|
|
160
160
|
|
|
161
|
+
# The role Studio::LocalReviewsController stamps on the account it provisions
|
|
162
|
+
# for a local review (the board's WAITING APPROVAL button).
|
|
163
|
+
#
|
|
164
|
+
# It defaults to "admin" because the pages sent for review are overwhelmingly
|
|
165
|
+
# admin-gated, and the operator's address is his PRODUCTION one — an address a
|
|
166
|
+
# fresh worktree database has never seen, so the sign-in would otherwise CREATE
|
|
167
|
+
# him at the default role and `require_admin` would bounce him to "/". The
|
|
168
|
+
# sign-in succeeds and he never sees the page: the bug this knob exists to end.
|
|
169
|
+
#
|
|
170
|
+
# Set it to nil (or "") to provision the account WITHOUT touching its role —
|
|
171
|
+
# for an app whose review pages are not admin-gated, or whose role column
|
|
172
|
+
# means something else. Only ever reached behind local_tool_enabled?
|
|
173
|
+
# (non-production AND loopback), so it grants nothing a local reader could not
|
|
174
|
+
# already take from the local email inbox beside it.
|
|
175
|
+
mattr_accessor :local_review_role, default: "admin"
|
|
176
|
+
|
|
177
|
+
# WHO the local-review mint signs in when the caller names no `?email=`.
|
|
178
|
+
#
|
|
179
|
+
# The board's WAITING APPROVAL CTA is a public, sign-in-free redirect, so it
|
|
180
|
+
# sends no email — putting one in that URL would publish the operator's
|
|
181
|
+
# address on a public page. The local stack answers the question instead: it
|
|
182
|
+
# is the machine the reviewer is sitting at.
|
|
183
|
+
#
|
|
184
|
+
# nil (the default) means "derive": the first user in this database already
|
|
185
|
+
# holding local_review_role, by id — falling back to "admin" when that setting
|
|
186
|
+
# is itself nil. So a desk that switches local_review_role OFF has no role to
|
|
187
|
+
# derive FROM and should name its operator here explicitly, as should a desk
|
|
188
|
+
# whose operator is not the first seeded account at that role.
|
|
189
|
+
mattr_accessor :local_review_email, default: nil
|
|
190
|
+
|
|
161
191
|
# Theme role colors (7 roles)
|
|
162
192
|
mattr_accessor :theme_primary, default: "#8E82FE"
|
|
163
193
|
mattr_accessor :theme_dark, default: "#1A1535"
|