feat(files): API tokens — a machine reaches the library folder by folder

A user can make a token for a machine of theirs (`/api/tokens`, signed-in only): `sfl_` + 32
random bytes + a checksum, only its SHA-256 kept, shown once. Sent as a Bearer header it is
honoured on six `/api/files` routes only — folders, list, file, hash, upload, delete — and there
only in the folders its grants name, with the rights they give (list / read / write / delete).
One check, `FilesCaller.may`, in every route; a folder or file out of reach answers like a
missing one; the files shared with the user are never in reach. A token beats a cookie on the
same request.

The Hub preset makes the user's one Hub folder (a new folder kind) and grants it every right.
Refused tokens are slowed down per client, a runaway machine per token; last use is stamped at
most once a minute; making and revoking a token is audited, and a token's uploads and deletes
name it in their audit rows.
This commit is contained in:
2026-10-14 18:40:23 +02:00
parent c335d54929
commit b2d34e6e6c
18 changed files with 1080 additions and 48 deletions
+10 -2
View File
@@ -38,7 +38,8 @@
"profile": "This download profile doesn't exist any more",
"season": "This season isn't in the library any more",
"show": "This show isn't in the library any more",
"folder": "That folder no longer exists"
"folder": "That folder no longer exists",
"api_token": "That API token no longer exists"
},
"admin": {
"not_allowed": "Their address isn't approved any more. Add it to the whitelist first.",
@@ -239,6 +240,13 @@
"file_folder_gone": "That folder no longer exists",
"file_hash_busy": "Too many files are being checked right now — try again in a moment",
"file_folder_not_its_source": "A file can only go into one of your own folders or back to the folder it came from",
"file_no_preview": "There is no preview for this file"
"file_no_preview": "There is no preview for this file",
"api_token_rate_limited": "Too many requests with API tokens — wait a few minutes",
"api_token_folder_required": "An API token must name the folder it uploads into",
"api_token_scope": "An API token cannot reach the files shared with you",
"api_token_preset_grants": "The Hub token gets its folder by itself — leave the folders out",
"api_token_no_grants": "Choose at least one folder the token may use",
"api_token_name_required": "Give the token a name",
"api_token_limit": "You already have {{limit}} tokens — revoke one first"
}
}