Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ USE_REDIS_CACHE=true
PORT=3001
APP_NAME=STATION BACKEND

# Token Cleanup Cron (default: 3am daily)
REFRESH_TOKEN_CLEANUP_CRON="0 3 * * *"

# UEX Sync Configuration
UEX_SYNC_ENABLED=true
UEX_CATEGORIES_SYNC_ENABLED=true
Expand Down
9 changes: 8 additions & 1 deletion backend/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TokenCleanupService } from './token-cleanup.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { RefreshTokenStrategy } from './refresh-token.strategy';
Expand All @@ -27,7 +28,13 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy, RefreshTokenStrategy],
providers: [
AuthService,
TokenCleanupService,
LocalStrategy,
JwtStrategy,
RefreshTokenStrategy,
],
exports: [AuthService],
})
export class AuthModule {}
64 changes: 64 additions & 0 deletions backend/src/modules/auth/token-cleanup.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { RefreshToken } from './refresh-token.entity';
import { PasswordReset } from './password-reset.entity';

@Injectable()
export class TokenCleanupService {
private readonly logger = new Logger(TokenCleanupService.name);

constructor(
@InjectRepository(RefreshToken)
private readonly refreshTokenRepository: Repository<RefreshToken>,
@InjectRepository(PasswordReset)
private readonly passwordResetRepository: Repository<PasswordReset>,
private readonly configService: ConfigService,
) {}

@Cron(
// Default: 3am daily — override with REFRESH_TOKEN_CLEANUP_CRON env var
process.env['REFRESH_TOKEN_CLEANUP_CRON'] ?? '0 3 * * *',
)
Comment on lines +5 to +24
async cleanupExpiredTokens(): Promise<void> {
if (process.env['NODE_ENV'] === 'test') {
return;
}

const start = Date.now();
this.logger.log('Starting expired/revoked token cleanup');
Comment on lines +21 to +31

try {
const now = new Date();

const { affected: refreshDeleted } = await this.refreshTokenRepository
.createQueryBuilder()
.delete()
.where('revoked = :revoked OR expires_at < :now', {
revoked: true,
now,
})
.execute();

const { affected: resetDeleted } = await this.passwordResetRepository
.createQueryBuilder()
.delete()
.where('used = :used OR expires_at < :now', { used: true, now })
.execute();
Comment on lines +36 to +49

const duration = Date.now() - start;
this.logger.log(
`Token cleanup complete in ${duration}ms — ` +
`deleted ${refreshDeleted ?? 0} refresh token(s), ` +
`${resetDeleted ?? 0} password reset(s)`,
);
} catch (error) {
this.logger.error(
'Token cleanup job failed',
error instanceof Error ? error.stack : String(error),
);
}
}
}
Loading