npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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, updateUserProviderId
  • ProfileAuthConfig — конфиг токенов/лимитов: resendEmailLimitSeconds, confirmationTokenLifetimeMinutes, recoveryTokenLifetimeMinutes
  • EmailConfig — extends ProfileAuthConfig + настройки email: domain, websiteUrl, primaryBrandColor, logoUrl, fromEmailUsername, templateDir, isOnlineSeconds, isOnlineRecentlySeconds
  • ProfileEmailTemplate — enum шаблонов писем: CONFIRM_EMAIL, FORGOT_PASSWORD, TEMP_PASSWORD, GENERIC_NOTIFICATION
  • ProfileLocales — интерфейс для всех текстов ошибок и 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);