@skroz/profile-api 1.0.19 → 1.0.21

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.
@@ -1,12 +1,28 @@
1
1
  import { ProfileAuthService } from '../services/ProfileAuthService';
2
2
  import { OAuthProvider } from '../oauth/OAuthProvider';
3
3
  import { ProfileContext } from '../types';
4
+ type RedisClient = {
5
+ get(key: string): Promise<string | null>;
6
+ setex(key: string, ttl: number, value: string): Promise<any>;
7
+ del(key: string): Promise<any>;
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>;
4
15
  export interface OauthResolverDependencies<TContext extends ProfileContext = ProfileContext> {
5
16
  authService: ProfileAuthService | ((ctx: TContext) => ProfileAuthService);
6
17
  userType: any;
7
18
  providers: Record<string, OAuthProvider>;
8
19
  telegramBotToken?: string;
20
+ /** Имя Telegram-бота (без @) для авторизации через бота (bot deep link) */
21
+ telegramBotName?: string;
22
+ /** Redis-клиент для авторизации через бота */
23
+ redis?: RedisClient;
9
24
  onUserCreated?: (user: any, ctx: TContext) => Promise<void>;
10
25
  onLogin?: (user: any, ctx: TContext) => Promise<void>;
11
26
  }
12
27
  export declare function createOauthResolver<TContext extends ProfileContext = ProfileContext>(deps: OauthResolverDependencies<TContext>): any;
28
+ export {};
@@ -35,11 +35,57 @@ 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");
41
42
  const type_graphql_1 = require("type-graphql");
42
43
  const OauthInput_1 = require("../dto/OauthInput");
44
+ // Префикс Redis-ключей намеренно длинный, чтобы исключить коллизии с любыми
45
+ // другими ключами в том же Redis-инстансе.
46
+ const TELEGRAM_BOT_AUTH_REDIS_PREFIX = 'skroz:profile:tgbotauth';
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
+ }
63
+ let TelegramBotAuthLinkPayload = class TelegramBotAuthLinkPayload {
64
+ };
65
+ __decorate([
66
+ (0, type_graphql_1.Field)(),
67
+ __metadata("design:type", String)
68
+ ], TelegramBotAuthLinkPayload.prototype, "token", void 0);
69
+ __decorate([
70
+ (0, type_graphql_1.Field)(),
71
+ __metadata("design:type", String)
72
+ ], TelegramBotAuthLinkPayload.prototype, "url", void 0);
73
+ TelegramBotAuthLinkPayload = __decorate([
74
+ (0, type_graphql_1.ObjectType)()
75
+ ], TelegramBotAuthLinkPayload);
76
+ let TelegramBotAuthConfirmResult = class TelegramBotAuthConfirmResult {
77
+ };
78
+ __decorate([
79
+ (0, type_graphql_1.Field)(),
80
+ __metadata("design:type", Boolean)
81
+ ], TelegramBotAuthConfirmResult.prototype, "confirmed", void 0);
82
+ __decorate([
83
+ (0, type_graphql_1.Field)(),
84
+ __metadata("design:type", Boolean)
85
+ ], TelegramBotAuthConfirmResult.prototype, "expired", void 0);
86
+ TelegramBotAuthConfirmResult = __decorate([
87
+ (0, type_graphql_1.ObjectType)()
88
+ ], TelegramBotAuthConfirmResult);
43
89
  function validateTelegramHash(data, botToken) {
44
90
  const { hash } = data, rest = __rest(data, ["hash"]);
45
91
  const sorted = Object.keys(rest)
@@ -55,7 +101,7 @@ function validateTelegramHash(data, botToken) {
55
101
  return hmac === hash;
56
102
  }
57
103
  function createOauthResolver(deps) {
58
- const { authService, userType, providers, telegramBotToken, onUserCreated, onLogin, } = deps;
104
+ const { authService, userType, providers, telegramBotToken, telegramBotName, redis, onUserCreated, onLogin, } = deps;
59
105
  const getAuthService = (ctx) => {
60
106
  if (typeof authService === 'function')
61
107
  return authService(ctx);
@@ -136,6 +182,50 @@ function createOauthResolver(deps) {
136
182
  return user;
137
183
  });
138
184
  }
185
+ generateTelegramAuthToken() {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ if (!telegramBotName)
188
+ throw new Error('telegramBotName is not configured');
189
+ if (!redis)
190
+ throw new Error('redis is not configured');
191
+ const token = (0, nanoid_1.nanoid)(32);
192
+ const redisKey = `${TELEGRAM_BOT_AUTH_REDIS_PREFIX}:${token}`;
193
+ yield redis.setex(redisKey, TELEGRAM_BOT_AUTH_TOKEN_TTL_SECONDS, 'pending');
194
+ const url = `https://t.me/${telegramBotName}?start=tgauth_${token}`;
195
+ return { token, url };
196
+ });
197
+ }
198
+ confirmTelegramAuthToken(token, ctx) {
199
+ return __awaiter(this, void 0, void 0, function* () {
200
+ if (!redis)
201
+ throw new Error('redis is not configured');
202
+ const redisKey = `${TELEGRAM_BOT_AUTH_REDIS_PREFIX}:${token}`;
203
+ const val = yield redis.get(redisKey);
204
+ if (!val) {
205
+ return { confirmed: false, expired: true };
206
+ }
207
+ if (val === 'pending') {
208
+ return { confirmed: false, expired: false };
209
+ }
210
+ const userId = parseInt(val, 10);
211
+ if (Number.isNaN(userId)) {
212
+ return { confirmed: false, expired: true };
213
+ }
214
+ const service = getAuthService(ctx);
215
+ const user = yield service.db.findUserById(userId);
216
+ if (!user) {
217
+ return { confirmed: false, expired: true };
218
+ }
219
+ const userAgent = decodeURI(ctx.req.get('user-agent') || '');
220
+ yield ctx.req.session.create({
221
+ userId: user.id,
222
+ ip: ctx.req.ip,
223
+ userAgent: userAgent.slice(0, 500),
224
+ });
225
+ yield redis.del(redisKey);
226
+ return { confirmed: true, expired: false };
227
+ });
228
+ }
139
229
  };
