This commit is contained in:
@@ -152,7 +152,7 @@ export class AdminController {
|
||||
@Post('images/cleanup')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
cleanupImages(@Body(zod(dryRunSchema)) dto: Body<typeof dryRunSchema>, @CurrentUser() user: AuthUser) {
|
||||
return this.admin.cleanupImages(dto.dryRun, user);
|
||||
return this.admin.cleanupImages(dto.dryRun, { type: 'user', id: user.id });
|
||||
}
|
||||
|
||||
@Post('email/test')
|
||||
|
||||
@@ -4,10 +4,11 @@ import { RemindersModule } from '../reminders/reminders.module';
|
||||
import { VenuesModule } from '../venues/venues.module';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
import { HousekeepingService } from './housekeeping.service';
|
||||
|
||||
@Module({
|
||||
imports: [ImagesModule, RemindersModule, VenuesModule],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService],
|
||||
providers: [AdminService, HousekeepingService],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SystemHealth,
|
||||
} from '@kooltap/shared';
|
||||
import { Prisma, PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AuditEntry as AuditRecord, AuditService } from '../audit/audit.service';
|
||||
import { RealtimeService } from '../realtime/realtime.service';
|
||||
import { RemindersService } from '../reminders/reminders.service';
|
||||
import { VenuesService } from '../venues/venues.service';
|
||||
@@ -38,6 +38,11 @@ const ORPHAN_IMAGE_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** Dashboard sessions that expired or were signed out before [before]; they can never be used again. */
|
||||
export const endedSessions = (before: Date): Prisma.RefreshTokenWhereInput => ({
|
||||
OR: [{ expiresAt: { lt: before } }, { revokedAt: { lt: before } }],
|
||||
});
|
||||
|
||||
/** Everything the superadmin page does. */
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -387,9 +392,13 @@ export class AdminService {
|
||||
const venue = venueId ? { venueId } : {};
|
||||
const tabEvents = { ...venue, receivedAt: { lt: before } };
|
||||
const auditLog = { ...venue, createdAt: { lt: before } };
|
||||
const refreshTokens = { ...endedSessions(before), ...(venueId ? { user: { venueId } } : {}) };
|
||||
const run: Record<PurgeTarget, () => Promise<number>> = {
|
||||
tabEvents: () => (dryRun ? this.prisma.tabEvent.count({ where: tabEvents }) : this.prisma.tabEvent.deleteMany({ where: tabEvents }).then((r) => r.count)),
|
||||
auditLog: () => (dryRun ? this.prisma.auditLog.count({ where: auditLog }) : this.prisma.auditLog.deleteMany({ where: auditLog }).then((r) => r.count)),
|
||||
refreshTokens: () => (dryRun
|
||||
? this.prisma.refreshToken.count({ where: refreshTokens })
|
||||
: this.prisma.refreshToken.deleteMany({ where: refreshTokens }).then((r) => r.count)),
|
||||
};
|
||||
|
||||
const counts: Partial<Record<PurgeTarget, number>> = {};
|
||||
@@ -401,7 +410,7 @@ export class AdminService {
|
||||
}
|
||||
|
||||
/** Deletes pictures no product uses, after a grace period for terminals that upload before they link. */
|
||||
async cleanupImages(dryRun: boolean, actor: AuthUser) {
|
||||
async cleanupImages(dryRun: boolean, actor: AuditRecord['actor']) {
|
||||
const images = await this.prisma.productImage.findMany({
|
||||
where: { products: { none: {} }, createdAt: { lt: new Date(Date.now() - ORPHAN_IMAGE_GRACE_MS) } },
|
||||
select: { id: true, bytes: true },
|
||||
@@ -409,7 +418,7 @@ export class AdminService {
|
||||
const bytes = images.reduce((sum, image) => sum + image.bytes, 0);
|
||||
if (!dryRun && images.length > 0) {
|
||||
await this.images.remove(images.map((image) => image.id));
|
||||
await this.audit.log({ venueId: null, actor: { type: 'user', id: actor.id }, action: 'admin_images_cleaned', details: { count: images.length } });
|
||||
await this.audit.log({ venueId: null, actor, action: 'admin_images_cleaned', details: { count: images.length } });
|
||||
}
|
||||
return { dryRun, count: images.length, bytes };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Injectable, Logger, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AdminService, endedSessions } from './admin.service';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Out of the way of startup and the migrations, then once a day
|
||||
const FIRST_RUN_DELAY_MS = 5 * 60 * 1000;
|
||||
const INTERVAL_MS = DAY_MS;
|
||||
|
||||
// Ended sessions are kept a while, so a superadmin can still see recent sign-outs while looking into something
|
||||
const ENDED_SESSION_RETENTION_MS = 7 * DAY_MS;
|
||||
|
||||
/**
|
||||
* Daily cleanup of what can never be used again: ended sessions, used and expired registration codes, and pictures
|
||||
* no product uses. History (terminal events, audit log) is a choice, so it stays with the purge on the admin page.
|
||||
* Every instance runs it; the deletes don't mind running twice.
|
||||
*/
|
||||
@Injectable()
|
||||
export class HousekeepingService implements OnApplicationBootstrap, OnApplicationShutdown {
|
||||
private readonly logger = new Logger(HousekeepingService.name);
|
||||
private timer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private admin: AdminService,
|
||||
) {}
|
||||
|
||||
onApplicationBootstrap() {
|
||||
// Tests run against a database they set up themselves
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
this.timer = setTimeout(() => {
|
||||
void this.run();
|
||||
this.timer = setInterval(() => void this.run(), INTERVAL_MS).unref();
|
||||
}, FIRST_RUN_DELAY_MS).unref();
|
||||
}
|
||||
|
||||
onApplicationShutdown() {
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
/** Never throws: a failure is logged and the next run tries again. */
|
||||
async run() {
|
||||
try {
|
||||
const sessions = await this.prisma.refreshToken.deleteMany({ where: endedSessions(new Date(Date.now() - ENDED_SESSION_RETENTION_MS)) });
|
||||
const codes = await this.admin.cleanupRegistrationCodes();
|
||||
const images = await this.admin.cleanupImages(false, { type: 'system', id: null });
|
||||
this.logger.log(`Deleted ${sessions.count} ended session(s), ${codes.deleted} registration code(s), ${images.count} unused picture(s)`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Housekeeping failed: ${error instanceof Error ? error.stack : error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user