fix(api): strip expired premium flags using read repair (#24)

This commit is contained in:
hampus-fluxer
2026-01-04 22:52:33 +01:00
committed by GitHub
parent b22c6733c3
commit c4be1d424c
3 changed files with 80 additions and 2 deletions

View File

@@ -55,3 +55,29 @@ export function checkIsPremium(user: PremiumCheckable): boolean {
return nowMs <= untilMs + GRACE_MS;
}
export const PREMIUM_CLEAR_FIELDS = [
'premium_type',
'premium_since',
'premium_until',
'premium_will_cancel',
'premium_billing_cycle',
] as const;
export type PremiumClearField = (typeof PREMIUM_CLEAR_FIELDS)[number];
export function shouldStripExpiredPremium(user: PremiumCheckable): boolean {
if ((user.premiumType ?? 0) <= 0) {
return false;
}
return !checkIsPremium(user);
}
export function mapExpiredPremiumFields<T>(mapper: (field: PremiumClearField) => T): Record<PremiumClearField, T> {
const result = {} as Record<PremiumClearField, T>;
for (const field of PREMIUM_CLEAR_FIELDS) {
result[field] = mapper(field);
}
return result;
}

View File

@@ -20,8 +20,10 @@
import {createUserID, type UserID} from '~/BrandedTypes';
import {buildPatchFromData, Db, executeVersionedUpdate, fetchMany, fetchOne} from '~/database/Cassandra';
import {EMPTY_USER_ROW, USER_COLUMNS, type UserRow} from '~/database/CassandraTypes';
import {Logger} from '~/Logger';
import {User} from '~/Models';
import {Users} from '~/Tables';
import {shouldStripExpiredPremium} from '~/user/UserHelpers';
const FETCH_USERS_BY_IDS_CQL = Users.selectCql({
where: Users.where.in('user_id', 'user_ids'),
@@ -68,8 +70,26 @@ export class UserDataRepository {
});
}
const user = await fetchOne<UserRow>(FETCH_USER_BY_ID_CQL, {user_id: userId});
return user ? new User(user) : null;
const userRow = await fetchOne<UserRow>(FETCH_USER_BY_ID_CQL, {user_id: userId});
if (!userRow) {
return null;
}
const user = new User(userRow);
if (shouldStripExpiredPremium(user)) {
try {
await this.readRepairExpiredPremium(user);
const repairedRow = await fetchOne<UserRow>(FETCH_USER_BY_ID_CQL, {
user_id: userId,
});
return repairedRow ? new User(repairedRow) : null;
} catch (error) {
Logger.warn({userId: user.id.toString(), error}, 'Failed to repair expired premium fields while reading user');
}
}
return user;
}
async findUniqueAssert(userId: UserID): Promise<User> {
@@ -148,4 +168,18 @@ export class UserDataRepository {
await this.patchUser(userId, patch);
}
private async readRepairExpiredPremium(user: User): Promise<void> {
await this.patchUser(user.id, createPremiumClearPatch());
}
}
function createPremiumClearPatch(): UserPatch {
return {
premium_type: Db.clear<number | null>(),
premium_since: Db.clear<Date | null>(),
premium_until: Db.clear<Date | null>(),
premium_will_cancel: Db.clear<boolean | null>(),
premium_billing_cycle: Db.clear<string | null>(),
};
}