@skroz/profile-api 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # @skroz/profile-api
2
+
3
+ GraphQL-резолверы и сервисы для аутентификации пользователей. Построена на type-graphql + TypeORM + Redis.
4
+
5
+ ## Архитектура
6
+
7
+ Библиотека предоставляет **фабрики резолверов** — функции, которые принимают зависимости и возвращают класс резолвера для регистрации в GraphQL-схеме. Это позволяет переиспользовать логику в разных приложениях с разными User-сущностями.
8
+
9
+ ## Билд
10
+
11
+ ```bash
12
+ cd libs/utils/packages/profile-api
13
+ yarn build
14
+ ```
15
+
16
+ ## Экспорты
17
+
18
+ ### Типы
19
+
20
+ - **`AuthUser`** — интерфейс пользователя: `id`, `email`, `name`, `avatar`, `password`, `isEmailConfirmed`, `isBanned`, `isTempPassword`, `urlSlug`, `telegramId`, `isEmailNotificationEnabled`, `isTelegramNotificationEnabled`, `lastSeenAt`, `save()`
21
+ - **`ProfileDbAdapter`** — интерфейс БД-адаптера, который нужно реализовать: `findUserByEmail`, `findUserById`, `findUserByTelegramId`, `createUser`, `isEmailTaken`, `findUserByProviderId`, `updateUserProviderId`
22
+ - **`ProfileAuthConfig`** — конфиг токенов/лимитов: `resendEmailLimitSeconds`, `confirmationTokenLifetimeMinutes`, `recoveryTokenLifetimeMinutes`
23
+ - **`EmailConfig`** — extends ProfileAuthConfig + настройки email: `domain`, `websiteUrl`, `primaryBrandColor`, `logoUrl`, `fromEmailUsername`, `templateDir`, `isOnlineSeconds`, `isOnlineRecentlySeconds`
24
+ - **`ProfileEmailTemplate`** — enum шаблонов писем: `CONFIRM_EMAIL`, `FORGOT_PASSWORD`, `TEMP_PASSWORD`, `GENERIC_NOTIFICATION`
25
+ - **`ProfileLocales`** — структура переводов для auth-ошибок и email-шаблонов
26
+ - **`ProfileContext<TUser>`** — GraphQL-контекст: `user?`, `req`, `res`, `t`
27
+
28
+ ### TypeOrmProfileAdapter
29
+
30
+ Готовая реализация `ProfileDbAdapter` поверх TypeORM.
31
+
32
+ ```ts
33
+ import { TypeOrmProfileAdapter } from '@skroz/profile-api';
34
+
35
+ const db = new TypeOrmProfileAdapter(() =>
36
+ getConnection().getRepository(User)
37
+ );
38
+ ```
39
+
40
+ Провайдер-ID ищет по полю `${provider}Id` (т.е. `googleId`, `vkId`, `telegramId` и т.д.) — эти поля должны быть в сущности User.
41
+
42
+ ### TypeOrmBaseUser
43
+
44
+ Базовая TypeORM-сущность со всеми полями `AuthUser`. Можно наследовать:
45
+
46
+ ```ts
47
+ import { TypeOrmBaseUser } from '@skroz/profile-api';
48
+
49
+ @Entity()
50
+ export class User extends TypeOrmBaseUser {
51
+ // дополнительные поля
52
+ }
53
+ ```
54
+
55
+ ### ProfileAuthService
56
+
57
+ Основной сервис. Принимает `(db, email, redis, config)`.
58
+
59
+ ```ts
60
+ import { ProfileAuthService } from '@skroz/profile-api';
61
+
62
+ const authService = new ProfileAuthService(db, emailService, redis, {
63
+ resendEmailLimitSeconds: 60,
64
+ confirmationTokenLifetimeMinutes: 60,
65
+ recoveryTokenLifetimeMinutes: 30,
66
+ });
67
+ ```
68
+
69
+ Публичные поля: `db`, `email`, `redis`, `config`.
70
+ Методы: `hashPassword`, `verifyPassword`, `setTokenToRedis`, `removeTokenFromRedis`, `getUserByToken`, `sendLink`.
71
+
72
+ ### ProfileEmailService
73
+
74
+ Отправка писем (confirm email, forgot password, temp password). Передаётся в `ProfileAuthService`.
75
+
76
+ ---
77
+
78
+ ## Резолверы
79
+
80
+ Все резолверы создаются через фабричные функции. `authService` в deps может быть как экземпляром, так и функцией `(ctx) => ProfileAuthService` (для per-request сервисов).
81
+
82
+ ### createAuthResolver(deps)
83
+
84
+ Auth-мутации: `register`, `login`, `logout`, `confirmEmail`, `sendToken`, `recoverPassword`.
85
+
86
+ ```ts
87
+ import { createAuthResolver } from '@skroz/profile-api';
88
+
89
+ const AuthResolver = createAuthResolver({
90
+ authService, // ProfileAuthService | (ctx) => ProfileAuthService
91
+ userType: User, // GraphQL ObjectType
92
+ onUserCreated: async (user, ctx) => { /* ... */ },
93
+ onLogin: async (user, ctx) => { /* ... */ },
94
+ onLogout: async (user, ctx) => { /* ... */ },
95
+ onEmailConfirmed: async (user, ctx) => { /* ... */ },
96
+ onPasswordRecovered: async (user, ctx) => { /* ... */ },
97
+ logTelegramBot: { sendError: async (msg) => { /* ... */ } },
98
+ });
99
+ ```
100
+
101
+ ### createProfileResolver(deps)
102
+
103
+ Мутации профиля (только для авторизованных): `updateEmail`, `updatePassword`, `updateProfile`, `toggleEmailNotification`.
104
+
105
+ ```ts
106
+ import { createProfileResolver } from '@skroz/profile-api';
107
+
108
+ const ProfileResolver = createProfileResolver({ authService, userType: User });
109
+ ```
110
+
111
+ ### createOauthResolver(deps)
112
+
113
+ OAuth-авторизация и вход через Telegram-бот.
114
+
115
+ Мутации:
116
+ - `oauthLogin(input: OauthLoginInput)` — вход через Google/VK/Яндекс/Mail/Apple/Telegram-виджет
117
+ - `generateTelegramAuthToken` → `{ token, url }` — генерирует токен и deep link для бота
118
+ - `confirmTelegramAuthToken(token)` → `{ confirmed, expired }` — polling со стороны фронта
119
+
120
+ ```ts
121
+ import { createOauthResolver, GoogleOauth, VKOauth } from '@skroz/profile-api';
122
+
123
+ const OauthResolver = createOauthResolver({
124
+ authService,
125
+ userType: User,
126
+ providers: {
127
+ google: new GoogleOauth(clientId, clientSecret, redirectUri),
128
+ vk: new VKOauth(clientId, clientSecret, redirectUri),
129
+ // ya, mail, apple — аналогично
130
+ },
131
+ telegramBotToken: process.env.TELEGRAM_BOT_TOKEN, // для Telegram Login Widget
132
+ telegramBotName: 'MyBot', // имя бота без @, для deep link авторизации
133
+ redis: getRedis(), // клиент с методами get/setex/del
134
+ onUserCreated: async (user, ctx) => { /* ... */ },
135
+ onLogin: async (user, ctx) => { /* ... */ },
136
+ });
137
+ ```
138
+
139
+ ---
140
+
141
+ ## confirmTelegramBotAuth
142
+
143
+ Вспомогательная функция для **Telegram-бота** (не для GraphQL-резолвера).
144
+
145
+ Когда пользователь переходит по deep link `/start tgauth_<token>`, бот вызывает эту функцию — она записывает `telegramUserId` в Redis, и фронт получает `confirmed: true` при следующем polling.
146
+
147
+ ```ts
148
+ import { confirmTelegramBotAuth } from '@skroz/profile-api';
149
+
150
+ // В обработчике команды /start в Telegram-боте:
151
+ if (text.startsWith('/start tgauth_')) {
152
+ const token = text.replace('/start tgauth_', '');
153
+ const confirmed = await confirmTelegramBotAuth(redis, token, user.id);
154
+ if (confirmed) {
155
+ // отправить пользователю сообщение об успешной авторизации
156
+ }
157
+ }
158
+ ```
159
+
160
+ Возвращает `true` если токен найден и подтверждён, `false` если истёк или не существует.
161
+ Использует тот же Redis-префикс (`skroz:profile:tgbotauth`) и TTL (300 сек), что и `generateTelegramAuthToken`.
162
+
163
+ ---
164
+
165
+ ## OAuth-провайдеры
166
+
167
+ ```ts
168
+ import { GoogleOauth, VKOauth, YandexOauth, MailOauth, AppleOauth } from '@skroz/profile-api';
169
+ ```
170
+
171
+ Каждый провайдер реализует интерфейс `OAuthProvider`:
172
+
173
+ ```ts
174
+ interface OAuthProvider {
175
+ exchangeCode(code: string): Promise<OAuthProfile>;
176
+ readonly trustedEmail: boolean; // true = email верифицирован провайдером
177
+ }
178
+
179
+ interface OAuthProfile {
180
+ providerId: string;
181
+ email?: string | null;
182
+ name?: string | null;
183
+ avatarUrl?: string | null;
184
+ }
185
+ ```
186
+
187
+ `trustedEmail: true` у Google, VK, Яндекс, Mail, Apple — при входе через них `isEmailConfirmed` и `isEmailNotificationEnabled` автоматически выставляются в `true`.
188
+
189
+ ---
190
+
191
+ ## DTO
192
+
193
+ GraphQL input/output классы для использования в схеме:
194
+
195
+ `AuthInput`, `UpdateEmailInput`, `UpdatePasswordInput`, `UpdateProfileInput`,
196
+ `ConfirmEmailInput`, `RecoverPasswordInput`, `SendTokenInput`, `SendTokenPayload`,
197
+ `OauthLoginInput`, `TelegramAuthData`
@@ -6,6 +6,12 @@ type RedisClient = {
6
6
  setex(key: string, ttl: number, value: string): Promise<any>;
7
7
  del(key: string): Promise<any>;
8
8
  };
