@skroz/profile-api 1.0.16 → 1.0.18

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.
Files changed (44) hide show
  1. package/dist/adapters/TypeOrmProfileAdapter.d.ts +4 -1
  2. package/dist/adapters/TypeOrmProfileAdapter.js +15 -1
  3. package/dist/dto/OauthInput.d.ts +14 -0
  4. package/dist/dto/OauthInput.js +65 -0
  5. package/dist/dto/index.d.ts +1 -0
  6. package/dist/dto/index.js +4 -1
  7. package/dist/entities/TypeOrmBaseUser.d.ts +5 -0
  8. package/dist/entities/TypeOrmBaseUser.js +20 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +2 -0
  11. package/dist/oauth/AppleOauth.d.ts +13 -0
  12. package/dist/oauth/AppleOauth.js +84 -0
  13. package/dist/oauth/GoogleOauth.d.ts +12 -0
  14. package/dist/oauth/GoogleOauth.js +54 -0
  15. package/dist/oauth/MailOauth.d.ts +12 -0
  16. package/dist/oauth/MailOauth.js +53 -0
  17. package/dist/oauth/OAuthProvider.d.ts +9 -0
  18. package/dist/oauth/OAuthProvider.js +2 -0
  19. package/dist/oauth/VKOauth.d.ts +12 -0
  20. package/dist/oauth/VKOauth.js +47 -0
  21. package/dist/oauth/YandexOauth.d.ts +11 -0
  22. package/dist/oauth/YandexOauth.js +56 -0
  23. package/dist/oauth/index.d.ts +6 -0
  24. package/dist/oauth/index.js +22 -0
  25. package/dist/resolvers/ProfileResolver.js +5 -3
  26. package/dist/resolvers/createOauthResolver.d.ts +12 -0
  27. package/dist/resolvers/createOauthResolver.js +146 -0
  28. package/dist/types/index.d.ts +4 -1
  29. package/package.json +3 -2
  30. package/src/adapters/TypeOrmProfileAdapter.ts +13 -2
  31. package/src/dto/OauthInput.ts +37 -0
  32. package/src/dto/index.ts +1 -0
  33. package/src/entities/TypeOrmBaseUser.ts +15 -0
  34. package/src/index.ts +2 -0
  35. package/src/oauth/AppleOauth.ts +83 -0
  36. package/src/oauth/GoogleOauth.ts +47 -0
  37. package/src/oauth/MailOauth.ts +49 -0
  38. package/src/oauth/OAuthProvider.ts +10 -0
  39. package/src/oauth/VKOauth.ts +43 -0
  40. package/src/oauth/YandexOauth.ts +49 -0
  41. package/src/oauth/index.ts +6 -0
  42. package/src/resolvers/ProfileResolver.ts +8 -6
  43. package/src/resolvers/createOauthResolver.ts +136 -0
  44. package/src/types/index.ts +3 -1
