perf(api): горячие пути OIDC и батч-запросы

- кэш importJWK: не импортировать RSA-ключ на каждый /oauth/userinfo
- Cache-Control для /.well-known/openid-configuration и jwks.json
- индекс refresh_sessions(token_hash) — было full scan на каждом authorize/logout
- GET /admin/users: 3 SELECT на пользователя → батч-запросы (N+1)
- revokeAllSessionsForUser: цикл UPDATE → один UPDATE
- preCompressed статики (файлы готовит Dockerfile)
This commit is contained in:
Denozordec
2026-09-23 14:51:14 +07:00
parent fd7d9fd13c
commit 87f35631b4
7 changed files with 121 additions and 38 deletions
+1
View File
@@ -91,6 +91,7 @@ export function migrateSchema(sqlite: Sqlite): void {
CREATE INDEX IF NOT EXISTS idx_user_apps_user ON user_apps(user_id);
CREATE INDEX IF NOT EXISTS idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_sessions_user ON refresh_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_sessions_token ON refresh_sessions(token_hash);
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_user_id, created_at);
+38 -9
View File
@@ -1,4 +1,4 @@
import { eq } from 'drizzle-orm'
import { and, eq, gt, isNull } from 'drizzle-orm'
import { createHash, randomUUID } from 'node:crypto'
import type { AppDb } from './index.js'
import {
@@ -51,6 +51,30 @@ export function getUserPermissions(db: AppDb, userId: string): string[] {
.map((r) => r.permission)
}
/** Apps всех пользователей одним запросом — для списков без N+1. */
export function listUserAppsBatch(db: AppDb): Map<string, string[]> {
const rows = db.select().from(userApps).all()
const byUser = new Map<string, string[]>()
for (const row of rows) {
const list = byUser.get(row.userId)
if (list) list.push(row.appId)
else byUser.set(row.userId, [row.appId])
}
return byUser
}
/** Permissions всех пользователей одним запросом — для списков без N+1. */
export function listUserPermissionsBatch(db: AppDb): Map<string, string[]> {
const rows = db.select().from(userPermissions).all()
const byUser = new Map<string, string[]>()
for (const row of rows) {
const list = byUser.get(row.userId)
if (list) list.push(row.permission)
else byUser.set(row.userId, [row.permission])
}
return byUser
}
export function createUser(
db: AppDb,
input: {
@@ -263,12 +287,17 @@ export function revokeSessionById(db: AppDb, sessionId: string): boolean {
export function revokeAllSessionsForUser(db: AppDb, userId: string): number {
const now = new Date().toISOString()
const active = listActiveSessions(db, { userId })
for (const s of active) {
db.update(refreshSessions)
.set({ revokedAt: now })
.where(eq(refreshSessions.id, s.id))
.run()
}
return active.length
// Активная сессия = не отозвана и не истекла; один UPDATE вместо цикла.
const result = db
.update(refreshSessions)
.set({ revokedAt: now })
.where(
and(
eq(refreshSessions.userId, userId),
isNull(refreshSessions.revokedAt),
gt(refreshSessions.expiresAt, now),
),
)
.run()
return result.changes
}
+14 -1
View File
@@ -1,4 +1,4 @@
import { and, eq, lt } from 'drizzle-orm'
import { and, eq, lt, sql } from 'drizzle-orm'
import { randomUUID } from 'node:crypto'
import type { AppDb } from './index.js'
import { webauthnChallenges, webauthnCredentials } from './schema/index.js'
@@ -85,6 +85,19 @@ export function countWebauthnCredentials(db: AppDb, userId: string): number {
return listWebauthnCredentials(db, userId).length
}
/** Число passkey-ов по всем пользователям одним GROUP BY — для списков без N+1. */
export function countWebauthnCredentialsBatch(db: AppDb): Map<string, number> {
const rows = db
.select({
userId: webauthnCredentials.userId,
total: sql<number>`count(*)`,
})
.from(webauthnCredentials)
.groupBy(webauthnCredentials.userId)
.all()
return new Map(rows.map((r) => [r.userId, Number(r.total)]))
}
export function getWebauthnCredentialById(
db: AppDb,
id: string,