9
+ /**
10
+ * Вызывается ботом при получении /start tgauth_<token>.
11
+ * Записывает telegramUserId в Redis, подтверждая авторизацию.
12
+ * Возвращает true если токен найден и подтверждён, false если истёк или не существует.
13
+ */
14
+ export declare function confirmTelegramBotAuth(redis: RedisClient, token: string, telegramUserId: number | string): Promise<boolean>;
9
15
  export interface OauthResolverDependencies<TContext extends ProfileContext = ProfileContext> {
10
16
  authService: ProfileAuthService | ((ctx: TContext) => ProfileAuthService);
11
17
  userType: any;
@@ -35,6 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.confirmTelegramBotAuth = confirmTelegramBotAuth;
38
39
  exports.createOauthResolver = createOauthResolver;
39
40
  const crypto_1 = __importDefault(require("crypto"));
40
41
  const nanoid_1 = require("nanoid");
@@ -44,6 +45,21 @@ const OauthInput_1 = require("../dto/OauthInput");
44
45
  // другими ключами в том же Redis-инстансе.
45
46
  const TELEGRAM_BOT_AUTH_REDIS_PREFIX = 'skroz:profile:tgbotauth';
46
47
  const TELEGRAM_BOT_AUTH_TOKEN_TTL_SECONDS = 300; // 5 минут
48
+ /**
49
+ * Вызывается ботом при получении /start tgauth_<token>.
50
+ * Записывает telegramUserId в Redis, подтверждая авторизацию.
51
+ * Возвращает true если токен найден и подтверждён, false если истёк или не существует.
52
+ */
53
+ function confirmTelegramBotAuth(redis, token, telegramUserId) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ const redisKey = `${TELEGRAM_BOT_AUTH_REDIS_PREFIX}:${token}`;
56
+ const val = yield redis.get(redisKey);
57
+ if (val !== 'pending')
58
+ return false;
59
+ yield redis.setex(redisKey, TELEGRAM_BOT_AUTH_TOKEN_TTL_SECONDS, String(telegramUserId));
60
+ return true;
61
+ });
62
+ }
47
63
  let TelegramBotAuthLinkPayload = class TelegramBotAuthLinkPayload {
48
64
  };
49
65
  __decorate([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skroz/profile-api",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "license": "MIT",
5
5
  "repository": "git@gitlab.com:skroz/libs/utils.git",
6
6
  "main": "dist/index.js",
@@ -44,5 +44,5 @@
44
44
  "type-graphql": "^1.1.1",
45
45
  "typeorm": "^0.2.45"
46
46
  },
47
- "gitHead": "7613b73a6980c945cb2ef597747f74855bc2cbfb"
47
+ "gitHead": "b76953b60d5790c28dd02476788130a62c255a94"
48
48
  }
@@ -6,11 +6,34 @@ import { OAuthProvider } from '../oauth/OAuthProvider';
6
6
  import { OauthLoginInput, TelegramAuthData } from '../dto/OauthInput';
7
7
  import { ProfileContext } from '../types';
8
8
 
9
+ type RedisClient = {
10
+ get(key: string): Promise<string | null>;
11
+ setex(key: string, ttl: number, value: string): Promise<any>;
12
+ del(key: string): Promise<any>;
13
+ };
14
+
9
15
  // Префикс Redis-ключей намеренно длинный, чтобы исключить коллизии с любыми
10
16
  // другими ключами в том же Redis-инстансе.
11
17
  const TELEGRAM_BOT_AUTH_REDIS_PREFIX = 'skroz:profile:tgbotauth';
12
18
  const TELEGRAM_BOT_AUTH_TOKEN_TTL_SECONDS = 300; // 5 минут
13
19
 
20
+ /**
21
+ * Вызывается ботом при получении /start tgauth_<token>.
22
+ * Записывает telegramUserId в Redis, подтверждая авторизацию.
23
+ * Возвращает true если токен найден и подтверждён, false если истёк или не существует.
24
+ */
25
+ export async function confirmTelegramBotAuth(
26
+ redis: RedisClient,
27
+ token: string,
28
+ telegramUserId: number | string
29
+ ): Promise<boolean> {
30
+ const redisKey = `${TELEGRAM_BOT_AUTH_REDIS_PREFIX}:${token}`;
31
+ const val = await redis.get(redisKey);
32
+ if (val !== 'pending') return false;
33
+ await redis.setex(redisKey, TELEGRAM_BOT_AUTH_TOKEN_TTL_SECONDS, String(telegramUserId));
34
+ return true;
35
+ }
36
+
14
37
  @ObjectType()
15
38
  class TelegramBotAuthLinkPayload {
16
39
  @Field()
@@ -29,12 +52,6 @@ class TelegramBotAuthConfirmResult {
29
52
  expired!: boolean;
30
53
  }
31
54
 
32
- type RedisClient = {
33
- get(key: string): Promise<string | null>;
34
- setex(key: string, ttl: number, value: string): Promise<any>;
35
- del(key: string): Promise<any>;
36
- };
37
-
38
55
  export interface OauthResolverDependencies<
39
56
  TContext extends ProfileContext = ProfileContext
40
57
  > {