@@ -0,0 +1,146 @@
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
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
15
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
16
+ return new (P || (P = Promise))(function (resolve, reject) {
17
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
18
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
19
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
20
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
21
+ });
22
+ };
23
+ var __rest = (this && this.__rest) || function (s, e) {
24
+ var t = {};
25
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
26
+ t[p] = s[p];
27
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
28
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
29
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
30
+ t[p[i]] = s[p[i]];
31
+ }
32
+ return t;
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.createOauthResolver = createOauthResolver;
39
+ const crypto_1 = __importDefault(require("crypto"));
40
+ const nanoid_1 = require("nanoid");
41
+ const type_graphql_1 = require("type-graphql");
42
+ const OauthInput_1 = require("../dto/OauthInput");
43
+ function validateTelegramHash(data, botToken) {
44
+ const { hash } = data, rest = __rest(data, ["hash"]);
45
+ const sorted = Object.keys(rest)
46
+ .filter((k) => {
47
+ const value = rest[k];
48
+ return value !== undefined && value !== null && value !== '';
49
+ })
50
+ .sort()
51
+ .map((k) => `${k}=${rest[k]}`)
52
+ .join('\n');
53
+ const secret = crypto_1.default.createHash('sha256').update(botToken).digest();
54
+ const hmac = crypto_1.default.createHmac('sha256', secret).update(sorted).digest('hex');
55
+ return hmac === hash;
56
+ }
57
+ function createOauthResolver(deps) {
58
+ const { authService, userType, providers, telegramBotToken, onUserCreated, onLogin, } = deps;
59
+ const getAuthService = (ctx) => {
60
+ if (typeof authService === 'function')
61
+ return authService(ctx);
62
+ return authService;
63
+ };
64
+ let OauthResolver = class OauthResolver {
65
+ oauthLogin(input, ctx) {
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ const service = getAuthService(ctx);
68
+ let user;
69
+ let isNew = false;
70
+ if (input.provider === 'telegram') {
71
+ if (!input.tgData)
72
+ throw new Error('tgData is required for Telegram login');
73
+ if (!telegramBotToken)
74
+ throw new Error('Telegram bot token is not configured');
75
+ if (!validateTelegramHash(input.tgData, telegramBotToken)) {
76
+ throw new Error('Telegram auth verification failed');
77
+ }
78
+ user = yield service.db.findUserByTelegramId(input.tgData.id);
79
+ if (!user) {
80
+ const passwordHash = yield service.hashPassword((0, nanoid_1.nanoid)(20));
81
+ user = yield service.db.createUser({
82
+ email: null,
83
+ passwordHash,
84
+ isTempPassword: true,
85
+ });
86
+ yield service.db.updateUserProviderId(user.id, 'telegram', input.tgData.id);
87
+ isNew = true;
88
+ }
89
+ }
90
+ else {
91
+ if (!input.code)
92
+ throw new Error('code is required for OAuth login');
93
+ const provider = providers[input.provider];
94
+ if (!provider)
95
+ throw new Error(`Unknown OAuth provider: ${input.provider}`);
96
+ const profile = yield provider.exchangeCode(input.code);
97
+ // 1. Find by provider ID
98
+ user = yield service.db.findUserByProviderId(input.provider, profile.providerId);
99
+ if (!user && profile.email) {
100
+ // 2. Find by email — link provider ID to existing account
101
+ user = yield service.db.findUserByEmail(profile.email);
102
+ if (user) {
103
+ yield service.db.updateUserProviderId(user.id, input.provider, profile.providerId);
104
+ }
105
+ }
106
+ if (!user) {
107
+ // 3. Create new user
108
+ const passwordHash = yield service.hashPassword((0, nanoid_1.nanoid)(20));
109
+ user = yield service.db.createUser({
110
+ email: profile.email,
111
+ passwordHash,
112
+ isTempPassword: true,
113
+ });
114
+ yield service.db.updateUserProviderId(user.id, input.provider, profile.providerId);
115
+ isNew = true;
116
+ }
117
+ }
118
+ if (user.isBanned)
119
+ throw new Error('User is banned');
120
+ const userAgent = decodeURI(ctx.req.get('user-agent') || '');
121
+ yield ctx.req.session.create({
122
+ userId: user.id,
123
+ ip: ctx.req.ip,
124
+ userAgent: userAgent.slice(0, 500),
125
+ });
126
+ if (isNew && onUserCreated)
127
+ yield onUserCreated(user, ctx);
128
+ if (!isNew && onLogin)
129
+ yield onLogin(user, ctx);
130
+ return user;
131
+ });
132
+ }
133
+ };
134
+ __decorate([
135
+ (0, type_graphql_1.Mutation)(() => userType),
136
+ __param(0, (0, type_graphql_1.Arg)('input')),
137
+ __param(1, (0, type_graphql_1.Ctx)()),
138
+ __metadata("design:type", Function),
139
+ __metadata("design:paramtypes", [OauthInput_1.OauthLoginInput, Object]),
140
+ __metadata("design:returntype", Promise)
141
+ ], OauthResolver.prototype, "oauthLogin", null);
142
+ OauthResolver = __decorate([
143
+ (0, type_graphql_1.Resolver)(() => userType)
144
+ ], OauthResolver);
145
+ return OauthResolver;
146
+ }
@@ -19,10 +19,13 @@ export interface ProfileDbAdapter {
19
19
  findUserById(id: number): Promise<AuthUser | null>;
20
20
  findUserByTelegramId(telegramId: string): Promise<AuthUser | null>;
21
21
  createUser(data: {
22
- email: string;
22
+ email?: string | null;
23
23
  passwordHash: string;
24
+ isTempPassword?: boolean;
24
25
  }): Promise<AuthUser>;
25
26
  isEmailTaken(email: string, excludeUserId?: number): Promise<boolean>;
27
+ findUserByProviderId(provider: string, id: string): Promise<AuthUser | null>;
28
+ updateUserProviderId(userId: number, provider: string, id: string): Promise<void>;
26
29
  }
