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 )