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
@@ -31,5 +31,7 @@
"file_folder_create": "Folder created",
"file_folder_rename": "Folder renamed",
"file_folder_retention": "Folder retention changed",
"file_folder_delete": "Folder deleted"
"file_folder_delete": "Folder deleted",
"api_token_create": "API token created",
"api_token_revoke": "API token revoked"
}
+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"
}
}
@@ -31,5 +31,7 @@
"file_folder_create": "Mappa létrehozva",
"file_folder_rename": "Mappa átnevezve",
"file_folder_retention": "Mappa megőrzési ideje módosítva",
"file_folder_delete": "Mappa törölve"
"file_folder_delete": "Mappa törölve",
"api_token_create": "API-token létrehozva",
"api_token_revoke": "API-token visszavonva"
}
+10 -2
View File
@@ -38,7 +38,8 @@
"profile": "Ez a letöltési profil már nem létezik",
"season": "Ez az évad már nincs a könyvtárban",
"show": "Ez a sorozat már nincs a könyvtárban",
"folder": "Ez a mappa már nem létezik"
"folder": "Ez a mappa már nem létezik",
"api_token": "Ez az API-token már nem létezik"
},
"admin": {
"not_allowed": "A címe már nincs jóváhagyva. Előbb vedd fel a fehérlistára.",
@@ -239,6 +240,13 @@
"file_folder_gone": "Ez a mappa már nem létezik",
"file_hash_busy": "Most túl sok fájl ellenőrzése fut — próbáld újra kicsit később",
"file_folder_not_its_source": "Egy fájl csak a saját mappáidba vagy vissza az eredeti mappájába kerülhet",
"file_no_preview": "Ehhez a fájlhoz nincs előnézet"
"file_no_preview": "Ehhez a fájlhoz nincs előnézet",
"api_token_rate_limited": "Túl sok kérés érkezett API-tokennel — várj néhány percet",
"api_token_folder_required": "Az API-tokennek meg kell neveznie a mappát, amelybe feltölt",
"api_token_scope": "API-tokennel nem érhetők el a veled megosztott fájlok",
"api_token_preset_grants": "A Hub-token magától kapja a mappáját — ne adj meg mappát",
"api_token_no_grants": "Válassz legalább egy mappát, amelyet a token használhat",
"api_token_name_required": "Adj nevet a tokennek",
"api_token_limit": "Már {{limit}} tokened van — előbb vonj vissza egyet"
}
}