feat: синхронизация бэкенда с документацией (AGENT_TASK_BACKEND_SYNC)

- Добвлен @fastify/cookie и настройку httpOnly cookie для refresh token
- Добавлен префикс /api/v1 для auth, profile, tests, admin
- Скорректировано в Login: возвращать user (id, nickname, avatarUrl, role, emailVerified),
  ставить refreshToken в Set-Cookie
- Скорректировано в Logout: Bearer + cookie, пустое тело, 200 + { message }, очищать cookie
- Скорректировано в Refresh: token из cookie, пустое тело, 200 + { accessToken }, Set-Cookie
- Добавлено в getPrivateProfile: поля role и plan
- Скорректировано в Tests: score = количество правильных, ответ { score, totalQuestions, percentage }
- Добавлено в question_cache_meta: поля valid, retryCount, questionsGenerated
- Обновлены тесты
This commit is contained in:
Anton
2026-03-06 13:58:34 +03:00
parent 99a2686532
commit 223feed0e0
21 changed files with 2244 additions and 62 deletions

View File

@@ -1,7 +1,7 @@
import { eq } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import type * as schema from '../../db/schema/index.js';
import { users, userStats } from '../../db/schema/index.js';
import { users, userStats, subscriptions } from '../../db/schema/index.js';
import { notFound, conflict, ERROR_CODES } from '../../utils/errors.js';
import type { User } from '../../db/schema/users.js';
import type { SelfLevel } from '../../db/schema/index.js';
@@ -50,6 +50,8 @@ export type PrivateProfile = PublicProfile & {
emailVerifiedAt: string | null;
createdAt: string;
updatedAt: string;
role: string;
plan: 'free' | 'pro';
};
async function getStatsForUser(db: Db, userId: string): Promise<ProfileStats> {
@@ -88,13 +90,19 @@ function toPublicProfile(user: User, stats: ProfileStats): PublicProfile {
};
}
function toPrivateProfile(user: User, stats: ProfileStats): PrivateProfile {
function toPrivateProfile(
user: User,
stats: ProfileStats,
plan: 'free' | 'pro' = 'free'
): PrivateProfile {
return {
...toPublicProfile(user, stats),
email: user.email,
emailVerifiedAt: user.emailVerifiedAt?.toISOString() ?? null,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
role: user.role,
plan,
};
}
@@ -115,12 +123,15 @@ export class UserService {
return user ?? null;
}
async getPrivateProfile(userId: string): Promise<PrivateProfile> {
async getPrivateProfile(
userId: string,
plan: 'free' | 'pro' = 'free'
): Promise<PrivateProfile> {
const [user, stats] = await Promise.all([this.getById(userId), getStatsForUser(this.db, userId)]);
if (!user) {
throw notFound('User not found');
}
return toPrivateProfile(user, stats);
return toPrivateProfile(user, stats, plan);
}
async getPublicProfile(username: string): Promise<PublicProfile> {
@@ -170,6 +181,18 @@ export class UserService {
}
const stats = await getStatsForUser(this.db, userId);
return toPrivateProfile(updated, stats);
const plan = await this.getPlanForUser(userId);
return toPrivateProfile(updated, stats, plan);
}
async getPlanForUser(userId: string): Promise<'free' | 'pro'> {
const [sub] = await this.db
.select()
.from(subscriptions)
.where(eq(subscriptions.userId, userId))
.limit(1);
if (!sub || (sub.expiresAt && sub.expiresAt < new Date())) return 'free';
if (sub.plan === 'pro' && (sub.status === 'active' || sub.status === 'trialing')) return 'pro';
return 'free';
}
}