27
30
  export interface ProfileAuthConfig {
28
31
  resendEmailLimitSeconds: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skroz/profile-api",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "license": "MIT",
5
5
  "repository": "git@gitlab.com:skroz/libs/utils.git",
6
6
  "main": "dist/index.js",
@@ -26,6 +26,7 @@
26
26
  "@os-team/session": "1.0.32",
27
27
  "argon2": "0.30.2",
28
28
  "class-validator": "0.13.2",
29
+ "isomorphic-unfetch": "4.0.2",
29
30
  "nanoid": "3.3.4",
30
31
  "pug": "3.0.2"
31
32
  },
@@ -43,5 +44,5 @@
43
44
  "type-graphql": "^1.1.1",
44
45
  "typeorm": "^0.2.45"
45
46
  },
46
- "gitHead": "20236f7b5f15fc83912ce95bea870d52b7ac0667"
47
+ "gitHead": "a074f427f040b742ba51a9a23942893a3c3b2cf6"
47
48
  }
@@ -20,11 +20,12 @@ export class TypeOrmProfileAdapter implements ProfileDbAdapter {
20
20
  return this.repo.findOne({ where: { telegramId } }) as Promise<AuthUser | null>;
21
21
  }
22
22
 
23
- async createUser(data: { email: string; passwordHash: string }): Promise<AuthUser> {
23
+ async createUser(data: { email?: string | null; passwordHash: string; isTempPassword?: boolean }): Promise<AuthUser> {
24
24
  const user = this.repo.create({
25
- email: data.email.toLowerCase(),
25
+ email: data.email ? data.email.toLowerCase() : null,
26
26
  password: data.passwordHash,
27
27
  isEmailConfirmed: false,
28
+ isTempPassword: data.isTempPassword ?? false,
28
29
  });
29
30
  return (await this.repo.save(user)) as AuthUser;
30
31
  }
@@ -37,4 +38,14 @@ export class TypeOrmProfileAdapter implements ProfileDbAdapter {
37
38
  const count = await this.repo.count({ where });
38
39
  return count > 0;
39
40
  }
41
+
42
+ async findUserByProviderId(provider: string, id: string): Promise<AuthUser | null> {
43
+ const field = `${provider}Id`;
44
+ return this.repo.findOne({ where: { [field]: id } }) as Promise<AuthUser | null>;
45
+ }
46
+
47
+ async updateUserProviderId(userId: number, provider: string, id: string): Promise<void> {
48
+ const field = `${provider}Id`;
49
+ await this.repo.update(userId, { [field]: id } as any);
50
+ }
40
51
  }
