Documentation

Permissions

Show Edit and other actions only to visitors who may use them with useContentPermissions, while the VitNode Content Engine authorizes every write on the server. Explains staff, moderator and member permissions.

A page should show Edit only to people who may edit. useContentPermissions asks the Content Engine what the current visitor may do with the records on screen, so the page never compares author ids or guesses who is a moderator. The server still checks every write itself.

This continues the article from Edit form. Authors can edit at /news/:id/edit, but nothing links there yet.

Ask the Content Engine

Create src/pages/article-permissions.ts. It wraps useContentPermissions with the typed fetcher call for your articles:

plugins/newsroom/src/pages/article-permissions.ts
import { useContentPermissions } from "@vitnode/core/tanstack/content";
import { fetcher } from "@vitnode/core/tanstack/fetcher";

import { manifest } from "@/manifest.gen";

/** What the visitor may do with each article on screen: `canEdit`, `canReport`. */
export const useArticlePermissions = (ids: readonly number[]) =>
  useContentPermissions({
    ids,
    queryKey: [manifest.id, "articles"],
    fetch: async request => {
      const res = await fetcher({
        plugin: manifest.id,
        method: "get",
        module: "content/news",
        path: "/permissions",
        args: { query: { ids: request.join(",") } },
      });
      if (res.status !== 200) throw new Error("Loading permissions failed.");

      return await res.json();
    },
  });

The route, GET /api/newsroom/content/news/permissions, exists because the article has publicWrite.update. queryKey starts with the article list's prefix, so refreshing the list refreshes the answers too.

Edit src/pages/article-page.tsx. Add an ArticleActions component above ArticlePage, and render it in the article's header. These are excerpts:

plugins/newsroom/src/pages/article-page.tsx
import { buttonVariants } from "@vitnode/core/components/ui/button";
import { HrefLink } from "@vitnode/core/tanstack/link";
import { useTranslations } from "use-intl";

import { useArticlePermissions } from "./article-permissions";

/** Asked in the browser, so the cached page stays the same for everyone. */
const ArticleActions = ({ id }: { id: number }) => {
  const t = useTranslations("newsroom.articles");
  const { canEdit } = useArticlePermissions([id]).of(id);

  return (
    <div className="flex flex-wrap gap-2">
      {canEdit ? (
        <HrefLink
          className={buttonVariants({ size: "sm", variant: "outline" })}
          href={`/news/${id}/edit`}
        >
          {t("edit_link")}
        </HrefLink>
      ) : null}
    </div>
  );
};
plugins/newsroom/src/pages/article-page.tsx
    <header className="flex flex-col gap-2">
      {/* title and date as before */}
      <ArticleActions id={item.id} />
    </header>

Add "edit_link": "Edit" under newsroom.articles in src/locales/en.json.

of(id) reads all false until the answer arrives, so the link appears a moment after the page, and never for someone who may not use it.

Check the result

  1. Sign in as the member who wrote an article and open it. Edit shows below the date and opens the edit form.
  2. Open it as another member. There is no Edit.
  3. Open an article staff wrote in the AdminCP. Its author is empty, so only a moderator sees Edit.
An article page viewed by its author, with the title, the date and an Edit button below it

The server still decides

The answer is a hint for the interface, not a lock. Every write route checks the session, the owner and the moderator permissions again, so a stale Edit, for example after an edit window closed, only leads to a refusal message. The flags also carry no reason; the write route says why when it refuses.

Keep the answer out of anything shared. Article pages are the same for every reader and can be cached; the permissions request is personal, never cached outside the tab, and keyed by the signed-in user.

Staff, moderators and members

Three kinds of permission meet on a content type:

WhoWhere they actGranted inExample
StaffThe AdminCP and its /admin/ routesAdminCP → Staff → Administratorsnewsroom:article:can_edit edits any article in the AdminCP
ModeratorsThe public site, the ModCPAdminCP → Staff → ModeratorsThe moderator can_edit edits any article from the site
MembersThe public siteOwnership, set by the serverThe author edits their own article

canEdit combines the last two. It is true for the owner while the rules allow it, and for a moderator with can_edit. An AdminCP session never adds powers on the public site. Moderators edit with the same forms as members, so one edit page serves both.

Lists and threads

On a page with many records, pass every id on screen to one hook call and read each row with of(id). The hook splits more than 50 ids into batches, so a long thread works too:

const permissions = useArticlePermissions(
  articles?.edges.map(article => article.id) ?? [],
);

permissions.of(article.id).canEdit;

Permissions API has the route's contract, every option and return value of the hook, and when to invalidate it. Next, let readers flag a problem with Reports.