feat(billing): cap each owner's stored file volume

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-02 15:44:36 +03:30
co-authored by Cursor
parent e6e90aabf3
commit 28938e8f77
20 changed files with 647 additions and 22 deletions
+21 -1
View File
@@ -16,6 +16,7 @@ import type {
LoginResponse,
LoginResult,
OrgModuleKey,
StorageUsageDto,
SubscriptionSummaryDto,
} from '@mento/shared';
import { Prisma } from '@prisma/client';
@@ -26,6 +27,7 @@ import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import { LocalFileStorage } from '../storage/local-file.storage';
import { MembershipsService } from '../memberships/memberships.service';
import { BillingService } from '../billing/billing.service';
import { StorageQuotaService } from '../billing/storage-quota.service';
import { PlatformAdminAlertService } from '../billing/platform-admin-alert.service';
import { AttendanceService } from '../attendance/attendance.service';
import type { Actor } from '../common/record-scope';
@@ -112,6 +114,7 @@ export class AuthService {
private readonly files: LocalFileStorage,
private readonly memberships: MembershipsService,
private readonly billing: BillingService,
private readonly storageQuota: StorageQuotaService,
private readonly adminAlerts: PlatformAdminAlertService,
private readonly attendance: AttendanceService,
) {}
@@ -621,11 +624,15 @@ export class AuthService {
const storageKey = path
.join(organizationId, 'users', userId, `avatar.${ext}`)
.replace(/\\/g, '/');
const prevBytes = prevKey ? await this.files.sizeOf(prevKey) : 0;
const delta = file.size - prevBytes;
if (delta > 0) await this.storageQuota.assertCanAdd(organizationId, delta);
await this.files.save(storageKey, file.buffer);
if (prevKey && prevKey !== storageKey) {
await this.files.remove(prevKey);
}
await this.writeAvatarKey(userId, storageKey);
if (delta) await this.storageQuota.addLocal(organizationId, delta);
return this.toAuthUserDto(existing, organizationId);
}
@@ -635,7 +642,11 @@ export class AuthService {
throw new UnauthorizedException();
}
const prevKey = await this.readAvatarKey(userId);
if (prevKey) await this.files.remove(prevKey);
if (prevKey) {
const bytes = await this.files.sizeOf(prevKey);
await this.files.remove(prevKey);
if (bytes) await this.storageQuota.addLocal(organizationId, -bytes);
}
await this.writeAvatarKey(userId, null);
return this.toAuthUserDto(existing, organizationId);
}
@@ -829,6 +840,12 @@ export class AuthService {
canOpenTeamSettings: true,
}
: undefined;
let storage: StorageUsageDto | undefined;
try {
storage = await this.storageQuota.usageForOrganization(organizationId);
} catch {
storage = undefined;
}
return this.toAuthUser({
...user,
role,
@@ -839,6 +856,7 @@ export class AuthService {
membership?.organization.enabledModules,
),
subscription: sub,
storage,
isOrgOwner: role === 'owner',
priorArchive: this.priorArchiveFor(membership),
permissions: (Array.isArray(membership?.orgRole?.permissions)
@@ -949,6 +967,7 @@ export class AuthService {
avatarStorageKey?: string | null;
enabledModules?: OrgModuleKey[];
subscription?: SubscriptionSummaryDto | null;
storage?: StorageUsageDto | null;
isOrgOwner?: boolean;
priorArchive?: AuthUserDto['priorArchive'];
viewAs?: AuthUserDto['viewAs'];
@@ -965,6 +984,7 @@ export class AuthService {
...(user.mobile ? { mobile: user.mobile } : {}),
...(user.enabledModules ? { enabledModules: user.enabledModules } : {}),
...(user.subscription ? { subscription: user.subscription } : {}),
...(user.storage ? { storage: user.storage } : {}),
...(user.isOrgOwner !== undefined ? { isOrgOwner: user.isOrgOwner } : {}),
...(user.priorArchive ? { priorArchive: user.priorArchive } : {}),
...(user.viewAs ? { viewAs: user.viewAs } : {}),