feat(files): API tokens in Settings, the Hub folder in Files, a notice before a token runs out

Settings gains an API tokens tab: a row per token (name and prefix; each
folder with its rights; last use and expiry, "expires soon" marked),
revoked from its menu behind a confirm. A new token is the Hub exchange in
one click, or folders and rights picked per folder (no Write where an
upload cannot land); expiry 30/90 days, 1 year or never (warned). The
token shows once in the same dialog, with Copy and a ready curl line.

The Hub folder closes the Sources in the Files rail, takes uploads and
moved-in files. A token about to run out warns its user once, a week
before (the Files GC's sixth pass); the notice opens the tokens tab.
This commit is contained in:
2026-10-14 23:33:34 +02:00
parent 4f58d15d3f
commit db56f25387
39 changed files with 1759 additions and 26 deletions
+5 -1
View File
@@ -124,6 +124,9 @@ export default function FilesRail({
compareText(folderLabel(a, t), folderLabel(b, t), i18n.language);
const sources = folders.filter((f) => f.kind === "source" && shown(f)).sort(byName);
const own = folders.filter((f) => f.kind === "own").sort(byName);
// The Hub folder closes the sources, shown even while empty: it exists only once a hub token made
// it, and an empty one is where the hub's files will turn up.
const hub = folders.filter((f) => f.kind === "hub");
const [renamingId, setRenamingId] = useState<number | null>(null);
const [creating, setCreating] = useState(false);
@@ -202,8 +205,9 @@ export default function FilesRail({
<div className="flex flex-col gap-0.5">
{failed}
{sources.map(entry)}
{hub.map(entry)}
{/* Only when there is nothing at all: files moved into own folders were still downloaded. */}
{foldersQ.data && sources.length === 0 && total === 0 && (
{foldersQ.data && sources.length + hub.length === 0 && total === 0 && (
<span className="px-2 py-1 text-xs text-muted">{t("files.rail.noSources")}</span>
)}
</div>