@@ -0,0 +1,37 @@
1
+ import { Field, InputType } from 'type-graphql';
2
+
3
+ @InputType()
4
+ export class TelegramAuthData {
5
+ @Field()
6
+ public id!: string;
7
+
8
+ @Field()
9
+ public first_name!: string;
10
+
11
+ @Field({ nullable: true })
12
+ public last_name?: string;
13
+
14
+ @Field({ nullable: true })
15
+ public username?: string;
16
+
17
+ @Field({ nullable: true })
18
+ public photo_url?: string;
19
+
20
+ @Field()
21
+ public auth_date!: string;
22
+
23
+ @Field()
24
+ public hash!: string;
25
+ }
26
+
27
+ @InputType()
28
+ export class OauthLoginInput {
29
+ @Field()
30
+ public provider!: string;
31
+
32
+ @Field({ nullable: true })
33
+ public code?: string;
34
+
35
+ @Field(() => TelegramAuthData, { nullable: true })
36
+ public tgData?: TelegramAuthData;
37
+ }
package/src/dto/index.ts CHANGED
@@ -14,3 +14,4 @@ export { default as UpdateEmailInput, updateEmailTransformers, updateEmailValida
14
14
  export { default as UpdateProfileInput, updateProfileTransformers, updateProfileValidators } from './UpdateProfileInput';
15
15
  export { default as SendTokenInput, sendTokenTransformers, sendTokenValidators } from './SendTokenInput';
16
16
  export { default as RecoverPasswordInput, recoverPasswordTransformers, recoverPasswordValidators } from './RecoverPasswordInput';
17
+ export { OauthLoginInput, TelegramAuthData } from './OauthInput';
@@ -60,6 +60,21 @@ export abstract class TypeOrmBaseUser extends TypeORMBaseEntity {
60
60
  @Column({ type: 'varchar', nullable: true, unique: true })
61
61
  public telegramId!: string | null;
62
62
 
63
+ @Column({ type: 'varchar', nullable: true })
64
+ public googleId?: string | null;
65
+
66
+ @Column({ type: 'varchar', nullable: true })
67
+ public vkId?: string | null;
68
+
69
+ @Column({ type: 'varchar', nullable: true })
70
+ public yaId?: string | null;
71
+
72
+ @Column({ type: 'varchar', nullable: true })
73
+ public mailId?: string | null;
74
+
75
+ @Column({ type: 'varchar', nullable: true })
76
+ public appleId?: string | null;
77
+
63
78
  @Field(() => String, { nullable: true })
64
79
  @Column({ type: 'varchar', nullable: true })
65
80
  public avatar?: string | null;
package/src/index.ts CHANGED
@@ -6,3 +6,5 @@ export * from './services/ProfileEmailService';
6
6
  export * from './dto';
7
7
  export * from './resolvers/AuthResolver';
8
8
  export * from './resolvers/ProfileResolver';
9
+ export * from './resolvers/createOauthResolver';
10
+ export * from './oauth';
@@ -0,0 +1,83 @@
1
+ import fetch from 'isomorphic-unfetch';
2
+ import crypto from 'crypto';
3
+ import { OAuthProfile, OAuthProvider } from './OAuthProvider';
4
+
5
+ interface AppleOauthConfig {
6
+ clientId: string; // Services ID (e.g. ru.vikneska.web)
7
+ teamId: string; // Apple Developer Team ID
8
+ keyId: string; // Key ID from Apple Developer
9
+ privateKey: string; // PEM private key content (ES256)
10
+ }
11
+
12
+ function base64urlEncode(str: string | Buffer): string {
13
+ const buf = typeof str === 'string' ? Buffer.from(str) : str;
14
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
15
+ }
16
+
17
+ function generateClientSecret(config: AppleOauthConfig): string {
18
+ const now = Math.floor(Date.now() / 1000);
19
+ const header = base64urlEncode(JSON.stringify({ alg: 'ES256', kid: config.keyId }));
20
+ const payload = base64urlEncode(JSON.stringify({
21
+ iss: config.teamId,
22
+ iat: now,
23
+ exp: now + 15777000, // ~6 months
24
+ aud: 'https://appleid.apple.com',
25
+ sub: config.clientId,
26
+ }));
27
+
28
+ const signingInput = `${header}.${payload}`;
29
+ const sign = crypto.createSign('SHA256');
30
+ sign.update(signingInput);
31
+ const signature = sign.sign(config.privateKey);
32
+ // Convert DER to base64url
33
+ const signatureBase64url = base64urlEncode(signature);
34
+
35
+ return `${signingInput}.${signatureBase64url}`;
36
+ }
37
+
38
+ function decodeJwtPayload(token: string): Record<string, any> {
39
+ const parts = token.split('.');
40
+ if (parts.length < 2) throw new Error('Invalid JWT');
41
+ const payload = Buffer.from(parts[1], 'base64').toString('utf8');
42
+ return JSON.parse(payload);
43
+ }
44
+
45
+ export class AppleOauth implements OAuthProvider {
46
+ constructor(private config: AppleOauthConfig) {}
47
+
48
+ async exchangeCode(code: string): Promise<OAuthProfile> {
49
+ const clientSecret = generateClientSecret(this.config);
50
+
51
+ const tokenResponse = await fetch('https://appleid.apple.com/auth/token', {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
54
+ body: new URLSearchParams({
55
+ grant_type: 'authorization_code',
56
+ client_id: this.config.clientId,
57
+ client_secret: clientSecret,
58
+ code,
59
+ }).toString(),
60
+ });
61
+
62
+ const tokenData = await tokenResponse.json();
63
+ if (!tokenData.id_token) {
64
+ throw new Error(`Apple OAuth error: ${tokenData.error || 'no id_token'}`);
65
+ }
66
+
67
+ // Apple sends user data only on first authorization — decode from id_token
68
+ const idTokenPayload = decodeJwtPayload(tokenData.id_token);
69
+ const appleId = idTokenPayload.sub;
70
+
71
+ if (!appleId) {
72
+ throw new Error('Apple OAuth: failed to get user ID from id_token');
73
+ }
74
+
75
+ return {
76
+ providerId: appleId,
77
+ // Apple provides email only on first authorization
78
+ email: idTokenPayload.email || null,
79
+ name: null, // Apple does not provide name in id_token; handle via front-end form on first login
80
+ avatarUrl: null, // Apple does not provide avatar
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,47 @@
1
+ import fetch from 'isomorphic-unfetch';
2
+ import { OAuthProfile, OAuthProvider } from './OAuthProvider';
3
+
4
+ interface GoogleOauthConfig {
5
+ clientId: string;
6
+ clientSecret: string;
7
+ redirectUri: string;
8
+ }
9
+
10
+ export class GoogleOauth implements OAuthProvider {
11
+ constructor(private config: GoogleOauthConfig) {}
12
+
13
+ async exchangeCode(code: string): Promise<OAuthProfile> {
14
+ const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
17
+ body: new URLSearchParams({
18
+ grant_type: 'authorization_code',
19
+ client_id: this.config.clientId,
20
+ client_secret: this.config.clientSecret,
21
+ redirect_uri: this.config.redirectUri,
22
+ code,
23
+ }).toString(),
24
+ });
25
+
26
+ const tokenData = await tokenResponse.json();
27
+ if (!tokenData.access_token) {
28
+ throw new Error(`Google OAuth error: ${tokenData.error_description || tokenData.error}`);
29
+ }
30
+
31
+ const userResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
32
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
33
+ });
34
+
35
+ const user = await userResponse.json();
36
+ if (!user.sub) {
37
+ throw new Error('Google OAuth: failed to get user profile');
38
+ }
39
+
40
+ return {
41
+ providerId: user.sub,
42
+ email: user.email || null,
43
+ name: user.name || null,
44
+ avatarUrl: user.picture || null,
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,49 @@
1
+ import fetch from 'isomorphic-unfetch';
2
+ import crypto from 'crypto';
3
+ import { OAuthProfile, OAuthProvider } from './OAuthProvider';
4
+
5
+ interface MailOauthConfig {
6
+ clientId: string;
7
+ clientSecret: string;
8
+ redirectUri: string;
9
+ }
10
+
11
+ export class MailOauth implements OAuthProvider {
12
+ constructor(private config: MailOauthConfig) {}
13
+
14
+ async exchangeCode(code: string): Promise<OAuthProfile> {
15
+ const tokenResponse = await fetch('https://connect.mail.ru/oauth/token', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
18
+ body: `grant_type=authorization_code&client_id=${this.config.clientId}&client_secret=${this.config.clientSecret}&code=${code}&redirect_uri=${this.config.redirectUri}`,
19
+ });
20
+
21
+ const tokenData = await tokenResponse.json();
22
+ if (!tokenData.access_token || !tokenData.x_mailru_vid) {
23
+ throw new Error(`Mail.ru OAuth error: ${tokenData.error_description || tokenData.error || 'no token'}`);
24
+ }
25
+
26
+ const mailRuId = tokenData.x_mailru_vid;
27
+
28
+ const sig = `app_id=${this.config.clientId}method=users.getInfosecure=1uids=${mailRuId}${this.config.clientSecret}`;
29
+ const md5Sig = crypto.createHash('md5').update(sig).digest('hex');
30
+ const infoUrl = `https://www.appsmail.ru/platform/api?sig=${md5Sig}&app_id=${this.config.clientId}&method=users.getInfo&secure=1&uids=${mailRuId}`;
31
+
32
+ const userResponse = await fetch(infoUrl);
33
+ const userData = await userResponse.json();
34
+ const user = Array.isArray(userData) ? userData[0] : null;
35
+
36
+ if (!user) {
37
+ throw new Error('Mail.ru OAuth: failed to get user profile');
38
+ }
39
+
40
+ const hasAvatar = user.has_pic === 1;
41
+
42
+ return {
43
+ providerId: String(mailRuId),
44
+ email: user.email || null,
45
+ name: user.first_name || null,
46
+ avatarUrl: hasAvatar ? user.pic_big : null,
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,10 @@
1
+ export interface OAuthProfile {
2
+ providerId: string;
3
+ email?: string | null;
4
+ name?: string | null;
5
+ avatarUrl?: string | null;
6
+ }
7
+
8
+ export interface OAuthProvider {
9
+ exchangeCode(code: string): Promise<OAuthProfile>;
10
+ }
@@ -0,0 +1,43 @@
1
+ import fetch from 'isomorphic-unfetch';
2
+ import { OAuthProfile, OAuthProvider } from './OAuthProvider';
3
+
4
+ interface VKOauthConfig {
5
+ clientId: string;
6
+ clientSecret: string;
7
+ redirectUri: string;
8
+ }
9
+
10
+ export class VKOauth implements OAuthProvider {
11
+ constructor(private config: VKOauthConfig) {}
12
+
13
+ async exchangeCode(code: string): Promise<OAuthProfile> {
14
+ const tokenResponse = await fetch(
15
+ `https://oauth.vk.com/access_token?client_id=${this.config.clientId}&client_secret=${this.config.clientSecret}&redirect_uri=${this.config.redirectUri}&code=${code}`
16
+ );
17
+
18
+ const tokenData = await tokenResponse.json();
19
+ if (tokenData.error) {
20
+ throw new Error(`VK OAuth error: ${tokenData.error_description || tokenData.error}`);
21
+ }
22
+ if (!tokenData.access_token || !tokenData.user_id) {
23
+ throw new Error('VK OAuth: failed to get access token');
24
+ }
25
+
26
+ const userResponse = await fetch(
27
+ `https://api.vk.com/method/users.get?user_ids=${tokenData.user_id}&fields=photo_200&access_token=${tokenData.access_token}&v=5.131`
28
+ );
29
+
30
+ const userData = await userResponse.json();
31
+ const user = userData.response?.[0];
32
+ if (!user) {
33
+ throw new Error('VK OAuth: failed to get user profile');
34
+ }
35
+
36
+ return {
37
+ providerId: String(tokenData.user_id),
38
+ email: tokenData.email || null,
39
+ name: user.first_name || null,
40
+ avatarUrl: user.photo_200 || null,
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,49 @@
1
+ import fetch from 'isomorphic-unfetch';
2
+ import { OAuthProfile, OAuthProvider } from './OAuthProvider';
3
+
4
+ interface YandexOauthConfig {
5
+ clientId: string;
6
+ clientSecret: string;
7
+ }
8
+
9
+ export class YandexOauth implements OAuthProvider {
10
+ constructor(private config: YandexOauthConfig) {}
11
+
12
+ async exchangeCode(code: string): Promise<OAuthProfile> {
13
+ const tokenResponse = await fetch('https://oauth.yandex.ru/token', {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
16
+ body: `grant_type=authorization_code&client_id=${this.config.clientId}&client_secret=${this.config.clientSecret}&code=${code}`,
17
+ });
18
+
19
+ const tokenData = await tokenResponse.json();
20
+ if (!tokenData.access_token) {
21
+ throw new Error(`Yandex OAuth error: ${tokenData.error_description || tokenData.error}`);
22
+ }
23
+
24
+ const userResponse = await fetch('https://login.yandex.ru/info', {
25
+ method: 'POST',
26
+ headers: {
27
+ Authorization: `OAuth ${tokenData.access_token}`,
28
+ 'Content-Type': 'application/x-www-form-urlencoded',
29
+ },
30
+ body: 'format=json',
31
+ });
32
+
33
+ const user = await userResponse.json();
34
+ if (!user.id) {
35
+ throw new Error('Yandex OAuth: failed to get user profile');
36
+ }
37
+
38
+ const avatarUrl = !user.is_avatar_empty
39
+ ? `https://avatars.yandex.net/get-yapic/${user.default_avatar_id}/islands-200`
40
+ : null;
41
+
42
+ return {
43
+ providerId: String(user.id),
44
+ email: user.default_email || null,
45
+ name: user.first_name || null,
46
+ avatarUrl,
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,6 @@
1
+ export * from './OAuthProvider';
2
+ export * from './GoogleOauth';
3
+ export * from './VKOauth';
4
+ export * from './YandexOauth';
5
+ export * from './MailOauth';
6
+ export * from './AppleOauth';
@@ -80,12 +80,14 @@ export function createProfileResolver<
80
80
  if (!user) throw new UnauthorizedError();
81
81
 
82
82
  const service = getAuthService(ctx);
83
- const isOldPasswordOk = await service.verifyPassword(
84
- user.password!,
85
- input.oldPassword
86
- );
87
- if (!isOldPasswordOk)
88
- throw new Error(t('validation:updatePassword.wrongPassword'));
83
+ if (!user.isTempPassword) {
84
+ const isOldPasswordOk = await service.verifyPassword(
85
+ user.password!,
86
+ input.oldPassword
87
+ );
88
+ if (!isOldPasswordOk)
89
+ throw new Error(t('validation:updatePassword.wrongPassword'));
90
+ }
89
91
 
90
92
  user.password = await service.hashPassword(input.password);
91
93
  user.isTempPassword = false;