AI Implementation feature(820): Project Scaffold and Platform Foundations (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-07-21 14:23:53 +00:00
parent e55c9cda56
commit 03bcb6b156
131 changed files with 23657 additions and 0 deletions
@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { SettingsService } from '../../modules/settings/settings.module';
export type EventStatusName = 'Stopped' | 'Running';
export interface EventStatus {
status: EventStatusName;
startUtc: string;
endUtc: string;
serverNowUtc: string;
countdownMs: number;
}
@Injectable()
export class EventStatusService {
constructor(private readonly settings: SettingsService) {}
async getStatus(now: Date = new Date()): Promise<EventStatus> {
const startStr = await this.settings.get('eventStartUtc');
const endStr = await this.settings.get('eventEndUtc');
const start = new Date(startStr);
const end = new Date(endStr);
const nowMs = now.getTime();
const startMs = start.getTime();
const endMs = end.getTime();
let status: EventStatusName = 'Stopped';
let countdownMs = 0;
if (nowMs < startMs) {
status = 'Stopped';
countdownMs = startMs - nowMs;
} else if (nowMs <= endMs) {
status = 'Running';
countdownMs = endMs - nowMs;
} else {
status = 'Stopped';
countdownMs = 0;
}
return {
status,
startUtc: start.toISOString(),
endUtc: end.toISOString(),
serverNowUtc: now.toISOString(),
countdownMs,
};
}
}
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
const MAX_FAILS = 5;
const BACKOFF_MS = [1_000, 5_000, 30_000, 120_000, 600_000, 1_800_000];
@Injectable()
export class LoginBackoffService {
private counters = new Map<string, { count: number; blockedUntil: number }>();
private key(ip: string, username: string): string {
return `${ip}::${username}`;
}
isBlocked(ip: string, username: string, now: number = Date.now()): number {
const k = this.key(ip, username);
const entry = this.counters.get(k);
if (!entry) return 0;
if (entry.blockedUntil > now) return entry.blockedUntil - now;
return 0;
}
recordFailure(ip: string, username: string, now: number = Date.now()): void {
const k = this.key(ip, username);
const entry = this.counters.get(k) ?? { count: 0, blockedUntil: 0 };
entry.count++;
if (entry.count > MAX_FAILS) {
const idx = Math.min(entry.count - MAX_FAILS - 1, BACKOFF_MS.length - 1);
entry.blockedUntil = now + BACKOFF_MS[idx];
} else if (entry.count === MAX_FAILS) {
entry.blockedUntil = now + BACKOFF_MS[0];
}
this.counters.set(k, entry);
}
reset(ip: string, username: string): void {
this.counters.delete(this.key(ip, username));
}
}
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
const WINDOW_MS = 60_000;
const MAX_PER_WINDOW = 10;
@Injectable()
export class RegistrationRateLimitService {
private hits: Map<string, number[]> = new Map();
isAllowed(ip: string, now: number = Date.now()): boolean {
const arr = (this.hits.get(ip) ?? []).filter((t) => now - t < WINDOW_MS);
this.hits.set(ip, arr);
return arr.length < MAX_PER_WINDOW;
}
record(ip: string, now: number = Date.now()): void {
const arr = (this.hits.get(ip) ?? []).filter((t) => now - t < WINDOW_MS);
arr.push(now);
this.hits.set(ip, arr);
}
}
@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { Subject, Observable } from 'rxjs';
@Injectable()
export class SseHubService {
private readonly eventSubject = new Subject<any>();
private readonly scoreboardSubject = new Subject<any>();
emitEvent(payload: any): void {
this.eventSubject.next(payload);
}
emitScoreboard(payload: any): void {
this.scoreboardSubject.next(payload);
}
event$(): Observable<any> {
return this.eventSubject.asObservable();
}
scoreboard$(): Observable<any> {
return this.scoreboardSubject.asObservable();
}
}