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