140
230
  __decorate([
141
231
  (0, type_graphql_1.Mutation)(() => userType),
@@ -145,6 +235,20 @@ function createOauthResolver(deps) {
145
235
  __metadata("design:paramtypes", [OauthInput_1.OauthLoginInput, Object]),
146
236
  __metadata("design:returntype", Promise)
147
237
  ], OauthResolver.prototype, "oauthLogin", null);
238
+ __decorate([
239
+ (0, type_graphql_1.Mutation)(() => TelegramBotAuthLinkPayload),
240
+ __metadata("design:type", Function),
241
+ __metadata("design:paramtypes", []),
242
+ __metadata("design:returntype", Promise)
243
+ ], OauthResolver.prototype, "generateTelegramAuthToken", null);
244
+ __decorate([
245
+ (0, type_graphql_1.Mutation)(() => TelegramBotAuthConfirmResult),
246
+ __param(0, (0, type_graphql_1.Arg)('token')),
247
+ __param(1, (0, type_graphql_1.Ctx)()),
248
+ __metadata("design:type", Function),
249
+ __metadata("design:paramtypes", [String, Object]),
250
+ __metadata("design:returntype", Promise)
251
+ ], OauthResolver.prototype, "confirmTelegramAuthToken", null);
148
252
  OauthResolver = __decorate([
149
253
  (0, type_graphql_1.Resolver)(() => userType)
150
254
  ], OauthResolver);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skroz/profile-api",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
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": "ad9d511d8f623ad1a5d3a6a83e016c4b12d4f211"
47
+ "gitHead": "4d48bd69998725da84ce966a6ab383fb5eb208cc"
48
48
  }
@@ -1,11 +1,57 @@
1
1
  import crypto from 'crypto';
2
2
  import { nanoid } from 'nanoid';
3
- import { Arg, Ctx, Mutation, Resolver } from 'type-graphql';
3
+ import { Arg, Ctx, Mutation, ObjectType, Field, Resolver } from 'type-graphql';
4
4
  import { ProfileAuthService } from '../services/ProfileAuthService';
5
5
  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
