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:
@@ -16,11 +16,20 @@ from sqlalchemy import select
|
||||
from app import apitokens
|
||||
from app.apitokens import files_caller
|
||||
from app.downloads import folders, service
|
||||
from app.downloads import gc as gcmod
|
||||
from app.downloads import upload as uploadmod
|
||||
from app.downloads.status import JobStatus
|
||||
from app.config import settings
|
||||
from app.main import app
|
||||
from app.models import ApiToken, ApiTokenGrant, AuditLog, DownloadJob, FileFolder, MediaAsset
|
||||
from app.models import (
|
||||
ApiToken,
|
||||
ApiTokenGrant,
|
||||
AuditLog,
|
||||
DownloadJob,
|
||||
FileFolder,
|
||||
MediaAsset,
|
||||
Notification,
|
||||
)
|
||||
from app.ratelimit import RateLimiter
|
||||
from app.routes import tokens as tokens_routes
|
||||
|
||||
@@ -329,6 +338,34 @@ def test_a_malformed_unknown_expired_revoked_or_suspended_token_holds_nothing(
|
||||
assert _refused(_bearer(raw)) == 401
|
||||
|
||||
|
||||
def test_a_token_about_to_run_out_warns_its_user_once_and_is_marked_in_the_list(
|
||||
db, make_user, as_user
|
||||
):
|
||||
alice, bob = make_user(), make_user()
|
||||
c = as_user(alice)
|
||||
soon = _make(c, preset="hub", expires_days=3)
|
||||
_make(c, preset="hub", expires_days=30) # outside the span
|
||||
_make(c, preset="hub") # never runs out
|
||||
gone = _make(c, preset="hub", expires_days=1)
|
||||
db.get(ApiToken, gone["id"]).expires_at = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||||
_make(as_user(bob), preset="hub", expires_days=2)
|
||||
db.commit()
|
||||
assert [t["expiring"] for t in c.get("/api/tokens").json()] == [True, False, False, False]
|
||||
|
||||
assert apitokens.warn_expiring(db) == 2 # alice's and bob's; the run-out one is not "soon"
|
||||
notes = db.execute(select(Notification).where(Notification.user_id == alice.id)).scalars()
|
||||
[note] = notes.all()
|
||||
assert note.type == "api_token_expiring"
|
||||
assert note.data["token_id"] == soon["id"] and note.data["name"] == "a machine"
|
||||
assert note.data["expires_at"] == soon["expires_at"]
|
||||
assert apitokens.warn_expiring(db) == 0 # once per token
|
||||
|
||||
|
||||
def test_the_files_gc_runs_the_token_warning(db, make_user, as_user, _root):
|
||||
_make(as_user(make_user()), preset="hub", expires_days=3)
|
||||
assert gcmod.run_download_gc(db)["tokens_warned"] == 1
|
||||
|
||||
|
||||
def test_another_users_token_cannot_be_revoked(db, make_user, as_user):
|
||||
alice, bob = make_user(), make_user()
|
||||
made = _make(as_user(alice), preset="hub")
|
||||
|
||||
Reference in New Issue
Block a user