@skroz/profile-api
v1.0.40
Published
GraphQL-резолверы и сервисы для аутентификации пользователей. Построена на type-graphql + TypeORM + Redis.
Downloads
1,851
Readme
@skroz/profile-api
GraphQL-резолверы и сервисы для аутентификации пользователей. Построена на type-graphql + TypeORM + Redis.
Архитектура
Библиотека предоставляет фабрики резолверов — функции, которые принимают зависимости и возвращают класс резолвера для регистрации в GraphQL-схеме. Это позволяет переиспользовать логику в разных приложениях с разными User-сущностями.
Билд
cd libs/utils/packages/profile-api
yarn buildЭкспорты
Типы
AuthUser— интерфейс пользователя:id,email,name,avatar,password,isEmailConfirmed,isBanned,isTempPassword,urlSlug,telegramId,isEmailNotificationEnabled,isTelegramNotificationEnabled,lastSeenAt,save()ProfileDbAdapter— интерфейс БД-адаптера, который нужно реализовать:findUserByEmail,findUserById,findUserByTelegramId,createUser,isEmailTaken,findUserByProviderId,updateUserProviderIdProfileAuthConfig— конфиг токенов/лимитов:resendEmailLimitSeconds,confirmationTokenLifetimeMinutes,recoveryTokenLifetimeMinutesEmailConfig— extends ProfileAuthConfig + настройки email:domain,websiteUrl,primaryBrandColor,logoUrl,fromEmailUsername,templateDir,isOnlineSeconds,isOnlineRecentlySecondsProfileEmailTemplate— enum шаблонов писем:CONFIRM_EMAIL,FORGOT_PASSWORD,TEMP_PASSWORD,GENERIC_NOTIFICATIONProfileLocales— интерфейс для всех текстов ошибок и email-шаблонов (лежит вsrc/locales/types.ts)profileRu,profileEn— готовые объекты локализацииProfileContext<TUser>— GraphQL-контекст:user?,req,res,t
TypeOrmProfileAdapter
Готовая реализация ProfileDbAdapter поверх TypeORM.
import { TypeOrmProfileAdapter } from '@skroz/profile-api';
const db = new TypeOrmProfileAdapter(() =>
getConnection().getRepository(User)
);Провайдер-ID ищет по полю ${provider}Id (т.е. googleId, vkId, telegramId и т.д.) — эти поля должны быть в сущности User.
TypeOrmBaseUser
Базовая TypeORM-сущность со всеми полями AuthUser. Можно наследовать:
import { TypeOrmBaseUser } from '@skroz/profile-api';
@Entity()
export class User extends TypeOrmBaseUser {
// дополнительные поля
}ProfileAuthService
Основной сервис. Принимает (db, email, redis, config).
import { ProfileAuthService } from '@skroz/profile-api';
const authService = new ProfileAuthService(db, emailService, redis, {
resendEmailLimitSeconds: 60,
confirmationTokenLifetimeMinutes: 60,
recoveryTokenLifetimeMinutes: 30,
});Публичные поля: db, email, redis, config.
Методы: hashPassword, verifyPassword, setTokenToRedis, removeTokenFromRedis, getUserByToken, sendLink.
ProfileEmailService
Отправка писем (confirm email, forgot password, temp password). Передаётся в ProfileAuthService.
Резолверы
Все резолверы создаются через фабричные функции. authService в deps может быть как экземпляром, так и функцией (ctx) => ProfileAuthService (для per-request сервисов).
createAuthResolver(deps)
Auth-мутации: register, login, logout, confirmEmail, sendToken, recoverPassword.
import { createAuthResolver } from '@skroz/profile-api';
const AuthResolver = createAuthResolver({
authService, // ProfileAuthService | (ctx) => ProfileAuthService
userType: User, // GraphQL ObjectType
onUserCreated: async (user, ctx) => { /* ... */ },
onLogin: async (user, ctx) => { /* ... */ },
onLogout: async (user, ctx) => { /* ... */ },
onEmailConfirmed: async (user, ctx) => { /* ... */ },
onPasswordRecovered: async (user, ctx) => { /* ... */ },
logTelegramBot: { sendError: async (msg) => { /* ... */ } },
});createProfileResolver(deps)
Мутации профиля (только для авторизованных): updateEmail, updatePassword, updateProfile, toggleEmailNotification.
import { createProfileResolver } from '@skroz/profile-api';
const ProfileResolver = createProfileResolver({ authService, userType: User });createOauthResolver(deps)
OAuth-авторизация и вход через Telegram-бот.
Мутации:
oauthLogin(input: OauthLoginInput)— вход через Google/VK/Яндекс/Mail/Apple/Telegram-виджетgenerateTelegramAuthToken→{ token, url }— генерирует токен и deep link для ботаconfirmTelegramAuthToken(token)→{ confirmed, expired }— polling со стороны фронта
import { createOauthResolver, GoogleOauth, VKOauth } from '@skroz/profile-api';
const OauthResolver = createOauthResolver({
authService,
userType: User,
providers: {
google: new GoogleOauth(clientId, clientSecret, redirectUri),
vk: new VKOauth(clientId, clientSecret, redirectUri),
// ya, mail, apple — аналогично
},
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN, // для Telegram Login Widget
telegramBotName: 'MyBot', // имя бота без @, для deep link авторизации
redis: getRedis(), // клиент с методами get/setex/del
onUserCreated: async (user, ctx) => { /* ... */ },
onLogin: async (user, ctx) => { /* ... */ },
});confirmTelegramBotAuth
Вспомогательная функция для Telegram-бота (не для GraphQL-резолвера).
Когда пользователь переходит по deep link /start tgauth_<token>, бот вызывает эту функцию — она записывает telegramUserId в Redis, и фронт получает confirmed: true при следующем polling.
import { confirmTelegramBotAuth } from '@skroz/profile-api';
// В обработчике команды /start в Telegram-боте:
if (text.startsWith('/start tgauth_')) {
const token = text.replace('/start tgauth_', '');
const confirmed = await confirmTelegramBotAuth(redis, token, user.id);
if (confirmed) {
// отправить пользователю сообщение об успешной авторизации
}
}Возвращает true если токен найден и подтверждён, false если истёк или не существует.
Использует тот же Redis-префикс (skroz:profile:tgbotauth) и TTL (300 сек), что и generateTelegramAuthToken.
OAuth-провайдеры
import { GoogleOauth, VKOauth, YandexOauth, MailOauth, AppleOauth } from '@skroz/profile-api';Каждый провайдер реализует интерфейс OAuthProvider:
interface OAuthProvider {
exchangeCode(code: string): Promise<OAuthProfile>;
readonly trustedEmail: boolean; // true = email верифицирован провайдером
}
interface OAuthProfile {
providerId: string;
email?: string | null;
name?: string | null;
avatarUrl?: string | null;
}trustedEmail: true у Google, VK, Яндекс, Mail, Apple — при входе через них isEmailConfirmed и isEmailNotificationEnabled автоматически выставляются в true.
Система уведомлений (Activity + Notification)
Переиспользуемая система событий: Activity — лента для UI, Notification — очередь доставки по каналам.
Архитектура
Activity (лента событий)
type: varchar ← проектный enum, хранится строкой
toUser ← получатель
fromUser? ← инициатор (null для системных событий)
payload: jsonb ← доп. данные (сумма, название и т.п.)
isViewed: boolean ← для счётчика непросмотренных
Notification (очередь доставки)
toUser, way, activity
← после отправки УДАЛЯЕТСЯ (не обновляется)
← таблица всегда маленькая, индексы быстрые1. Базовые entity
import { TypeOrmBaseActivity, TypeOrmBaseNotification } from '@skroz/profile-api';
enum ActivityType { PAYMENT_COMPLETED = 'PAYMENT_COMPLETED', NEW_SCENE = 'NEW_SCENE' }
registerEnumType(ActivityType, { name: 'ActivityType' });
@ObjectType()
@Entity('activities')
class Activity extends TypeOrmBaseActivity {
@Field(() => ActivityType)
public type!: ActivityType; // переопределяем тип для GraphQL
// дополнительные @ManyToOne если нужны в шаблонах
}
@ObjectType()
@Index('IDX_NOTIFICATIONS_TO_USER_WAY', ['toUser', 'way'])
@Entity('notifications')
class Notification extends TypeOrmBaseNotification {}2. Конфигурация каналов
import { NotificationChannels, NotificationSegments, NotificationWay } from '@skroz/profile-api';
// Центр истины: какой ActivityType → в какие каналы.
// Record<ActivityType, ...> гарантирует покрытие всех значений enum.
const NOTIFICATION_CHANNELS: NotificationChannels<ActivityType> = {
[ActivityType.PAYMENT_COMPLETED]: [NotificationWay.TELEGRAM, NotificationWay.EMAIL],
[ActivityType.NEW_SCENE]: [NotificationWay.TELEGRAM],
};
// Группировка для cron-джоба
const NOTIFICATION_SEGMENTS: NotificationSegments<ActivityType> = {
MAIN: [ActivityType.PAYMENT_COMPLETED, ActivityType.NEW_SCENE],
};3. Создание уведомлений при событии
import { createNotificationsForActivity } from '@skroz/profile-api';
// Вызывается после сохранения Activity:
await createNotificationsForActivity(
activity,
toUser, // уже загруженный User
NOTIFICATION_CHANNELS,
Notification, // конкретный класс проекта
// extraWayCheck — опционально, для PUSH-подписок:
async (user, way) => {
if (way !== NotificationWay.PUSH) return false;
const count = await PushSubscription.count({ where: { user } });
return count > 0;
},
);isWayAvailable проверяется автоматически перед созданием Notification-записи.
Первым делом для любого канала проверяется isBanned || isDeleted — если true, запись не создаётся.
- TELEGRAM:
telegramIdзадан +isTelegramNotificationEnabled = true - EMAIL:
emailзадан +isEmailConfirmed+isEmailNotificationEnabled - POPUP: пользователь онлайн (через
checkOnlineс порогом изTypeOrmBaseUser.config) - PUSH: в либе всегда возвращает
false— требует реализации в проекте
PUSH не реализован в либе намеренно: он требует проверки наличия подписок в БД (таблица PushSubscription или аналог), которой нет в либе. Реализуется через extraWayCheck:
await createNotificationsForActivity(
activity,
toUser,
NOTIFICATION_CHANNELS,
Notification,
async (user, way) => {
if (way !== NotificationWay.PUSH) return false;
const count = await getConnection()
.getRepository(PushSubscription)
.count({ where: { user } });
return count > 0;
},
);Если extraWayCheck не передан, PUSH-уведомления не создаются совсем — это безопасный дефолт.
4. Cron-джоб
import { createNotificationJob } from '@skroz/profile-api';
const notificationJob = createNotificationJob({
NotificationEntity: Notification,
cronTime: '0/30 * * * * *', // каждые 30 сек
channels: NOTIFICATION_CHANNELS,
segments: NOTIFICATION_SEGMENTS,
telegramHandler: async (notifications, segmentKey) => {
// отправить, вернуть activityId успешных
const doneIds: number[] = [];
for (const n of notifications) {
const ok = await TelegramService.sendNotification(n);
if (ok) doneIds.push(n.activityId);
}
return doneIds;
},
emailHandler: async (notifications, segmentKey) => {
return EmailService.sendNotifications(notifications);
},
emailSilencePeriodMs: 6 * 60 * 60 * 1000, // 6 часов между письмами одному юзеру
emailLastSeenThresholdMs: 30 * 60 * 1000, // не слать если онлайн < 30 мин назад
telegramBatchSize: 10,
emailBatchSize: 50,
activityJoins: [
{ property: 'Activity.message', alias: 'Message' }, // для шаблонов
],
onError: (e) => TelegramService.sendToAdmin({ text: String(e) }),
});
notificationJob.start();После успешной отправки Notification-запись удаляется — cron-джоб вызывает deleteNotificationsByActivityIds автоматически.
NotificationWay enum
import { NotificationWay } from '@skroz/profile-api';
// TELEGRAM | EMAIL | POPUP | PUSHРегистрируется в GraphQL-схеме автоматически при импорте.
peerDependencies
Система уведомлений требует cron >= 2.0.0 (peerDependency библиотеки).
DTO
GraphQL input/output классы для использования в схеме:
AuthInput, UpdateEmailInput, UpdatePasswordInput, UpdateProfileInput,
ConfirmEmailInput, RecoverPasswordInput, SendTokenInput, SendTokenPayload,
OauthLoginInput, TelegramAuthData
Локализация
Библиотека поставляется с готовыми переводами для всех ошибок валидации и шаблонов писем.
Использование готовых локалей
Импортируйте объекты локализации и подмешайте их в ресурсы вашего i18next. Обычно это делается в файлах src/locales/validation/ru.ts вашего приложения:
import { profileRu } from '@skroz/profile-api';
import { ValidationTranslations } from './types';
const ru: ValidationTranslations = {
...profileRu,
story: { ... }, // Специфичные для проекта переводы
};Добавление новых языков
Для добавления нового языка (например, испанского) используйте интерфейс ProfileLocales:
import { ProfileLocales, FieldValidation } from '@skroz/profile-api';
const emailFields: FieldValidation = {
isEmail: 'Formato de correo electrónico no válido',
};
export const profileEs: ProfileLocales = {
auth: {
emailConfirmed: 'Correo electrónico confirmado con éxito',
emailExists: 'Este correo electrónico ya está en uso',
},
// ... все поля будут подсвечены TypeScript
};ProfileEmailService и локали
Поскольку письма могут отправляться из фоновых задач (cron), где нет контекста запроса с функцией t, ProfileEmailService требует объект локалей в конструкторе:
const emailService = new ProfileEmailService(config, profileRu);