@skroz/profile-api 1.0.21 → 1.0.24

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`
@@ -0,0 +1,6 @@
1
+ import { BaseEntity as TypeORMBaseEntity } from 'typeorm';
2
+ export declare abstract class TypeOrmBaseEntity extends TypeORMBaseEntity {
3
+ id: number;
4
+ createdAt: Date;
5
+ updatedAt: Date;
6
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TypeOrmBaseEntity = void 0;
13
+ const typeorm_1 = require("typeorm");
14
+ const type_graphql_1 = require("type-graphql");
15
+ let TypeOrmBaseEntity = class TypeOrmBaseEntity extends typeorm_1.BaseEntity {
16
+ };
17
+ exports.TypeOrmBaseEntity = TypeOrmBaseEntity;
18
+ __decorate([
19
+ (0, type_graphql_1.Field)(() => type_graphql_1.ID),
20
+ (0, typeorm_1.PrimaryGeneratedColumn)(),
21
+ __metadata("design:type", Number)
22
+ ], TypeOrmBaseEntity.prototype, "id", void 0);
23
+ __decorate([
24
+ (0, type_graphql_1.Field)(),
25
+ (0, typeorm_1.CreateDateColumn)({ type: 'timestamptz' }),
26
+ __metadata("design:type", Date)
27
+ ], TypeOrmBaseEntity.prototype, "createdAt", void 0);
28
+ __decorate([
29
+ (0, type_graphql_1.Field)(),
30
+ (0, typeorm_1.UpdateDateColumn)({ type: 'timestamptz' }),
31
+ __metadata("design:type", Date)
32
+ ], TypeOrmBaseEntity.prototype, "updatedAt", void 0);
33
+ exports.TypeOrmBaseEntity = TypeOrmBaseEntity = __decorate([
34
+ (0, type_graphql_1.ObjectType)({ isAbstract: true })
35
+ ], TypeOrmBaseEntity);
@@ -1,12 +1,9 @@
1
- import { BaseEntity as TypeORMBaseEntity } from 'typeorm';
2
- export declare abstract class TypeOrmBaseUser extends TypeORMBaseEntity {
1
+ import { TypeOrmBaseEntity } from './TypeOrmBaseEntity';
2
+ export declare abstract class TypeOrmBaseUser extends TypeOrmBaseEntity {
3
3
  static config: {
4
4
  isOnlineSeconds: number;
5
5
  isOnlineRecentlySeconds: number;
6
6
  };
7
- id: number;
8
- createdAt: Date;
9
- updatedAt: Date;
10
7
  email: string | null;
11
8
  password: string;
12
9
  isTempPassword: boolean;
@@ -13,7 +13,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.TypeOrmBaseUser = void 0;
14
14
  const typeorm_1 = require("typeorm");
15
15
  const type_graphql_1 = require("type-graphql");
16
- let TypeOrmBaseUser = TypeOrmBaseUser_1 = class TypeOrmBaseUser extends typeorm_1.BaseEntity {
16
+ const TypeOrmBaseEntity_1 = require("./TypeOrmBaseEntity");
17
+ let TypeOrmBaseUser = TypeOrmBaseUser_1 = class TypeOrmBaseUser extends TypeOrmBaseEntity_1.TypeOrmBaseEntity {
17
18
  checkOnline(seconds) {
18
19
  if (!this.lastSeenAt)
19
20
  return false;
@@ -33,21 +34,6 @@ TypeOrmBaseUser.config = {
33
34
  isOnlineSeconds: 60,
34
35
  isOnlineRecentlySeconds: 300,
35
36
  };
36
- __decorate([
37
- (0, type_graphql_1.Field)(() => type_graphql_1.ID),
38
- (0, typeorm_1.PrimaryGeneratedColumn)(),
39
- __metadata("design:type", Number)
40
- ], TypeOrmBaseUser.prototype, "id", void 0);
41
- __decorate([
42
- (0, type_graphql_1.Field)(() => type_graphql_1.GraphQLTimestamp),
43
- (0, typeorm_1.CreateDateColumn)({ type: 'timestamptz' }),
44
- __metadata("design:type", Date)
45
- ], TypeOrmBaseUser.prototype, "createdAt", void 0);
46
- __decorate([
47
- (0, type_graphql_1.Field)(() => type_graphql_1.GraphQLTimestamp),
48
- (0, typeorm_1.UpdateDateColumn)({ type: 'timestamptz' }),
49
- __metadata("design:type", Date)
50
- ], TypeOrmBaseUser.prototype, "updatedAt", void 0);
51
37
  __decorate([
52
38
  (0, typeorm_1.Column)({ type: 'varchar', nullable: true, unique: true }),
53
39
  __metadata("design:type", Object)
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types';
2
2
  export * from './adapters/TypeOrmProfileAdapter';
3
+ export * from './entities/TypeOrmBaseEntity';
3
4
  export * from './entities/TypeOrmBaseUser';
4
5
  export * from './services/ProfileAuthService';
5
6
  export * from './services/ProfileEmailService';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types"), exports);
18
18
  __exportStar(require("./adapters/TypeOrmProfileAdapter"), exports);
19
+ __exportStar(require("./entities/TypeOrmBaseEntity"), exports);
19
20
  __exportStar(require("./entities/TypeOrmBaseUser"), exports);
20
21
  __exportStar(require("./services/ProfileAuthService"), exports);
21
22
  __exportStar(require("./services/ProfileEmailService"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skroz/profile-api",
3
- "version": "1.0.21",
3
+ "version": "1.0.24",
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": "4d48bd69998725da84ce966a6ab383fb5eb208cc"
47
+ "gitHead": "b3531fbaf9016d5f69ec9a0d55dd789635c276aa"
48
48
  }
@@ -0,0 +1,22 @@
1
+ import {
2
+ BaseEntity as TypeORMBaseEntity,
3
+ CreateDateColumn,
4
+ PrimaryGeneratedColumn,
5
+ UpdateDateColumn,
6
+ } from 'typeorm';
7
+ import { Field, ID, ObjectType } from 'type-graphql';
8
+
9
+ @ObjectType({ isAbstract: true })
10
+ export abstract class TypeOrmBaseEntity extends TypeORMBaseEntity {
11
+ @Field(() => ID)
12
+ @PrimaryGeneratedColumn()
13
+ public id!: number;
14
+
15
+ @Field()
16
+ @CreateDateColumn({ type: 'timestamptz' })
17
+ public createdAt!: Date;
18
+
19
+ @Field()
20
+ @UpdateDateColumn({ type: 'timestamptz' })
21
+ public updatedAt!: Date;
22
+ }
@@ -1,31 +1,14 @@
1
- import {
2
- Column,
3
- CreateDateColumn,
4
- PrimaryGeneratedColumn,
5
- UpdateDateColumn,
6
- BaseEntity as TypeORMBaseEntity,
7
- } from 'typeorm';
8
- import { Field, ID, ObjectType, GraphQLTimestamp } from 'type-graphql';
1
+ import { Column } from 'typeorm';
2
+ import { Field, ObjectType, GraphQLTimestamp } from 'type-graphql';
3
+ import { TypeOrmBaseEntity } from './TypeOrmBaseEntity';
9
4
 
10
5
  @ObjectType({ isAbstract: true })
11
- export abstract class TypeOrmBaseUser extends TypeORMBaseEntity {
6
+ export abstract class TypeOrmBaseUser extends TypeOrmBaseEntity {
12
7
  public static config = {
13
8
  isOnlineSeconds: 60,
14
9
  isOnlineRecentlySeconds: 300,
15
10
  };
16
11
 
17
- @Field(() => ID)
18
- @PrimaryGeneratedColumn()
19
- public id!: number;
20
-
21
- @Field(() => GraphQLTimestamp)
22
- @CreateDateColumn({ type: 'timestamptz' })
23
- public createdAt!: Date;
24
-
25
- @Field(() => GraphQLTimestamp)
26
- @UpdateDateColumn({ type: 'timestamptz' })
27
- public updatedAt!: Date;
28
-
29
12
  @Column({ type: 'varchar', nullable: true, unique: true })
30
13
  public email!: string | null;
31
14
 
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types';
2
2
  export * from './adapters/TypeOrmProfileAdapter';
3
+ export * from './entities/TypeOrmBaseEntity';
3
4
  export * from './entities/TypeOrmBaseUser';
4
5
  export * from './services/ProfileAuthService';
5
6
  export * from './services/ProfileEmailService';