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