+
15
+ // Префикс Redis-ключей намеренно длинный, чтобы исключить коллизии с любыми
16
+ // другими ключами в том же Redis-инстансе.
17
+ const TELEGRAM_BOT_AUTH_REDIS_PREFIX = 'skroz:profile:tgbotauth';
18
+ const TELEGRAM_BOT_AUTH_TOKEN_TTL_SECONDS = 300; // 5 минут
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
+
37
+ @ObjectType()
38
+ class TelegramBotAuthLinkPayload {
39
+ @Field()
40
+ token!: string;
41
+
42
+ @Field()
43
+ url!: string;
44
+ }
45
+
46
+ @ObjectType()
47
+ class TelegramBotAuthConfirmResult {
48
+ @Field()
49
+ confirmed!: boolean;
50
+
51
+ @Field()
52
+ expired!: boolean;
53
+ }
54
+
9
55
  export interface OauthResolverDependencies<
10
56
  TContext extends ProfileContext = ProfileContext
11
57
  > {
@@ -13,6 +59,10 @@ export interface OauthResolverDependencies<
13
59
  userType: any;
14
60
  providers: Record<string, OAuthProvider>;
15
61
  telegramBotToken?: string;
62
+ /** Имя Telegram-бота (без @) для авторизации через бота (bot deep link) */
63
+ telegramBotName?: string;
64
+ /** Redis-клиент для авторизации через бота */
65
+ redis?: RedisClient;
16
66
  onUserCreated?: (user: any, ctx: TContext) => Promise<void>;
17
67
  onLogin?: (user: any, ctx: TContext) => Promise<void>;
18
68
  }
@@ -43,6 +93,8 @@ export function createOauthResolver<
43
93
  userType,
44
94
  providers,
45
95
  telegramBotToken,
96
+ telegramBotName,
97
+ redis,
46
98
  onUserCreated,
47
99
  onLogin,
48
100
  } = deps;
@@ -137,6 +189,58 @@ export function createOauthResolver<
137
189
 
138
190
  return user;
139
191
  }
192
+
193
+ @Mutation(() => TelegramBotAuthLinkPayload)
194
+ async generateTelegramAuthToken(): Promise<TelegramBotAuthLinkPayload> {
195
+ if (!telegramBotName) throw new Error('telegramBotName is not configured');
196
+ if (!redis) throw new Error('redis is not configured');
197
+
198
+ const token = nanoid(32);
199
+ const redisKey = `${TELEGRAM_BOT_AUTH_REDIS_PREFIX}:${token}`;
200
+ await redis.setex(redisKey, TELEGRAM_BOT_AUTH_TOKEN_TTL_SECONDS, 'pending');
201
+ const url = `https://t.me/${telegramBotName}?start=tgauth_${token}`;
202
+ return { token, url };
203
+ }
204
+
205
+ @Mutation(() => TelegramBotAuthConfirmResult)
206
+ async confirmTelegramAuthToken(
207
+ @Arg('token') token: string,
208
+ @Ctx() ctx: TContext
209
+ ): Promise<TelegramBotAuthConfirmResult> {
210
+ if (!redis) throw new Error('redis is not configured');
211
+
212
+ const redisKey = `${TELEGRAM_BOT_AUTH_REDIS_PREFIX}:${token}`;
213
+ const val = await redis.get(redisKey);
214
+
215
+ if (!val) {
216
+ return { confirmed: false, expired: true };
217
+ }
218
+
219
+ if (val === 'pending') {
220
+ return { confirmed: false, expired: false };
221
+ }
222
+
223
+ const userId = parseInt(val, 10);
224
+ if (Number.isNaN(userId)) {
225
+ return { confirmed: false, expired: true };
226
+ }
227
+
228
+ const service = getAuthService(ctx);
229
+ const user = await service.db.findUserById(userId);
230
+ if (!user) {
231
+ return { confirmed: false, expired: true };
232
+ }
233
+
234
+ const userAgent = decodeURI(ctx.req.get('user-agent') || '');
235
+ await ctx.req.session.create({
236
+ userId: user.id,
237
+ ip: ctx.req.ip,
238
+ userAgent: userAgent.slice(0, 500),
239
+ });
240
+ await redis.del(redisKey);
241
+
242
+ return { confirmed: true, expired: false };
243
+ }
140
244
  }
141
245
 
142
246
  return OauthResolver;