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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user