mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 20:29:42 +02:00
feat: GPU offload, signup limit, load tests & capacity planning
- Route all AI workloads (Ollama, STT, TTS, Image Gen) to GPU server (192.168.178.11) via LAN instead of host.docker.internal - Upgrade default model to gemma3:12b and max concurrent to 5 - Add daily signup limit service (MAX_DAILY_SIGNUPS env var) - Add GET /api/v1/auth/signup-status public endpoint - Add k6 load test suite (web-apps, auth, sync-websocket, ollama) - Add capacity planning documentation - Fix: add eslint-config to sveltekit-base and calendar Dockerfiles Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
16367384c7
commit
9276d9a212
12 changed files with 683 additions and 14 deletions
|
|
@ -15,6 +15,7 @@ import { jwtAuth } from './middleware/jwt-auth';
|
|||
import { serviceAuth } from './middleware/service-auth';
|
||||
import { initializeEmail } from './email/send';
|
||||
import { SecurityEventsService, AccountLockoutService } from './services/security';
|
||||
import { SignupLimitService } from './services/signup-limit';
|
||||
import { ApiKeysService } from './services/api-keys';
|
||||
import { createAuthRoutes } from './routes/auth';
|
||||
import { createGuildRoutes } from './routes/guilds';
|
||||
|
|
@ -31,6 +32,7 @@ const auth = createBetterAuth(config.databaseUrl);
|
|||
initializeEmail(config.smtp);
|
||||
const security = new SecurityEventsService(db);
|
||||
const lockout = new AccountLockoutService(db);
|
||||
const signupLimit = new SignupLimitService(db);
|
||||
const apiKeysService = new ApiKeysService(db);
|
||||
|
||||
// ─── App ────────────────────────────────────────────────────
|
||||
|
|
@ -61,7 +63,7 @@ app.get('/.well-known/openid-configuration', async (c) => auth.handler(c.req.raw
|
|||
|
||||
// ─── Custom Auth Endpoints ──────────────────────────────────
|
||||
|
||||
app.route('/api/v1/auth', createAuthRoutes(auth, config, security, lockout));
|
||||
app.route('/api/v1/auth', createAuthRoutes(auth, config, security, lockout, signupLimit));
|
||||
|
||||
// ─── Guilds ─────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Hono } from 'hono';
|
|||
import type { AuthUser } from '../middleware/jwt-auth';
|
||||
import type { BetterAuthInstance } from '../auth/better-auth.config';
|
||||
import type { SecurityEventsService, AccountLockoutService } from '../services/security';
|
||||
import type { SignupLimitService } from '../services/signup-limit';
|
||||
import type { Config } from '../config';
|
||||
import { sourceAppStore, passwordResetRedirectStore } from '../auth/stores';
|
||||
|
||||
|
|
@ -16,15 +17,37 @@ export function createAuthRoutes(
|
|||
auth: BetterAuthInstance,
|
||||
config: Config,
|
||||
security: SecurityEventsService,
|
||||
lockout: AccountLockoutService
|
||||
lockout: AccountLockoutService,
|
||||
signupLimit: SignupLimitService
|
||||
) {
|
||||
const app = new Hono<{ Variables: { user: AuthUser } }>();
|
||||
|
||||
// ─── Registration ────────────────────────────────────────
|
||||
|
||||
// ─── Signup Status (public) ─────────────────────────────
|
||||
|
||||
app.get('/signup-status', async (c) => {
|
||||
const status = await signupLimit.getStatus();
|
||||
return c.json(status);
|
||||
});
|
||||
|
||||
app.post('/register', async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
// Check daily signup limit
|
||||
const limitCheck = await signupLimit.checkLimit();
|
||||
if (!limitCheck.allowed) {
|
||||
return c.json(
|
||||
{
|
||||
error: 'Registration limit reached',
|
||||
message: 'Das tägliche Registrierungslimit ist erreicht. Versuche es morgen wieder.',
|
||||
spotsRemaining: 0,
|
||||
resetsAt: limitCheck.resetsAt,
|
||||
},
|
||||
429
|
||||
);
|
||||
}
|
||||
|
||||
// Store source app URL for email verification redirect
|
||||
if (body.sourceAppUrl && body.email) {
|
||||
sourceAppStore.set(body.email, body.sourceAppUrl);
|
||||
|
|
|
|||
93
services/mana-auth/src/services/signup-limit.ts
Normal file
93
services/mana-auth/src/services/signup-limit.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Signup Limit — Daily registration cap ("Organic Growth Gate")
|
||||
*
|
||||
* Limits new registrations per day to protect hardware and
|
||||
* enable organic growth. Uses PostgreSQL security_events table
|
||||
* (no Redis dependency needed).
|
||||
*
|
||||
* Configure via MAX_DAILY_SIGNUPS env var (default: 0 = unlimited).
|
||||
*/
|
||||
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { Database } from '../db/connection';
|
||||
|
||||
export class SignupLimitService {
|
||||
private maxDaily: number;
|
||||
|
||||
constructor(private db: Database) {
|
||||
this.maxDaily = parseInt(process.env.MAX_DAILY_SIGNUPS || '0', 10);
|
||||
}
|
||||
|
||||
/** Check if registration is allowed right now */
|
||||
async checkLimit(): Promise<{
|
||||
allowed: boolean;
|
||||
current: number;
|
||||
limit: number;
|
||||
resetsAt: string;
|
||||
}> {
|
||||
// 0 = unlimited (feature disabled)
|
||||
if (this.maxDaily <= 0) {
|
||||
return { allowed: true, current: 0, limit: 0, resetsAt: '' };
|
||||
}
|
||||
|
||||
const todayCount = await this.getTodayCount();
|
||||
const midnight = new Date();
|
||||
midnight.setHours(24, 0, 0, 0);
|
||||
|
||||
return {
|
||||
allowed: todayCount < this.maxDaily,
|
||||
current: todayCount,
|
||||
limit: this.maxDaily,
|
||||
resetsAt: midnight.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Count registrations today (UTC) */
|
||||
private async getTodayCount(): Promise<number> {
|
||||
try {
|
||||
const result = await this.db.execute(
|
||||
sql`SELECT COUNT(*) as count
|
||||
FROM auth.security_events
|
||||
WHERE event_type = 'REGISTER'
|
||||
AND created_at >= CURRENT_DATE
|
||||
AND created_at < CURRENT_DATE + INTERVAL '1 day'`
|
||||
);
|
||||
const row = (result as any)[0];
|
||||
return row ? Number(row.count) : 0;
|
||||
} catch {
|
||||
// On error, allow registration (fail open)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Public status for the signup page */
|
||||
async getStatus(): Promise<{
|
||||
registrationOpen: boolean;
|
||||
spotsRemaining: number | null;
|
||||
totalToday: number;
|
||||
limit: number;
|
||||
resetsAt: string;
|
||||
}> {
|
||||
if (this.maxDaily <= 0) {
|
||||
return {
|
||||
registrationOpen: true,
|
||||
spotsRemaining: null,
|
||||
totalToday: 0,
|
||||
limit: 0,
|
||||
resetsAt: '',
|
||||
};
|
||||
}
|
||||
|
||||
const todayCount = await this.getTodayCount();
|
||||
const midnight = new Date();
|
||||
midnight.setHours(24, 0, 0, 0);
|
||||
|
||||
return {
|
||||
registrationOpen: todayCount < this.maxDaily,
|
||||
spotsRemaining: Math.max(0, this.maxDaily - todayCount),
|
||||
totalToday: todayCount,
|
||||
limit: this.maxDaily,
|
||||
resetsAt: midnight.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue