@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.
|
|
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": "
|
|
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
|
> {
|