返回 AiToEarn
youtube.auth.service.ts
根目录 / project / aitoearn-electron / server / src / modules / plat / youtube / youtube.auth.service.ts
1 /*
2 * @Author: nevin
3 * @Date: 2025-05-27 14:48:12
4 * @LastEditTime: 2025-05-27 14:48:12
5 * @LastEditors: nevin
6 * @Description: YouTube授权服务
7 */
8 import { Injectable, Inject, forwardRef } from '@nestjs/common';
9 import { GoogleService } from '../google/google.service';
10 import { google } from 'googleapis';
11 import { AccessToken } from './comment';
12 import { getRandomString } from 'src/util';
13 import { InjectModel } from '@nestjs/mongoose';
14 import { Model } from 'mongoose';
15 import { User } from 'src/db/schema/user.schema';
16 import { Account, AccountType, AccountStatus } from 'src/db/schema/account.schema';
17 import { AccountToken, TokenPlatform, TokenStatus } from 'src/db/schema/accountToken.schema';
18 import { RedisService } from 'src/lib/redis/redis.service';
19 import { AuthService } from 'src/auth/auth.service';
20 import { getCurrentTimestamp } from 'src/util/time.util';
21 import { YouTubeAuthTokens } from './dto/youtube.dto'
22 import axios from 'axios';
23 import { ConfigService } from '@nestjs/config';
24 import { IdService } from 'src/db/id.service';
25 import { AccountService } from 'src/modules/account/account.service';
26
27
28 @Injectable()
29 export class YouTubeAuthService {
30
31 private webClientSecret: string;
32 private webClientId: string;
33 private webRenderBaseUrl: string;
34
35 constructor(
36 private configService: ConfigService,
37 private readonly idService: IdService,
38
39 @Inject(forwardRef(() => GoogleService))
40 private readonly googleService: GoogleService,
41 private readonly redisService: RedisService,
42 @InjectModel(User.name)
43 private userModel: Model<User>,
44 @InjectModel(Account.name)
45 private accountModel: Model<Account>,
46 private readonly AuthService: AuthService,
47 @InjectModel(AccountToken.name)
48 private AccountTokenModel: Model<AccountToken>,
49 private readonly accountService: AccountService,
50 ) {
51 this.initGoogleSecrets();
52 }
53
54 private async initGoogleSecrets() {
55 this.webClientSecret = this.configService.get<string>("GOOGLE_CONFIG.WEB_CLIENT_SECRET");
56 this.webClientId = this.configService.get<string>("GOOGLE_CONFIG.WEB_CLIENT_ID");
57 this.webRenderBaseUrl = this.configService.get<string>("GOOGLE_CONFIG.WEB_RENDER_URL");
58 }
59
60 private async getId() {
61 return this.idService.createId('accountId', 100000000, 1);
62 }
63
64 /**
65 * 初始化YouTube API客户端
66 * @param accessToken 访问令牌
67 * @returns YouTube API客户端
68 */
69 initializeYouTubeClient(accessToken: string): any {
70 const auth = new google.auth.OAuth2();
71 auth.setCredentials({ access_token: accessToken });
72 return google.youtube({ version: 'v3', auth });
73 }
74
75 /**
76 * 获取YouTube授权URL
77 * @param mail 用户邮箱
78 * @returns 授权URL
79 */
80 async getAuthorizationUrl(mail: string, userId: string): Promise<object> {
81 try {
82 const state = getRandomString(8);
83 this.redisService.setKey(`youtube:state:${userId}:${state}`, { mail }, 60 * 10);
84
85 // 指定YouTube特定的scope
86 const youtubeScopes = [
87 "https://www.googleapis.com/auth/youtube.force-ssl",
88 "https://www.googleapis.com/auth/youtube.readonly",
89 "https://www.googleapis.com/auth/youtube.upload",
90 "https://www.googleapis.com/auth/userinfo.profile"
91 ];
92
93 const stateData = {
94 originalState: state, // 保留原始state值
95 userId: userId, // 添加token
96 email: mail
97 };
98
99 // 将状态数据转换为JSON字符串并编码
100 const encodedState = encodeURIComponent(JSON.stringify(stateData));
101
102 const params = new URLSearchParams({
103 scope: youtubeScopes.join(" "),
104 access_type: "offline",
105 include_granted_scopes: "true",
106 response_type: "code",
107 state: encodedState,
108 redirect_uri: `${this.webRenderBaseUrl}/api/plat/youtube/auth/callback`,
109 client_id: this.webClientId,
110 prompt: "consent", // 强制要求用户确认授权,以便我们能够获取refresh_token
111 // login_hint: userId,
112 });
113
114 const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
115 authUrl.search = params.toString();
116
117 return {url: authUrl.toString()};
118 } catch (error) {
119 console.error('Error generating auth URL:', error);
120 throw new Error('无法生成授权URL');
121 }
122 }
123
124 /**
125 * 获取用户的YouTube访问令牌
126 * @param accountId 账号ID
127 * @returns 访问令牌
128 */
129 async getUserAccessToken(accountId: string): Promise<string> {
130 console.log("accountId:--", accountId);
131 const accountTokenInfo = await this.AccountTokenModel.findOne({accountId: accountId});
132
133 // if (!res) return '';
134
135 // // 剩余时间
136 // const overTime = res.expires_in;
137
138 // if (overTime < 60 * 60 && overTime > 0) {
139 // // 刷新token
140 // this.refreshAccessToken(userId, res.refresh_token);
141 // }
142 // const accountTokenInfo = await this.AccountTokenModel.findOne({accountId: accountId});
143 await this.refreshAccessToken(accountTokenInfo.userId, accountTokenInfo.accountId, accountTokenInfo.refreshToken);
144
145 const res: AccessToken = await this.redisService.get(
146 `youtube:accessToken:${accountId}`,
147 );
148 return res.access_token;
149 }
150
151 /**
152 * 刷新用户的YouTube访问令牌
153 * @param accountId 账号ID
154 * @returns 新的系统令牌
155 */
156 async refreshAccessToken(userId: string, accountId: string, refreshToken: string): Promise<object> {
157 try {
158 const userInfo = await this.userModel.findOne({_id: userId});
159 // if(!refreshToken) {
160
161 // console.log("=============userInfo====================");
162 // console.log(userInfo)
163 // refreshToken = userInfo?.googleAccount?.refreshToken ?? ''
164 // }
165
166 const tokenUrl = 'https://oauth2.googleapis.com/token';
167
168 // 请求体的参数
169 const params = new URLSearchParams({
170 client_id: this.webClientId, // 使用你的 client_id
171 client_secret: this.webClientSecret, // 使用你的 client_secret
172 refresh_token: refreshToken, // 提供刷新令牌
173 grant_type: 'refresh_token', // 认证类型是刷新令牌
174 });
175
176 // 发送 POST 请求到 Google token endpoint 来刷新 access token
177 const response = await axios.post(tokenUrl, params.toString(), {
178 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
179 });
180 console.log("================response================")
181 console.log(response);
182 const accessTokenInfo = response.data;
183 // console.log("================accessTokenInfo================")
184 // console.log(accessTokenInfo);
185 // 剩余有效秒数
186 const expires = accessTokenInfo.expires_in
187 // accessTokenInfo.expires_in - getCurrentTimestamp() - 60 * 60;
188 this.redisService.setKey(
189 `youtube:accessToken:${accountId}`,
190 accessTokenInfo,
191 expires,
192 );
193
194 const TokenInfo = {
195 phone: userInfo?.phone ?? '', // 如果 userInfo.phone 为 undefined 或 null,则使用空字符串
196 id: userId,
197 name: userInfo.name,
198 isManager: false,
199 googleId: userInfo?.googleAccount?.googleId ?? ''
200 }
201 console.log("发送获取systemToken的info---", TokenInfo);
202 const systemToken = await this.AuthService.generateToken(TokenInfo)
203
204 const returnRes = {url: systemToken};
205 return returnRes;
206 // 返回新的 access token 和其他信息
207 // return response.data; // 包含新的 access_token、expires_in、token_type 等信息
208 } catch (err) {
209 console.log('Error while refreshing access token', err);
210 throw new Error('Failed to refresh access token');
211 }
212 }
213
214 /**
215 * 验证并保存授权码
216 * @param code 授权码
217 * @param state 状态码
218 * @returns 系统令牌
219 */
220 async handleAuthorizationCode(code: string, state: string, userId: string) {
221 try {
222 // 获取state关联的邮箱信息
223 const stateInfo = await this.redisService.get(`youtube:state:${userId}:${state}`);
224 if (!stateInfo || !stateInfo.mail) {
225 throw new Error('无效的状态码');
226 }
227
228 // 使用授权码获取访问令牌和刷新令牌
229 const params = new URLSearchParams({
230 code: code,
231 redirect_uri: `${this.webRenderBaseUrl}/api/plat/youtube/auth/callback`,
232 client_id: this.webClientId,
233 grant_type: "authorization_code",
234 client_secret: this.webClientSecret,
235 });
236
237 const response = await axios.post('https://oauth2.googleapis.com/token', params.toString(), {
238 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
239 });
240
241 const { access_token, refresh_token, expires_in, id_token } = response.data;
242
243 // 验证ID令牌以获取用户信息
244 const oauth2Client = new google.auth.OAuth2();
245 oauth2Client.setCredentials({ access_token });
246
247 const ticket = await oauth2Client.verifyIdToken({
248 idToken: id_token,
249 audience: this.webClientId
250 });
251
252 const payload = ticket.getPayload();
253 const googleId = payload.sub;
254 const email = payload.email;
255
256 // 获取YouTube频道信息,用于更新账号数据库
257 await this.updateYouTubeAccountInfo(userId, email, googleId, access_token, refresh_token, expires_in);
258
259 // 缓存令牌
260 await this.redisService.setKey(
261 `youtube:accessToken:${googleId}`,
262 {
263 access_token,
264 refresh_token,
265 expiresAt: getCurrentTimestamp() + expires_in
266 },
267 expires_in
268 );
269
270 // 查询AccountToken数据库里是否存在令牌,如果存在,且上面获得refresh_token 存在,且不为空或null,则更新
271 // 如果不存在,则创建
272 let accountToken = await this.AccountTokenModel.findOne({
273 platform: AccountType.YOUTUBE,
274 accountId: googleId
275 });
276
277 if (accountToken) {
278 // 更新现有令牌
279 if (refresh_token && refresh_token.trim() !== '') {
280 accountToken.refreshToken = refresh_token;
281 }
282
283 accountToken.expiresAt = new Date((getCurrentTimestamp() + expires_in) * 1000);
284 accountToken.updateTime = new Date();
285 await accountToken.save();
286 } else {
287 // 创建新的令牌记录
288 accountToken = await this.AccountTokenModel.create({
289 userId,
290 platform: AccountType.YOUTUBE,
291 accountId: googleId,
292 refreshToken: refresh_token,
293 status: TokenStatus.USABLE,
294 createTime: new Date(),
295 updateTime: new Date(),
296 expiresAt: new Date((getCurrentTimestamp() + expires_in) * 1000)
297 });
298 }
299
300 // // 返回系统令牌
301 // return this.AuthService.generateToken(TokenInfo);
302 const existingAccount = await this.accountModel.findOne({
303 type: AccountType.YOUTUBE,
304 googleId: googleId
305 });
306
307 const results = {
308 data:
309 {
310 accountInfo: existingAccount,
311 userInfo: {
312 "userId": userId,
313 "uid": googleId
314 }
315 },
316 msg:"success", code: 0
317
318 };
319 console.log("最终返回", results);
320
321 return results;
322
323 } catch (error) {
324 console.error('处理授权码失败:', error);
325 throw new Error('授权失败');
326 }
327 }
328
329 /**
330 * 获取YouTube频道信息并更新账号数据库
331 * @param userId 用户ID
332 * @param googleId Google ID
333 * @param accessToken 访问令牌
334 * @param refreshToken 刷新令牌
335 */
336 private async updateYouTubeAccountInfo(
337 userId: string,
338 email: string,
339 googleId: string,
340 accessToken: string,
341 refreshToken: string,
342 expires_in: number
343 ): Promise<void> {
344 try {
345 // 初始化YouTube客户端
346 const youtube = this.initializeYouTubeClient(accessToken);
347
348 const channelInfo = {
349 // id: await this.getId(),
350 userId: userId,
351 type: AccountType.YOUTUBE,
352 uid: googleId,
353 googleId: googleId,
354 account: "accountUrl",
355 nickname: "",
356 avatar: "",
357 fansCount: 0,
358 workCount: 0,
359 likeCount: 0, // YouTube API不直接提供此信息
360 readCount: 0,
361 collectCount: 0, // YouTube API不直接提供此信息
362 forwardCount: 0, // YouTube API不直接提供此信息
363 commentCount: 0, // YouTube API不直接提供此信息
364 // loginTime: new Date(),
365 updateTime: new Date(),
366 status: AccountStatus.USABLE,
367 loginCookie: "1111", // YouTube不使用cookie认证
368 token: "111", // 存储访问令牌
369 // groupId: defaultGrpoupId, // 默认分组,可以根据需要调整
370 // income: 0
371 };
372
373 let hasChannel = false;
374 // 获取当前用户的YouTube频道信息
375 const response = await youtube.channels.list({
376 part: 'snippet,statistics',
377 mine: true
378 });
379
380 if (!response.data.items || response.data.items.length === 0) {
381
382 console.error("获取YouTube频道信息失败");
383 hasChannel = false;
384 // 如果没有频道或获取频道信息失败,则从Google用户信息获取
385 if (!hasChannel) {
386 console.log("无法获取YouTube频道信息,将从Google用户信息获取");
387 try {
388 const responseGoogle = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', {
389 headers: {
390 Authorization: `Bearer ${accessToken}`,
391 },
392 });
393
394 const userInfoData = responseGoogle.data;
395
396 // 使用Google用户信息更新账号数据
397 channelInfo.account = userInfoData.email || googleId;
398 channelInfo.nickname = userInfoData.name || '';
399 channelInfo.avatar = userInfoData.picture || '';
400
401 console.log("成功获取Google用户信息:", userInfoData);
402 } catch (error) {
403 console.error("获取Google用户信息失败:", error);
404 // 使用基本信息,确保至少有账号名称
405 channelInfo.account = googleId;
406 channelInfo.nickname = "YouTube User";
407 }
408 }
409
410 } else {
411 hasChannel = true;
412 const channel = response.data.items[0];
413 // 使用频道信息更新账号数据
414 channelInfo.account = channel.snippet.customUrl || channel.id;
415 channelInfo.nickname = channel.snippet.title;
416 channelInfo.avatar = channel.snippet.thumbnails.default.url;
417 channelInfo.fansCount = parseInt(channel.statistics.subscriberCount) || 0;
418 channelInfo.workCount = parseInt(channel.statistics.videoCount) || 0;
419 channelInfo.readCount = parseInt(channel.statistics.viewCount) || 0;
420
421 console.log("成功获取YouTube频道信息:", channel.snippet.title);
422 }
423
424 console.log(channelInfo);
425
426 // 创建或更新账号
427 // const account = await this.accountModel.findOneAndUpdate(
428 // { googleId: googleId, type: AccountType.YOUTUBE },
429 // channelInfo,
430 // { upsert: true, new: true }
431 // );
432 const account = await this.accountService.addOrUpdateAccount(channelInfo);
433
434 console.log("成功创建或更新YouTube账号:", account);
435
436
437 // 检查是否存在账号Token
438 const existingToken = await this.AccountTokenModel.findOne({
439 accountId: googleId,
440 platform: TokenPlatform.YOUTUBE
441 });
442
443 if (existingToken) {
444 // 更新现有Token
445 await this.AccountTokenModel.findOneAndUpdate(
446 { accountId: googleId, platform: TokenPlatform.YOUTUBE },
447 { refreshToken: refreshToken, updateTime: new Date() },
448 );
449 console.log("成功更新YouTube账号Token");
450 } else {
451 // 创建新Token
452 await this.AccountTokenModel.create({
453 accountId: googleId,
454 platform: TokenPlatform.YOUTUBE,
455 refreshToken: refreshToken,
456 expiresAt: new Date((getCurrentTimestamp() + expires_in) * 1000),
457 status: TokenStatus.USABLE,
458 createTime: new Date(),
459 updateTime: new Date(),
460 });
461 console.log("成功创建YouTube账号Token");
462 }
463
464
465 // // 检查账号是否存在
466 // const existingAccount = await this.accountModel.findOne({
467 // type: AccountType.YOUTUBE,
468 // googleId: googleId
469 // });
470
471 // if (existingAccount) {
472 // // 更新现有账号,保留原有的createTime
473 // channelInfo.status = existingAccount.status;
474 // channelInfo.groupId = existingAccount.groupId;
475 // channelInfo.income = existingAccount.income;
476
477 // // 更新账号信息
478 // await this.accountModel.findOneAndUpdate(
479 // { googleId: googleId, type: AccountType.YOUTUBE },
480 // channelInfo,
481 // { new: true }
482 // );
483
484 // console.log("成功更新YouTube账号信息");
485
486 // // 更新refresh_token
487 // await this.AccountTokenModel.findOneAndUpdate(
488 // { accountId: googleId, platform: TokenPlatform.YOUTUBE },
489 // { refreshToken: refreshToken, updateTime: new Date() },
490 // );
491 // console.log("平台:", TokenPlatform.YOUTUBE);
492
493 // const updateInfo = {
494 // nickname: channel.snippet.title,
495 // avatar: channel.snippet.thumbnails.default.url,
496 // fansCount: channel.statistics.subscriberCount || 0,
497 // workCount: channel.statistics.videoCount || 0,
498 // // likeCount: 0, // YouTube API不直接提供此信息
499 // readCount: channel.statistics.viewCount || 0,
500 // // collectCount: 0, // YouTube API不直接提供此信息
501 // // forwardCount: 0, // YouTube API不直接提供此信息
502 // // commentCount: 0, // YouTube API不直接提供此信息
503 // // loginTime: new Date(),
504 // updateTime: new Date(),
505 // status: AccountStatus.USABLE,
506 // // loginCookie: '', // YouTube不使用cookie认证
507 // // token: "", // 存储访问令牌
508 // // groupId: 1, // 默认分组,可以根据需要调整
509 // // income: 0
510 // }
511
512 // await this.accountModel.updateOne(
513 // { _id: existingAccount._id },
514 // { $set: updateInfo }
515 // );
516 // console.log(`已更新YouTube账号: ${updateInfo.nickname}`);
517
518
519 // } else {
520 // // 获取当前最大ID
521 // const maxIdAccount = await this.accountModel.findOne({}, { id: 1 }).sort({ id: -1 });
522 // const nextId = maxIdAccount ? maxIdAccount.id + 1 : 1;
523
524 // // 创建新账号
525 // await this.accountModel.create({
526 // ...channelInfo,
527 // id: nextId
528 // });
529 // console.log(`已创建新YouTube账号: ${channelInfo.nickname} (${channelInfo.uid})`);
530
531 // // 创建refresh_token和access_token
532 // const accountTokenInfo = {
533 // userId: userId,
534 // platform: TokenPlatform.YOUTUBE,
535 // refreshToken: refreshToken,
536 // accountId: googleId,
537 // status: TokenStatus.USABLE,
538 // createTime: new Date(),
539 // updateTime: new Date(),
540 // expiresAt: new Date((getCurrentTimestamp() + expires_in) * 1000)
541 // }
542 // await this.AccountTokenModel.create({
543 // ...accountTokenInfo
544 // });
545 // console.log(`已创建新YouTube账号Token: ${accountTokenInfo.accountId} (${accountTokenInfo.userId})`);
546
547 // }
548 } catch (error) {
549 console.error('更新YouTube账号信息失败:', error);
550 // 不抛出异常,避免影响授权流程
551 }
552 }
553
554 /**
555 * 获取初始化后的YouTube API客户端
556 * @param accountId 账号id
557 * @returns 初始化后的YouTube API客户端
558 */
559 async getYouTubeClient(accountId: string): Promise<any> {
560 const accessToken = await this.getUserAccessToken(accountId);
561 if (!accessToken) {
562 throw new Error('No access token available, user needs to authorize');
563 }
564
565 return this.initializeYouTubeClient(accessToken);
566 }
567
568 /**
569 * 检查用户是否已授权YouTube
570 * @param accountId 账号ID
571 * @returns 是否已授权
572 */
573 async isAuthorized(accountId: string): Promise<boolean> {
574 try {
575 const accessToken = await this.getUserAccessToken(accountId);
576 return !!accessToken;
577 } catch (error) {
578 return false;
579 }
580 }
581
582 /**
583 * 撤销YouTube授权
584 * @param accountId 账号ID
585 * @returns 撤销结果
586 */
587 async revokeAuthorization(accountId: string): Promise<boolean> {
588 try {
589 const accessToken = await this.getUserAccessToken(accountId);
590 if (!accessToken) {
591 return true; // 已经没有授权了
592 }
593
594 // 撤销令牌
595 await this.googleService.getClient().revokeToken(accessToken);
596
597 // 删除缓存的令牌
598 await this.redisService.del(`youtube:accessToken:${accountId}`);
599
600 // 更新用户信息,移除授权信息
601 await this.AccountTokenModel.updateOne(
602 { accountId: accountId },
603 { $unset: { 'refreshToken': 1, 'expiresAt': 1 } }
604 );
605
606 return true;
607 } catch (error) {
608 console.error('Error revoking authorization:', error);
609 return false;
610 }
611 }
612
613 /**
614 * 保存用户YouTube授权信息
615 * @param userId 用户ID
616 * @param tokens 授权令牌
617 */
618 async saveUserTokens(userId: string, tokens: YouTubeAuthTokens): Promise<void> {
619 try {
620 // 更新用户信息
621 await this.userModel.updateOne(
622 { _id: userId },
623 {
624 $set: {
625 'googleAccount.accessToken': tokens.accessToken,
626 'googleAccount.refreshToken': tokens.refreshToken,
627 'googleAccount.expiresAt': tokens.expiresAt || (getCurrentTimestamp() + 3600)
628 }
629 }
630 );
631
632 // 缓存访问令牌
633 const expiresIn = tokens.expiresAt ? tokens.expiresAt - getCurrentTimestamp() : 3600;
634 await this.redisService.setKey(
635 `google:accessToken:${userId}`,
636 { access_token: tokens.accessToken, refresh_token: tokens.refreshToken },
637 expiresIn > 0 ? expiresIn : 3600
638 );
639 } catch (error) {
640 console.error('Error saving user tokens:', error);
641 throw new Error('Failed to save user tokens');
642 }
643 }
644 }
645
645 lines TYPESCRIPT