The admin is where the news is written: the entries, the editor, and the
settings. Tayori.Router.tayori_admin/2 mounts it in your router.
import Tayori.Router
scope "/admin" do
pipe_through [:browser, :require_admin]
tayori_admin "/news",
resolver: MyAppWeb.TayoriResolver,
on_mount: [{MyAppWeb.AdminAuth, :require_admin}],
csp_nonce_assign_key: :csp_nonce
endIt is a page of its own, as Lotus's dashboard and Oban Web are: its own
document, script and style sheet, served at /admin/news/assets/ with the
hash of the build in their names. Your app's layout, CSS and JavaScript
never reach it, and it never reaches them. It joins your app's LiveView
socket (/live, or :socket_path).
Behind your authentication
Mount the admin in a scope whose pipeline lets in only your admins. Three things keep it closed, as in Lotus:
- Your pipeline runs on the request of the page, and on the requests of its script and style sheet.
- Your resolver reads the admin from that request
(
Tayori.Resolver.resolve_user/1) and says what they may do (Tayori.Resolver.resolve_access/1)::all- write, publish, delete, and change the settings;:read_only- read the entries and the drafts, change nothing;:forbidden- nothing: the admin answers with a 404;{:forbidden, path}- nothing: the admin sends them topath.
- Your
on_mounthooks (:on_mount) run on every mount and reconnect of the admin's LiveViews, before Tayori's: check the admin's session there, so that a session that ended stops the admin at once.
There is no default: a resolver without resolve_user/1 and
resolve_access/1 does not compile, and the resolver the installer
writes lets no one in.
The admin goes into the LiveView's session as resolve_user/1 returns
it: return a small map or struct, not a user with all its associations.
An admin of its own
An app with admins apart from its users (their own table, as in indie.camera) reads them from its own assigns:
def resolve_user(conn), do: conn.assigns[:current_admin_user]
def resolve_access(%MyApp.Admin.User{}), do: :all
def resolve_access(nil), do: {:forbidden, "/admin/login"}Who wrote it
Tayori.Resolver.author/1 gives the author kept on each new entry:
def author(admin), do: %{"id" => admin.id, "name" => admin.name}Segments
Tayori.Resolver.segments/0 gives the segments the editor offers, alone
or in groups:
def segments do
[
{"Plan", [{"plan:free", "Free"}, {"plan:pro", "Pro"}]},
{"Role", [{"role:owner", "Owners"}, {"role:editor", "Editors"}]}
]
endAn entry with no segments is for everyone; an entry with segments is for the readers in at least one of them. See Readers.
Languages
The editor has a tab for each language of the entry, and adds the others
of :locales. The first language is required; the others are optional,
and readers of a language an entry lacks read the first.
Attachments
The editor is Kotoba's, with its attachments: images, PDFs and videos,
dropped or pasted into the body. Kotoba keeps them in its storage, which
your app configures (config :kotoba, storage: ..., see Kotoba's Uploads
guide), or in another for the admin alone:
tayori_admin "/news", resolver: MyAppWeb.TayoriResolver, storage: MyApp.NewsStorageWriting
The editor has the entry's title, summary and body in each language, and beside them its cards:
- Publication: publish now or at a time, save a draft, copy a preview link (see below), and see the entry on the public page.
- Cover: an optional picture of the entry's own, dropped or picked, with its description for screen readers.
- Details: the end of its address, its categories (Categories, in the admin's bar, has the list), and pinning it.
- Audience: the segments it is for (see Readers).
- Announcement, Email and Last 30 days, with those features on (see Features).
Preview shows the entry as your readers will see it: on the public page, in the panel, or as its announcement, in the language of the tab.
Publishing
An entry is a draft until it is published: now, or at a time picked in
the admin's own time zone (their browser's, when your app has a time zone
database such as tz or tzdata, else UTC). A scheduled entry is
published at its time, by the node that reaches it first; each entry is
announced once, to every open page, and sends what it sends (emails,
webhooks, your notifier; see Notifications).
Unpublishing makes it a draft again; publishing it again announces it
again.
A preview link shows the last saved version of a draft or a scheduled
entry on the public page, to whoever has it, for a week. The admin finds
your tayori_page in the same router; when the page is elsewhere, give
its path:
tayori_admin "/news", resolver: MyAppWeb.TayoriResolver, page: "/changelog"The theme
theme: :sumi gives the admin the Sumi theme of Hattori AI. Both themes
follow the system's light and dark, or the admin's choice in the bar's
appearance menu (light, dark or the system's), kept in their browser.
Content Security Policy
With :csp_nonce_assign_key, the admin's <link> and <script> carry
the nonce of your policy, read from conn.assigns (one key, or
%{style: key, script: key}). The admin needs:
script-src 'self' 'nonce-…';style-src 'self' 'nonce-…'andstyle-src-attr 'unsafe-inline'(the editor and the date picker place their popovers with inline styles);connect-src 'self' wss:for the LiveView socket;img-srcandmedia-srcwith the origin of Kotoba's storage, for attachments.
Reconnects after a deploy
The names of the admin's script and style sheet carry the hash of the build, and are cached for a year. A page of an older build asks for files the new build does not have, and gets a 404: reload it.