feat: 修复第三方登录的问题
This commit is contained in:
parent
7d5e734e39
commit
8b5d7ed6dc
2
.env
2
.env
|
|
@ -8,7 +8,7 @@ DB_NAME='auth_db'
|
|||
# github
|
||||
GITHUB_CLIENT_ID=Ov23lihk723FlNAwlFg6
|
||||
GITHUB_CLIENT_SECRET=b839f50bba1f006ffdd43fb73c5ae221a54e1e2e
|
||||
GITHUB_CALLBACK_URL=http://localhost:3030/auth/github/callback
|
||||
GITHUB_CALLBACK_URL=http://108.171.193.155:8000/callback
|
||||
|
||||
# 飞书
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
# OAuth JWT 集成实现文档
|
||||
|
||||
## 🎯 实现目标
|
||||
|
||||
将飞书和 GitHub 的第三方 OAuth access_token 转换为项目自己的 JWT Token,实现统一的认证体系。
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 用户数据模型扩展
|
||||
|
||||
扩展了 `User` 实体以支持多种登录方式:
|
||||
|
||||
```typescript
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ unique: true })
|
||||
username: string;
|
||||
|
||||
@Column({ nullable: true }) // OAuth 用户不需要密码
|
||||
password: string;
|
||||
|
||||
@Column()
|
||||
email: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
// OAuth 相关字段
|
||||
@Column({ nullable: true })
|
||||
provider: string; // 'local', 'github', 'lark'
|
||||
|
||||
@Column({ nullable: true })
|
||||
providerId: string; // 第三方平台的用户ID
|
||||
|
||||
@Column({ nullable: true })
|
||||
providerUsername: string; // 第三方平台的用户名
|
||||
|
||||
// JWT 相关字段
|
||||
@Column({ nullable: true })
|
||||
refreshToken: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
refreshTokenExpires: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### UserService 扩展
|
||||
|
||||
添加了 OAuth 用户专用的方法:
|
||||
|
||||
1. **`findUserByProvider(provider, providerId)`** - 根据第三方平台信息查找用户
|
||||
2. **`createOAuthUser(provider, providerId, providerUsername, email)`** - 创建 OAuth 用户
|
||||
|
||||
### 用户名生成策略
|
||||
|
||||
为避免用户名冲突,OAuth 用户的用户名格式为:
|
||||
- 格式:`{provider}_{providerUsername}`
|
||||
- 示例:`github_octocat`, `lark_zhangsan`
|
||||
- 如果冲突,自动添加数字后缀:`github_octocat_1`
|
||||
|
||||
## 🔄 OAuth 流程
|
||||
|
||||
### GitHub OAuth 流程
|
||||
|
||||
1. **登录入口**:`GET /github/login?redirectUri=<回调地址>`
|
||||
2. **OAuth 回调**:`GET /github/callback?code=<授权码>`
|
||||
3. **处理流程**:
|
||||
```
|
||||
授权码 → GitHub Access Token → 用户信息 → 查找/创建本地用户 → 生成 JWT Token
|
||||
```
|
||||
|
||||
### 飞书 OAuth 流程
|
||||
|
||||
1. **登录入口**:`GET /lark/login?redirectUri=<回调地址>`
|
||||
2. **OAuth 回调**:`GET /lark/callback?code=<授权码>`
|
||||
3. **处理流程**:
|
||||
```
|
||||
授权码 → 飞书 Access Token → 用户信息 → 查找/创建本地用户 → 生成 JWT Token
|
||||
```
|
||||
|
||||
## 📝 API 响应格式
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "GitHub login successful",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "github_octocat",
|
||||
"email": "octocat@github.com",
|
||||
"provider": "github",
|
||||
"providerUsername": "octocat"
|
||||
},
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expires_in": 3600,
|
||||
"expires_at": 1703123456789,
|
||||
"refresh_token_expires_in": 604800,
|
||||
"refresh_token_expires_at": 1703728256789
|
||||
}
|
||||
```
|
||||
|
||||
## 🛡️ 安全特性
|
||||
|
||||
1. **用户隔离** - OAuth 用户和本地用户完全分离
|
||||
2. **唯一标识** - 使用 `provider + providerId` 作为唯一标识
|
||||
3. **JWT 安全** - 使用项目统一的 JWT 密钥和过期策略
|
||||
4. **缓存机制** - 第三方 token 和用户信息缓存,减少 API 调用
|
||||
|
||||
## 🔧 配置要求
|
||||
|
||||
### 环境变量
|
||||
|
||||
```env
|
||||
# GitHub OAuth
|
||||
GITHUB_CLIENT_ID=your_github_client_id
|
||||
GITHUB_CLIENT_SECRET=your_github_client_secret
|
||||
GITHUB_CALLBACK_URL=http://localhost:3030/auth/github/callback
|
||||
|
||||
# 飞书 OAuth
|
||||
FEISHU_APP_ID=your_feishu_app_id
|
||||
FEISHU_APP_SECRET=your_feishu_app_secret
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
访问测试页面:`http://localhost:8000/test-github-sso.html`
|
||||
|
||||
支持的登录方式:
|
||||
- GitHub OAuth
|
||||
- 飞书 OAuth
|
||||
- Passport GitHub 策略(用于重定向场景)
|
||||
|
||||
## 🚀 使用示例
|
||||
|
||||
### 前端调用
|
||||
|
||||
```javascript
|
||||
// GitHub 登录
|
||||
window.location.href = 'http://localhost:3030/github/login?redirectUri=' +
|
||||
encodeURIComponent(window.location.origin + '/callback');
|
||||
|
||||
// 飞书登录
|
||||
window.location.href = 'http://localhost:3030/lark/login?redirectUri=' +
|
||||
encodeURIComponent(window.location.origin + '/callback');
|
||||
```
|
||||
|
||||
### 使用 JWT Token
|
||||
|
||||
```javascript
|
||||
// 在后续请求中使用 JWT Token
|
||||
fetch('/api/protected', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 📋 优势
|
||||
|
||||
1. **统一认证** - 所有用户最终都使用项目的 JWT Token
|
||||
2. **类型安全** - 完整的 TypeScript 类型定义
|
||||
3. **扩展性强** - 易于添加新的 OAuth 提供商
|
||||
4. **数据一致性** - 统一的用户数据模型
|
||||
5. **安全可靠** - 遵循 OAuth 2.0 和 JWT 最佳实践
|
||||
|
|
@ -8,6 +8,7 @@ import { UserModule } from './user/user.module';
|
|||
import { User } from './user/entities/user.entity';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { LarkModule } from './lark/lark.module';
|
||||
import { GitHubModule } from './github/github.module';
|
||||
import { DeepseekModule } from './deepseek/deepseek.module';
|
||||
|
||||
@Module({
|
||||
|
|
@ -35,6 +36,7 @@ import { DeepseekModule } from './deepseek/deepseek.module';
|
|||
UserModule,
|
||||
AuthModule,
|
||||
LarkModule,
|
||||
GitHubModule,
|
||||
DeepseekModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly userService: UserService) { }
|
||||
@Get('github')
|
||||
@UseGuards(AuthGuard('github'))
|
||||
async githubLogin() {
|
||||
|
|
@ -12,11 +14,23 @@ export class AuthController {
|
|||
|
||||
@Get('github/callback')
|
||||
@UseGuards(AuthGuard('github'))
|
||||
async githubCallback(@Req() req, @Res() res: Response) {
|
||||
const user = req.user;
|
||||
async githubCallback(@Req() req: any, @Res() res: Response) {
|
||||
const oauthUser = req.user;
|
||||
|
||||
// 构造前端 URL,附带用户信息或 Token
|
||||
const frontendUrl = `http://localhost:8000/?username=${user.username}&email=${user.email}`;
|
||||
// 生成项目的 JWT Token
|
||||
const jwtTokenData = await this.userService.generateToken(oauthUser);
|
||||
|
||||
// 构造前端 URL,附带用户信息和 JWT Token
|
||||
const params = new URLSearchParams({
|
||||
userId: oauthUser.id.toString(),
|
||||
username: oauthUser.username,
|
||||
email: oauthUser.email,
|
||||
provider: oauthUser.provider,
|
||||
providerUsername: oauthUser.providerUsername || oauthUser.username,
|
||||
access_token: jwtTokenData.access_token
|
||||
});
|
||||
|
||||
const frontendUrl = `http://localhost:8000/test-github-sso.html?${params.toString()}`;
|
||||
// 重定向到前端
|
||||
return res.redirect(frontendUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import { UserModule } from '../user/user.module';
|
|||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { GitHubStrategy } from './github.strategy';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { AuthController } from './auth.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -16,9 +18,11 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||
signOptions: { expiresIn: '1h' },
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
GitHubStrategy,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-github2';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
|
||||
constructor() {
|
||||
constructor(private userService: UserService) {
|
||||
super({
|
||||
clientID: process.env.GITHUB_CLIENT_ID, // 从环境变量加载
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
|
|
@ -13,13 +14,24 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
|
|||
});
|
||||
}
|
||||
|
||||
async validate(accessToken: string, refreshToken: string, profile: any) {
|
||||
async validate(_accessToken: string, _refreshToken: string, profile: any) {
|
||||
// 返回用户信息,或者直接处理用户登录逻辑
|
||||
const { id, username, emails } = profile;
|
||||
return {
|
||||
id,
|
||||
const email = emails?.[0]?.value || null;
|
||||
|
||||
// 检查 GitHub 用户是否已存在
|
||||
let user = await this.userService.findUserByProvider('github', id.toString());
|
||||
|
||||
if (!user) {
|
||||
// 如果用户不存在,创建新的 OAuth 用户
|
||||
user = await this.userService.createOAuthUser(
|
||||
'github',
|
||||
id.toString(),
|
||||
username,
|
||||
email: emails?.[0]?.value || null,
|
||||
};
|
||||
email || `${username}@github.local`
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { Controller, Get, Query, Res } from '@nestjs/common';
|
||||
import { GitHubService } from './github.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { Response } from 'express';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
|
||||
@Controller('github')
|
||||
export class GitHubController {
|
||||
constructor(
|
||||
private readonly githubService: GitHubService,
|
||||
private readonly userService: UserService,
|
||||
) { }
|
||||
|
||||
@Get('login')
|
||||
@Public()
|
||||
async login(@Query('redirectUri') redirectUri: string, @Res() res: Response) {
|
||||
const loginUrl = await this.githubService.getLoginUrl(redirectUri);
|
||||
return res.redirect(loginUrl);
|
||||
}
|
||||
|
||||
@Get('callback')
|
||||
@Public()
|
||||
async callback(
|
||||
@Query('code') code: string,
|
||||
@Query('state') _state: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
if (!code) {
|
||||
return res.status(400).send('Authorization code is missing');
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenData = await this.githubService.getAccessToken(code);
|
||||
|
||||
if (tokenData && tokenData.access_token) {
|
||||
const userInfo = await this.githubService.getUserInfo(tokenData.access_token);
|
||||
|
||||
// 检查 GitHub 用户是否已存在
|
||||
let user = await this.userService.findUserByProvider('github', userInfo.id.toString());
|
||||
|
||||
if (!user) {
|
||||
// 创建新的 OAuth 用户
|
||||
user = await this.userService.createOAuthUser(
|
||||
'github',
|
||||
userInfo.id.toString(),
|
||||
userInfo.login,
|
||||
userInfo.email || `${userInfo.login}@github.local`
|
||||
);
|
||||
}
|
||||
|
||||
// 生成项目的 JWT Token
|
||||
const jwtTokenData = await this.userService.generateToken(user);
|
||||
|
||||
return res.json({
|
||||
message: 'GitHub login successful',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
provider: user.provider,
|
||||
providerUsername: user.providerUsername
|
||||
},
|
||||
...jwtTokenData
|
||||
});
|
||||
|
||||
} else {
|
||||
return res.status(400).send('Failed to retrieve access token');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('GitHub OAuth callback error:', error);
|
||||
return res.status(500).send('Internal server error during GitHub authentication');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { GitHubService } from './github.service';
|
||||
import { GitHubController } from './github.controller';
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
controllers: [GitHubController],
|
||||
providers: [GitHubService],
|
||||
exports: [GitHubService],
|
||||
})
|
||||
export class GitHubModule { }
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import axios from 'axios';
|
||||
|
||||
@Injectable()
|
||||
export class GitHubService {
|
||||
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
private readonly clientId = process.env.GITHUB_CLIENT_ID;
|
||||
private readonly clientSecret = process.env.GITHUB_CLIENT_SECRET;
|
||||
private readonly redirectUri = process.env.GITHUB_CALLBACK_URL;
|
||||
|
||||
async getLoginUrl(redirectUri?: string): Promise<string> {
|
||||
const state = Math.random().toString(36).substring(2, 15);
|
||||
const scope = 'user:email';
|
||||
const callbackUrl = redirectUri || this.redirectUri;
|
||||
|
||||
// 缓存 state 用于验证
|
||||
await this.cacheManager.set(`github_state_${state}`, callbackUrl, 600); // 10分钟过期
|
||||
|
||||
return `https://github.com/login/oauth/authorize?client_id=${this.clientId}&redirect_uri=${encodeURIComponent(
|
||||
callbackUrl,
|
||||
)}&scope=${scope}&state=${state}`;
|
||||
}
|
||||
|
||||
async getAccessToken(authCode: string): Promise<any> {
|
||||
const url = 'https://github.com/login/oauth/access_token';
|
||||
const data = {
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret,
|
||||
code: authCode,
|
||||
redirect_uri: this.redirectUri,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, data, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const tokenData = response.data;
|
||||
|
||||
if (tokenData && tokenData.access_token) {
|
||||
// 将 token 存储到缓存中,设置过期时间
|
||||
await this.cacheManager.set(
|
||||
`github_token_${tokenData.access_token}`,
|
||||
tokenData,
|
||||
3600, // 缓存1小时
|
||||
);
|
||||
return tokenData;
|
||||
} else {
|
||||
throw new Error('获取 GitHub token 失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('GitHub token exchange error:', error);
|
||||
throw new Error('获取 GitHub token 失败');
|
||||
}
|
||||
}
|
||||
|
||||
async getUserInfo(accessToken: string): Promise<any> {
|
||||
// 先检查缓存
|
||||
const cachedUserInfo = await this.cacheManager.get(`github_user_${accessToken}`);
|
||||
if (cachedUserInfo) {
|
||||
return cachedUserInfo;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户基本信息
|
||||
const userResponse = await axios.get('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `token ${accessToken}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
|
||||
// 获取用户邮箱信息
|
||||
const emailResponse = await axios.get('https://api.github.com/user/emails', {
|
||||
headers: {
|
||||
Authorization: `token ${accessToken}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = {
|
||||
...userResponse.data,
|
||||
emails: emailResponse.data,
|
||||
email: emailResponse.data.find((email: any) => email.primary)?.email || userResponse.data.email,
|
||||
};
|
||||
|
||||
// 缓存用户信息
|
||||
await this.cacheManager.set(`github_user_${accessToken}`, userInfo, 3600); // 缓存1小时
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('GitHub user info error:', error);
|
||||
throw new Error('获取 GitHub 用户信息失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { Controller, Get, Query, Res } from '@nestjs/common';
|
||||
import { LarkService } from './lark.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { Response } from 'express';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
|
||||
@Controller('lark')
|
||||
export class LarkController {
|
||||
constructor(private readonly larkService: LarkService) { }
|
||||
constructor(
|
||||
private readonly larkService: LarkService,
|
||||
private readonly userService: UserService,
|
||||
) { }
|
||||
|
||||
@Get('login')
|
||||
@Public()
|
||||
|
|
@ -21,13 +25,46 @@ export class LarkController {
|
|||
return res.status(400).send('Authorization code is missing');
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenData = await this.larkService.getAccessToken(code);
|
||||
|
||||
if (tokenData && tokenData.code === 0) {
|
||||
const user = tokenData.data;
|
||||
return res.json({ message: 'Login successful', user });
|
||||
const accessToken = tokenData.data.access_token;
|
||||
const userInfo = await this.larkService.getUserInfo(accessToken);
|
||||
|
||||
// 检查飞书用户是否已存在
|
||||
let user = await this.userService.findUserByProvider('lark', userInfo.user_id);
|
||||
|
||||
if (!user) {
|
||||
// 创建新的 OAuth 用户
|
||||
user = await this.userService.createOAuthUser(
|
||||
'lark',
|
||||
userInfo.user_id,
|
||||
userInfo.name || userInfo.user_id,
|
||||
userInfo.email || `${userInfo.user_id}@feishu.local`
|
||||
);
|
||||
}
|
||||
|
||||
// 生成项目的 JWT Token
|
||||
const jwtTokenData = await this.userService.generateToken(user);
|
||||
|
||||
return res.json({
|
||||
message: 'Lark login successful',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
provider: user.provider,
|
||||
providerUsername: user.providerUsername
|
||||
},
|
||||
...jwtTokenData
|
||||
});
|
||||
} else {
|
||||
return res.status(400).send('Failed to retrieve access token');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Lark OAuth callback error:', error);
|
||||
return res.status(500).send('Internal server error during Lark authentication');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { LarkService } from './lark.service';
|
||||
import { LarkController } from './lark.controller';
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
controllers: [LarkController],
|
||||
providers: [LarkService],
|
||||
exports: [LarkService],
|
||||
})
|
||||
export class LarkModule { }
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class LarkService {
|
|||
}
|
||||
|
||||
async getAccessToken(authCode: string): Promise<any> {
|
||||
const cachedToken = await this.cacheManager.get('tokenData');
|
||||
const cachedToken = await this.cacheManager.get(`lark_token_${authCode}`);
|
||||
if (cachedToken) {
|
||||
return cachedToken; // 如果缓存中有 token,直接返回
|
||||
}
|
||||
|
|
@ -32,10 +32,41 @@ export class LarkService {
|
|||
const tokenData = response.data;
|
||||
if (tokenData && tokenData.code === 0) {
|
||||
// 将 token 存储到缓存中,设置过期时间
|
||||
await this.cacheManager.set('tokenData', tokenData, 3600); // 缓存1小时
|
||||
await this.cacheManager.set(`lark_token_${authCode}`, tokenData, 3600); // 缓存1小时
|
||||
return tokenData;
|
||||
} else {
|
||||
throw new Error('获取 token 失败');
|
||||
throw new Error('获取飞书 token 失败');
|
||||
}
|
||||
}
|
||||
|
||||
async getUserInfo(accessToken: string): Promise<any> {
|
||||
// 先检查缓存
|
||||
const cachedUserInfo = await this.cacheManager.get(`lark_user_${accessToken}`);
|
||||
if (cachedUserInfo) {
|
||||
return cachedUserInfo;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户基本信息
|
||||
const userResponse = await axios.get('https://open.feishu.cn/open-apis/authen/v1/user_info', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = userResponse.data;
|
||||
|
||||
if (userInfo && userInfo.code === 0) {
|
||||
// 缓存用户信息
|
||||
await this.cacheManager.set(`lark_user_${accessToken}`, userInfo.data, 3600); // 缓存1小时
|
||||
return userInfo.data;
|
||||
} else {
|
||||
throw new Error('获取飞书用户信息失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Lark user info error:', error);
|
||||
throw new Error('获取飞书用户信息失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export class User {
|
|||
@Column({ unique: true })
|
||||
username: string;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
password: string;
|
||||
|
||||
@Column()
|
||||
|
|
@ -22,4 +22,14 @@ export class User {
|
|||
|
||||
@Column({ nullable: true })
|
||||
refreshTokenExpires: Date;
|
||||
|
||||
// OAuth 相关字段
|
||||
@Column({ nullable: true })
|
||||
provider: string; // 'local', 'github', 'lark'
|
||||
|
||||
@Column({ nullable: true })
|
||||
providerId: string; // 第三方平台的用户ID
|
||||
|
||||
@Column({ nullable: true })
|
||||
providerUsername: string; // 第三方平台的用户名
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ export class UserService {
|
|||
private jwtService: JwtService,
|
||||
) { }
|
||||
|
||||
async createUser(username: string, password: string, email: string) {
|
||||
if (!password) {
|
||||
throw new Error('Password is required');
|
||||
}
|
||||
async createUser(username: string, password: string | null, email: string) {
|
||||
let hashedPassword = null;
|
||||
|
||||
if (password) {
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
const user = this.userRepository.create({
|
||||
username,
|
||||
|
|
@ -35,6 +35,41 @@ export class UserService {
|
|||
return this.userRepository.findOne({ where: { username } });
|
||||
}
|
||||
|
||||
async findUserByProvider(provider: string, providerId: string): Promise<User | undefined> {
|
||||
return this.userRepository.findOne({
|
||||
where: { provider, providerId }
|
||||
});
|
||||
}
|
||||
|
||||
async createOAuthUser(
|
||||
provider: string,
|
||||
providerId: string,
|
||||
providerUsername: string,
|
||||
email: string
|
||||
): Promise<User> {
|
||||
// 生成唯一的用户名:provider_providerUsername
|
||||
const username = `${provider}_${providerUsername}`;
|
||||
|
||||
// 检查用户名是否已存在,如果存在则添加随机后缀
|
||||
let finalUsername = username;
|
||||
let counter = 1;
|
||||
while (await this.findUserByUsername(finalUsername)) {
|
||||
finalUsername = `${username}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const user = this.userRepository.create({
|
||||
username: finalUsername,
|
||||
password: null, // OAuth 用户不需要密码
|
||||
email,
|
||||
provider,
|
||||
providerId,
|
||||
providerUsername,
|
||||
});
|
||||
|
||||
return await this.userRepository.save(user);
|
||||
}
|
||||
|
||||
async validatePassword(
|
||||
plainPassword: string,
|
||||
hashedPassword: string,
|
||||
|
|
@ -50,6 +85,7 @@ export class UserService {
|
|||
{
|
||||
sub: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
},
|
||||
{ expiresIn: `${accessTokenExpiresIn}s` },
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue