| Age | Commit message (Collapse) | Author |
|
|
|
|
|
|
|
Any holder may add or remove another account, witnessed as
redaktion_grant/revoke so the vouching is legible. Not behind elevation:
onboarding must not wait for a keyholder, and a compromised Redaktion
account can already publish.
|
|
Administrative actions are gated behind a 30-minute elevation window:
creating and retiring accounts, editing roles, clearing a second factor.
Reading the list is not gated, and content work is untouched.
elevated? is tied to is_admin?, so losing the role closes the window at
once. The window opens when the second factor verifies at login, so an
admin heading straight for user management is already elevated, and closes
on logout with the other session state. Five wrong codes end the session,
mirroring the login challenge.
users#update carries no elevation filter, since self-service reaches it;
the role field is gated in user_params instead and fails closed.
|
|
RoleRequired supplies require_redaktion and require_admin for surfaces that
are not nodes and so cannot be reached by Node#restricted?.
Navigation is content rather than plumbing, so menu_items requires
redaktion. User management is janitorial and requires admin: index, new,
create, reset_otp, deactivate, reactivate. verify_status now also covers
show, without which any logged-in user could read any account by walking a
small id space. Editing your own account stays open.
The dashboard hides the Users and Navigation buttons from those who cannot
use them; everything else stays visible to everyone. Both denials share one
message and land on the dashboard.
Adds redella (redaktion) and alufa (redaktion + alumni) fixtures.
|
|
nodes#new marks the kinds that place under /updates, derived from each kind's
own path_prefix. The three parent pickers mark candidates the current user
cannot publish into; menu_search returns needs_redaktion per result, computed
per user rather than as a bare restricted flag, and the menu-item picker opts
out.
restore_from_trash! checks its destination: the restore is the move, applied
rather than staged, so no later publish would have caught it. The node
returns as a draft either way.
|
|
nodes#show renders publish and trash as disabled_action spans with a hint
naming what an editor can still do, matching how locked_by_other already
reads. Editing and reverting stay live: drafting is free everywhere.
|
|
CccConventions::TAG_SCOPES maps "update" and "pressemitteilung" to /updates
and "disclosure" to /disclosure. Page.aggregate applies the mapping, so an
aggregate over one of those tags is bounded by its subtree regardless of what
the shortcode says. Tags stay unrestricted; positions are publish-gated.
Both RSS actions call Page.aggregate rather than repeating its tag join.
Empty feeds no longer crash: Atom falls back to the current time for the
required <updated>, and the optional dc:date is omitted.
|
|
publish_draft!, trash!, destroy_from_trash!, attach_asset! and
Asset#destroy_witnessed! now refuse unless the acting user holds redaktion,
and only when the subject is on a restricted surface: the front page, the
updates tree that feeds ~100k subscribers, or disclosure. Drafting,
autosaving, tagging and creating stay free everywhere for everyone.
Enforcement is in the models rather than the controllers, since attach_asset!
and the rest are reachable from rake tasks and internal paths. It follows the
errors.add-plus-bare-raise pattern the rest of Node already uses, so every
existing RecordInvalid rescue reports it with a localised message; only
assets_controller#destroy needed a rescue added.
A nil user is treated as a system context and bypasses the gate. The default
nil on three of those verbs is what makes that reachable, and removing those
defaults once every call site passes a user is the next tightening.
|
|
Deactivation adds the alumni role and leaves the others in place, so
reactivation is lossless and nobody has to remember what an account held.
login_from_session checks alumni? on every request, so a signed-in user is
locked out on their next one without any session invalidation. Guards
prevent deactivating yourself or the last active admin, and both verbs are
witnessed in the action log.
|
|
Replaces the two-way admin/user split with four groups ordered by
capability: administration, Redaktion, editors, alumni. alumni takes
precedence over capability in role_group, so a retired admin appears at the
bottom rather than the top.
Forms now offer the three roles as checkboxes rather than a single admin
checkbox, with a trailing hidden blank so an empty set can be posted, and
user_params permits roles only for admins. Three create buttons prefill the
common combinations.
|
|
Every path through Permission raised or lied: validates_presence_of on a
boolean made granted = false unsaveable, since false.blank? is true;
get_permission_for read a nonexistent identifier attribute; has_permission?
called an undefined plural method and returned a truthy relation in every
case; and set_permission mutated granted without saving. Nothing outside the
model referenced it and the table was empty.
|
|
|
|
German content is reachable at both /updates/foo and /de/updates/foo,
and a page with no English translation renders German at
/en/updates/foo through the fallback chain, so three URLs each
claimed to be canonical. Both tags now derive from one
locale-to-URL function, which points every duplicate at the address of
the version actually served, the unprefixed German URL, matching
default_url_options.
Alternates are declared only for locales in which the page genuinely has
a translation, since /en/ serving German through the fallback is not an
English version of the page. x-default points at German, the site's
primary language.
|
|
Replaces one hardcoded German description and an unrenderable SVG with
per-page title, description, canonical URL, locale and publication date,
plus the card variant or a site-wide default.
|
|
|
|
|
|
|
|
A lock with no draft or autosave behind it was invisible to
everyone including its holder, until the next editor collided
with it. work_in_progress now admits those nodes, and the
widget names the holder rather than the last editor on any
locked row.
Also flips the ordering to stalest-first, so a backlog reads as a queue
rather than a feed, and gives the heading a real total: the count came
from the already-limited relation and could never exceed five.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Enforced at restore via a login-time stamp, written only at genuine
logins so the limit stays absolute rather than sliding. The cookie
name rotation logs everyone out once at deploy. Second-factor users
are deliberately not treated worse than password-only ones.
|
|
Enrolled users get a pending marker instead of a session after the
password step; a valid code through the challenge writes the real
session via reset_session. otp_required without enrollment funnels
into setup everywhere except the enrollment, user, and login
machinery.
|
|
|
|
|
|
Asset names in summaries and publish deltas link to assets#show with
an inline Chronik beside each, suppressed inside that asset's own
zoom, per the node convention. assets#show gains a history button.
Also renames the details summary (no longer only translations) and
moves View Diff onto its own line.
|
|
ensure_autosave! gives body keystrokes and asset curation one shared
layer, so head is never mutated in place and every curation change
surfaces in the publish delta. Stale rendered join ids are mapped
across the clone via asset_id. Curation now requires holding the
lock; a missing lock answers 423, matching the autosave endpoint.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
head ? head.title : draft.title never accounted for a node with only
an autosave, reuses editable_page
|
|
A starred PDF previously ran through the same crop-and-lightbox path
a photo does -- exactly the awkward treatment explicit headline
designation was meant to avoid. _headline_image.html.erb now branches
on @headline_asset.pdf?: a PDF renders as a linked card (a :medium
thumbnail, a file icon, its name), no gallery participation at all.
The existing image-headline and gallery-fallback logic is otherwise
unchanged, now scoped to an image-specific headline only.
Other attached PDFs -- headlined or not -- list below as plain links,
same reasoning as why non-headline photos still get a gallery
trigger: an attached document shouldn't go invisible just because
nothing's been starred yet.
headline_image's own render guard needed widening to cover a page
with PDFs attached but no images and no headline at all -- the one
case none of the existing conditions accounted for.
|
|
|
|
|
|
|
|
|
|
|
|
pages_controller#preview loaded a page by id and rendered it
directly, assuming @page.node was always present. True for head and
draft, since both carry a real node_id: false for autosave, which
deliberately has node_id: nil (the mechanism that excludes it from
Node#pages and the revision count). A node with an autosave and no
draft underneath it: lock, start editing, never explicitly save a
draft: loads that autosave directly with no node at all, and any
view code assuming @page.node is real (headline image, credits,
gallery links) raises.
When @page has no node, resolve it by checking autosave_id, then
draft_id, then head_id on Node, and set @page.node in memory only:
the autosave's node_id: nil is never persisted differently. The
existing swap from draft to a fresher autosave, when both exist, is
unchanged.
Three tests: draft with a fresher autosave on top, autosave with no
draft underneath, and the ordinary head-only case with neither.
|