11// SPDX-License-Identifier: AGPL-3.0-or-later
22
3- import type { UserID } from '../../../BrandedTypes' ;
3+ import { createUserID , type UserID } from '../../../BrandedTypes' ;
44import { BatchBuilder , fetchMany , fetchOne , upsertOne } from '../../../database/CassandraQueryExecution' ;
55import { Db } from '../../../database/CassandraTypes' ;
66import type { AuthSessionRow , AuthSessionTombstoneRow , UserCountryHistoryRow } from '../../../database/types/AuthTypes' ;
77import { Logger } from '../../../Logger' ;
8- import { getPhoneFraudGraphService } from '../../../middleware/ServiceSingletons' ;
8+ import { getCacheService , getPhoneFraudGraphService } from '../../../middleware/ServiceSingletons' ;
99import { AuthSession , AuthSessionTombstone } from '../../../models/AuthSession' ;
1010import { AuthSessions , AuthSessionsByUserId , AuthSessionTombstones , UserCountryHistory } from '../../../Tables' ;
1111
12- function invalidateAuthSessionCache ( _sessionIdHash : Buffer ) : void { }
12+ const AUTH_SESSION_CACHE_TTL_SECONDS = 30 ;
13+ const AUTH_SESSION_MISS_CACHE_TTL_SECONDS = 5 ;
14+
15+ interface CachedAuthSession {
16+ user_id : string ;
17+ session_id_hash : string ;
18+ created_at : number ;
19+ approx_last_used_at : number ;
20+ client_ip : string ;
21+ client_user_agent : string | null ;
22+ client_os : string | null ;
23+ client_country : string | null ;
24+ version : number ;
25+ }
26+
27+ function authSessionCacheKey ( sessionIdHash : Buffer ) : string {
28+ return `auth:session:${ sessionIdHash . toString ( 'base64url' ) } ` ;
29+ }
30+
31+ function encodeCachedAuthSession ( row : AuthSessionRow ) : CachedAuthSession {
32+ return {
33+ user_id : row . user_id . toString ( ) ,
34+ session_id_hash : row . session_id_hash . toString ( 'base64url' ) ,
35+ created_at : row . created_at . getTime ( ) ,
36+ approx_last_used_at : row . approx_last_used_at . getTime ( ) ,
37+ client_ip : row . client_ip ,
38+ client_user_agent : row . client_user_agent ,
39+ client_os : row . client_os ,
40+ client_country : row . client_country ,
41+ version : row . version ,
42+ } ;
43+ }
44+
45+ function decodeCachedAuthSession ( cached : CachedAuthSession ) : AuthSessionRow {
46+ return {
47+ user_id : createUserID ( BigInt ( cached . user_id ) ) ,
48+ session_id_hash : Buffer . from ( cached . session_id_hash , 'base64url' ) ,
49+ created_at : new Date ( cached . created_at ) ,
50+ approx_last_used_at : new Date ( cached . approx_last_used_at ) ,
51+ client_ip : cached . client_ip ,
52+ client_user_agent : cached . client_user_agent ,
53+ client_os : cached . client_os ,
54+ client_country : cached . client_country ,
55+ version : cached . version ,
56+ } ;
57+ }
58+
59+ async function invalidateAuthSessionCache ( sessionIdHashes : ReadonlyArray < Buffer > ) : Promise < void > {
60+ if ( sessionIdHashes . length === 0 ) return ;
61+ try {
62+ const cache = getCacheService ( ) ;
63+ await Promise . all ( sessionIdHashes . map ( ( sessionIdHash ) => cache . delete ( authSessionCacheKey ( sessionIdHash ) ) ) ) ;
64+ } catch ( error ) {
65+ Logger . error ( { error} , 'Failed to invalidate cached auth sessions; they expire with the cache ttl' ) ;
66+ }
67+ }
1368
1469const FETCH_AUTH_SESSIONS_CQL = AuthSessions . selectCql ( {
1570 where : AuthSessions . where . in ( 'session_id_hash' , 'session_id_hashes' ) ,
@@ -46,6 +101,7 @@ export class AuthSessionRepository {
46101 } ) ,
47102 ) ;
48103 await batch . execute ( ) ;
104+ await invalidateAuthSessionCache ( [ sessionData . session_id_hash ] ) ;
49105 try {
50106 await getPhoneFraudGraphService ( ) . recordSessionForCohortGraph (
51107 sessionData . user_id ,
@@ -107,10 +163,27 @@ export class AuthSessionRepository {
107163 }
108164
109165 async getAuthSessionByToken ( sessionIdHash : Buffer ) : Promise < AuthSession | null > {
110- const session = await fetchOne < AuthSessionRow > ( FETCH_AUTH_SESSION_BY_TOKEN_CQL , {
166+ try {
167+ const cached = await getCacheService ( ) . getOrSet < CachedAuthSession | null > (
168+ authSessionCacheKey ( sessionIdHash ) ,
169+ async ( ) => {
170+ const session = await this . fetchAuthSessionByToken ( sessionIdHash ) ;
171+ return session ? encodeCachedAuthSession ( session ) : null ;
172+ } ,
173+ ( value ) => ( value === null ? AUTH_SESSION_MISS_CACHE_TTL_SECONDS : AUTH_SESSION_CACHE_TTL_SECONDS ) ,
174+ ) ;
175+ return cached ? new AuthSession ( decodeCachedAuthSession ( cached ) ) : null ;
176+ } catch ( error ) {
177+ Logger . warn ( { error} , 'Auth session cache lookup failed; falling back to the datastore' ) ;
178+ const session = await this . fetchAuthSessionByToken ( sessionIdHash ) ;
179+ return session ? new AuthSession ( session ) : null ;
180+ }
181+ }
182+
183+ private async fetchAuthSessionByToken ( sessionIdHash : Buffer ) : Promise < AuthSessionRow | null > {
184+ return fetchOne < AuthSessionRow > ( FETCH_AUTH_SESSION_BY_TOKEN_CQL , {
111185 session_id_hash : sessionIdHash ,
112186 } ) ;
113- return session ? new AuthSession ( session ) : null ;
114187 }
115188
116189 async listAuthSessions ( userId : UserID ) : Promise < Array < AuthSession > > {
@@ -138,11 +211,12 @@ export class AuthSessionRepository {
138211 await upsertOne (
139212 AuthSessions . patchByPk ( { session_id_hash : sessionIdHash } , { approx_last_used_at : Db . set ( approximateLastUsedAt ) } ) ,
140213 ) ;
141- invalidateAuthSessionCache ( sessionIdHash ) ;
214+ await invalidateAuthSessionCache ( [ sessionIdHash ] ) ;
142215 }
143216
144217 async deleteAuthSessions ( userId : UserID , sessionIdHashes : Array < Buffer > ) : Promise < void > {
145218 if ( sessionIdHashes . length === 0 ) return ;
219+ await invalidateAuthSessionCache ( sessionIdHashes ) ;
146220 let originals : Array < AuthSessionRow > = [ ] ;
147221 try {
148222 originals = await fetchMany < AuthSessionRow > ( FETCH_AUTH_SESSIONS_CQL , {
@@ -165,6 +239,7 @@ export class AuthSessionRepository {
165239 batch . addPrepared ( AuthSessionTombstones . insert ( toTombstoneRow ( original , deletedAt ) ) ) ;
166240 }
167241 await batch . execute ( ) ;
242+ await invalidateAuthSessionCache ( sessionIdHashes ) ;
168243 }
169244
170245 async deleteAllAuthSessions ( userId : UserID ) : Promise < void > {
@@ -174,10 +249,12 @@ export class AuthSessionRepository {
174249 user_id : userId ,
175250 } ) ;
176251 if ( sessionRefs . length === 0 ) return ;
252+ const sessionIdHashes = sessionRefs . map ( ( session ) => session . session_id_hash ) ;
253+ await invalidateAuthSessionCache ( sessionIdHashes ) ;
177254 let originals : Array < AuthSessionRow > = [ ] ;
178255 try {
179256 originals = await fetchMany < AuthSessionRow > ( FETCH_AUTH_SESSIONS_CQL , {
180- session_id_hashes : sessionRefs . map ( ( s ) => s . session_id_hash ) ,
257+ session_id_hashes : sessionIdHashes ,
181258 } ) ;
182259 } catch ( error ) {
183260 Logger . warn (
@@ -187,19 +264,20 @@ export class AuthSessionRepository {
187264 }
188265 const deletedAt = new Date ( ) ;
189266 const batch = new BatchBuilder ( ) ;
190- for ( const session of sessionRefs ) {
191- batch . addPrepared ( AuthSessions . deleteByPk ( { session_id_hash : session . session_id_hash } ) ) ;
267+ for ( const sessionIdHash of sessionIdHashes ) {
268+ batch . addPrepared ( AuthSessions . deleteByPk ( { session_id_hash : sessionIdHash } ) ) ;
192269 batch . addPrepared (
193270 AuthSessionsByUserId . deleteByPk ( {
194271 user_id : userId ,
195- session_id_hash : session . session_id_hash ,
272+ session_id_hash : sessionIdHash ,
196273 } ) ,
197274 ) ;
198275 }
199276 for ( const original of originals ) {
200277 batch . addPrepared ( AuthSessionTombstones . insert ( toTombstoneRow ( original , deletedAt ) ) ) ;
201278 }
202279 await batch . execute ( ) ;
280+ await invalidateAuthSessionCache ( sessionIdHashes ) ;
203281 }
204282}
205283
0 commit comments