v0.151.1
 1from __future__ import annotations
 2
 3from plain.chores import Chore, register_chore
 4from plain.postgres import Q
 5from plain.utils import timezone
 6
 7from .models import AccessToken, AuthorizationCode, RefreshToken
 8
 9
10@register_chore
11class ClearExpiredOAuthTokens(Chore):
12    """Delete spent authorization codes and dead OAuth tokens.
13
14    Refresh-token rotation issues a new pair on every use, so without this
15    these tables grow unbounded and the hot-path code/token lookups slow down.
16    """
17
18    def run(self) -> str:
19        now = timezone.now()
20
21        codes = AuthorizationCode.query.filter(
22            Q(used=True) | Q(expires_at__lt=now)
23        ).delete()
24
25        # Refresh tokens first: a RefreshToken's CASCADE FK to AccessToken means
26        # deleting an access token would take a still-valid refresh with it. So
27        # drop dead refresh tokens up front, then only remove access tokens that
28        # no surviving (valid) refresh token still points at.
29        refresh = RefreshToken.query.filter(
30            Q(revoked=True) | Q(expires_at__lt=now)
31        ).delete()
32
33        live_access_ids = RefreshToken.query.values_list("access_token", flat=True)
34        access = (
35            AccessToken.query.filter(Q(revoked=True) | Q(expires_at__lt=now))
36            .exclude(id__in=live_access_ids)
37            .delete()
38        )
39
40        return (
41            f"{codes} codes, {refresh} refresh tokens, {access} access tokens deleted"
42        )