Explorar o código

created feisu-server

warrior hai 15 horas
pai
achega
7ae332bd77

+ 15 - 65
modules/fmode-feishu-api/src/client.ts

@@ -58,18 +58,6 @@ export interface FeishuUserInfo {
   tenantKey?: string;
 }
 
-/**
- * 登录用户信息响应接口
- */
-export interface LoginUserInfoResponse {
-  accessToken: string;
-  tokenType: string;
-  expiresIn: number;
-  refreshToken: string;
-  refreshExpiresIn: number;
-  scope: string;
-}
-
 /**
  * API 转发请求选项接口
  */
@@ -188,6 +176,7 @@ export class FeishuClient {
    * @param appSecret - 应用密钥
    * @param code - 授权码
    * @param grantType - 授权类型
+   * @param redirectUri - 重定向URI(可选)
    * @returns Promise<UserAccessTokenResponse>
    * @throws {Error} 获取失败时抛出错误
    */
@@ -195,22 +184,29 @@ export class FeishuClient {
     appId: string,
     appSecret: string,
     code: string,
-    grantType: string = 'authorization_code'
+    grantType: string = 'authorization_code',
+    redirectUri?: string
   ): Promise<UserAccessTokenResponse> {
     try {
       const url = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token';
 
+      const requestBody: any = {
+        grant_type: grantType,
+        client_id: appId,
+        client_secret: appSecret,
+        code: code
+      };
+
+      if (redirectUri) {
+        requestBody.redirect_uri = redirectUri;
+      }
+
       const response = await fetch(url, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json; charset=utf-8'
         },
-        body: JSON.stringify({
-          grant_type: grantType,
-          client_id: appId,
-          client_secret: appSecret,
-          code: code
-        })
+        body: JSON.stringify(requestBody)
       });
 
       const result = await response.json();
@@ -331,52 +327,6 @@ export class FeishuClient {
     }
   }
 
-  /**
-   * 获取登录用户身份(网页免登)
-   *
-   * @static
-   * @async
-   * @param tenantAccessToken - 租户访问令牌
-   * @param code - 免登授权码
-   * @returns Promise<LoginUserInfoResponse>
-   * @throws {Error} 获取失败时抛出错误
-   */
-  static async getLoginUserInfo(tenantAccessToken: string, code: string): Promise<LoginUserInfoResponse> {
-    try {
-      const url = 'https://open.feishu.cn/open-apis/authen/v1/access_token';
-
-      const response = await fetch(url, {
-        method: 'POST',
-        headers: {
-          'Authorization': `Bearer ${tenantAccessToken}`,
-          'Content-Type': 'application/json; charset=utf-8'
-        },
-        body: JSON.stringify({
-          grant_type: 'authorization_code',
-          code: code
-        })
-      });
-
-      const result = await response.json();
-
-      if (result.code !== 0) {
-        throw new Error(`获取登录用户身份失败: ${result.msg}, code: ${result.code}`);
-      }
-
-      return {
-        accessToken: result.data.access_token,
-        tokenType: result.data.token_type,
-        expiresIn: result.data.expires_in,
-        refreshToken: result.data.refresh_token,
-        refreshExpiresIn: result.data.refresh_expires_in,
-        scope: result.data.scope
-      };
-    } catch (error) {
-      console.error('获取登录用户身份异常:', error);
-      throw error;
-    }
-  }
-
   /**
    * 转发请求到飞书API
    *

+ 7 - 83
modules/fmode-feishu-api/src/router.ts

@@ -65,7 +65,6 @@ export function createFeishuRouter(config: FeishuRouterConfig): express.Router {
       apps: apps,
       endpoints: {
         oauth2Login: "POST /oauth2/login - OAuth2扫码登录",
-        oauth2Feishu: "POST /oauth2/feishu - 网页应用免登录",
         oauth2Refresh: "POST /oauth2/refresh_token - 刷新令牌",
         userSync: "POST /user/sync - 同步用户信息",
         forward: "POST /forward - 转发API请求",
@@ -117,11 +116,11 @@ export function createFeishuRouter(config: FeishuRouterConfig): express.Router {
   }
 
   /**
-   * OAuth2扫码登录
+   * OAuth2扫码或免密登录
    */
   router.post('/oauth2/login', async function (req: Request, res: Response) {
     try {
-      const { appId, code } = req.body;
+      const { appId, code, redirect_uri } = req.body;
 
       if (!appId || !code) {
         goWrong(res, "缺少appId或code参数");
@@ -142,7 +141,8 @@ export function createFeishuRouter(config: FeishuRouterConfig): express.Router {
         appConfig.appId,
         appConfig.appSecret,
         code,
-        'authorization_code'
+        'authorization_code',
+        redirect_uri
       );
 
       console.log(`成功获取OAuth2令牌,有效期: ${tokenInfo.expiresIn}秒`);
@@ -158,6 +158,7 @@ export function createFeishuRouter(config: FeishuRouterConfig): express.Router {
       await recordAuthEvent('oauth2_' + appId, 'oauth2_login', {
         appId: appId,
         code: code.substring(0, 8) + '...',
+        redirect_uri: redirect_uri,
         userInfo: {
           name: userInfo.name,
           unionId: userInfo.unionId,
@@ -186,7 +187,8 @@ export function createFeishuRouter(config: FeishuRouterConfig): express.Router {
             refreshToken: tokenInfo.refreshToken,
             expiresIn: tokenInfo.expiresIn
           },
-          sessionToken: userSessionInfo.sessionToken.get('sessionToken')
+          sessionToken: userSessionInfo.sessionToken.get('sessionToken'),
+          redirect_uri: redirect_uri
         }
       });
 
@@ -253,84 +255,6 @@ export function createFeishuRouter(config: FeishuRouterConfig): express.Router {
     }
   });
 
-  /**
-   * 飞书网页免登OAuth2接口
-   */
-  router.post('/oauth2/feishu', async function (req: Request, res: Response) {
-    try {
-      const { appId, code } = req.body;
-
-      if (!appId || !code) {
-        goWrong(res, "缺少appId或code参数");
-        return;
-      }
-
-      const appConfig = feishuConfig.getAppConfig(appId);
-      console.log(`使用应用配置: ${appId}`);
-
-      const tenantAccessToken = await feishuTokenManager.getTenantAccessToken(appId);
-
-      if (typeof code !== 'string' || code.length < 10) {
-        goWrong(res, "授权码格式无效");
-        return;
-      }
-
-      console.log(`开始网页免登OAuth2流程,appId: ${appId}, code: ${code.substring(0, 8)}...`);
-
-      const loginInfo = await FeishuClient.getLoginUserInfo(tenantAccessToken, code);
-
-      console.log(`成功获取登录用户身份,access_token有效期: ${loginInfo.expiresIn}秒`);
-
-      const userInfo = await FeishuClient.getUserInfo(loginInfo.accessToken);
-
-      console.log(`成功获取飞书用户信息,unionId: ${userInfo.unionId}, userId: ${userInfo.userId}`);
-
-      const userSessionInfo = await feishuUserManager.getUserInfoSessionToken(userInfo, appId);
-
-      console.log(`用户登录处理完成,用户ID: ${userSessionInfo.user.id}`);
-
-      await recordAuthEvent('weblogin_' + appId, 'weblogin_oauth2', {
-        appId: appId,
-        code: code.substring(0, 8) + '...',
-        userInfo: {
-          name: userInfo.name,
-          unionId: userInfo.unionId,
-          openId: userInfo.openId
-        },
-        userId: userSessionInfo.user.id,
-        username: userSessionInfo.user.get('username')
-      });
-
-      res.json({
-        code: 1,
-        mess: "登录成功",
-        data: {
-          userInfo: {
-            ...userInfo,
-            objectId: userSessionInfo.user.id,
-            username: userSessionInfo.user.get('username'),
-            nickname: userSessionInfo.user.get('nickname'),
-            mobile: userSessionInfo.user.get('mobile'),
-            email: userSessionInfo.user.get('email'),
-            avatar: userSessionInfo.user.get('avatar')
-          },
-          sessionToken: userSessionInfo.sessionToken.get('sessionToken')
-        }
-      });
-
-    } catch (error: any) {
-      console.error('飞书网页免登失败:', error);
-
-      if (error.message?.includes('code')) {
-        goWrong(res, "授权码已过期或无效,请重新授权");
-      } else if (error.message?.includes('应用配置中缺少company信息')) {
-        goWrong(res, "应用配置不完整,请联系管理员");
-      } else {
-        goWrong(res, error.message || "网页免登失败");
-      }
-    }
-  });
-
   /**
    * 同步飞书用户信息
    */

+ 43 - 52
modules/fmode-feishu-api/src/user-manager.ts

@@ -5,7 +5,6 @@
  * - 负责飞书OAuth2登录用户的获取或创建
  * - SessionToken管理
  * - 用户信息同步和更新
- * - 支持多公司/组织用户管理
  *
  * @module user-manager
  * @author fmode
@@ -29,7 +28,6 @@ export interface UserSessionInfo {
     mobile?: string;
     email?: string;
     avatar: string;
-    company: any;
     sessionToken: string;
   };
 }
@@ -61,16 +59,9 @@ export class FeishuUserManager {
    */
   async getUserInfoSessionToken(userInfo: FeishuUserInfo, appId: string): Promise<UserSessionInfo> {
     try {
-      const appConfig = this.configManager.getAppConfig(appId);
-      const company = appConfig.company;
+      console.log(`处理飞书用户登录,unionId: ${userInfo.unionId}, openId: ${userInfo.openId}`);
 
-      if (!company) {
-        throw new Error(`应用配置中缺少company信息: ${appId}`);
-      }
-
-      console.log(`处理飞书用户登录,unionId: ${userInfo.unionId}, openId: ${userInfo.openId}, company: ${company}`);
-
-      let user = await this.findOrCreateUser(userInfo, company);
+      let user = await this.findOrCreateUser(userInfo);
 
       const sessionToken = await this.generateSessionToken(user);
 
@@ -88,7 +79,6 @@ export class FeishuUserManager {
           mobile: user.get('mobile'),
           email: user.get('email'),
           avatar: user.get('avatar'),
-          company: user.get('company'),
           sessionToken: sessionToken.get('sessionToken'),
         }
       };
@@ -104,18 +94,17 @@ export class FeishuUserManager {
    *
    * @async
    * @param userInfo - 飞书用户信息
-   * @param company - 公司ID
    * @returns Promise<any> Parse用户对象
    * @throws {Error} 查找或创建失败时抛出错误
    */
-  async findOrCreateUser(userInfo: FeishuUserInfo, company: string): Promise<any> {
+  async findOrCreateUser(userInfo: FeishuUserInfo): Promise<any> {
     try {
-      let username = `${company}_${userInfo.openId}`;
+      let username = `feishu_${userInfo.openId}`;
 
-      let user = await this.findUserByUnionId(userInfo.unionId, company);
+      let user = await this.findUserByUnionId(userInfo.unionId);
 
       if (!user) {
-        user = await this.findUserByOpenId(userInfo.openId, company);
+        user = await this.findUserByOpenId(userInfo.openId);
       }
 
       if (!user) {
@@ -123,7 +112,7 @@ export class FeishuUserManager {
       }
 
       if (!user && userInfo.mobile) {
-        user = await this.findUserByMobile(userInfo.mobile, company);
+        user = await this.findUserByMobile(userInfo.mobile);
       }
 
       if (user) {
@@ -133,7 +122,7 @@ export class FeishuUserManager {
       }
 
       console.log(`创建新用户,unionId: ${userInfo.unionId}`);
-      return await this.createNewUser(userInfo, company);
+      return await this.createNewUser(userInfo);
 
     } catch (error) {
       console.error('查找或创建用户失败:', error);
@@ -159,10 +148,9 @@ export class FeishuUserManager {
    *
    * @async
    * @param unionId - 飞书用户unionId
-   * @param company - 公司ID
    * @returns Promise<any | null>
    */
-  async findUserByUnionId(unionId: string, company: string): Promise<any | null> {
+  async findUserByUnionId(unionId: string): Promise<any | null> {
     try {
       const Parse = (global as any).Parse;
       if (!Parse) {
@@ -171,11 +159,6 @@ export class FeishuUserManager {
 
       const query = new Parse.Query("_User");
       query.equalTo("data.feishu.unionId", unionId);
-      query.equalTo("company", {
-        __type: 'Pointer',
-        className: 'Company',
-        objectId: company
-      });
 
       return await query.first({ useMasterKey: true });
     } catch (error) {
@@ -189,10 +172,9 @@ export class FeishuUserManager {
    *
    * @async
    * @param openId - 飞书用户openId
-   * @param company - 公司ID
    * @returns Promise<any | null>
    */
-  async findUserByOpenId(openId: string, company: string): Promise<any | null> {
+  async findUserByOpenId(openId: string): Promise<any | null> {
     try {
       const Parse = (global as any).Parse;
       if (!Parse) {
@@ -201,11 +183,6 @@ export class FeishuUserManager {
 
       const query = new Parse.Query("_User");
       query.equalTo("data.feishu.openId", openId);
-      query.equalTo("company", {
-        __type: 'Pointer',
-        className: 'Company',
-        objectId: company
-      });
 
       return await query.first({ useMasterKey: true });
     } catch (error) {
@@ -243,10 +220,9 @@ export class FeishuUserManager {
    *
    * @async
    * @param mobile - 手机号
-   * @param company - 公司ID
    * @returns Promise<any | null>
    */
-  async findUserByMobile(mobile: string, company: string): Promise<any | null> {
+  async findUserByMobile(mobile: string): Promise<any | null> {
     try {
       const Parse = (global as any).Parse;
       if (!Parse) {
@@ -255,11 +231,6 @@ export class FeishuUserManager {
 
       const query = new Parse.Query("_User");
       query.equalTo("mobile", mobile);
-      query.equalTo("company", {
-        __type: 'Pointer',
-        className: 'Company',
-        objectId: company
-      });
 
       return await query.first({ useMasterKey: true });
     } catch (error) {
@@ -315,11 +286,10 @@ export class FeishuUserManager {
    *
    * @async
    * @param userInfo - 飞书用户信息
-   * @param company - 公司ID
    * @returns Promise<any> Parse用户对象
    * @throws {Error} 创建失败时抛出错误
    */
-  async createNewUser(userInfo: FeishuUserInfo, company: string): Promise<any> {
+  async createNewUser(userInfo: FeishuUserInfo): Promise<any> {
     try {
       const Parse = (global as any).Parse;
       if (!Parse) {
@@ -329,7 +299,7 @@ export class FeishuUserManager {
       const UserClass = Parse.Object.extend("_User");
       const user = new UserClass();
 
-      const username = `${company}_${userInfo.openId}`;
+      const username = `feishu_${userInfo.openId}`;
       const defaultPassword = this.generateDefaultPassword(username);
 
       user.set("username", username);
@@ -340,12 +310,6 @@ export class FeishuUserManager {
       user.set("type", 'user');
       user.set("status", 'normal');
 
-      user.set("company", {
-        __type: 'Pointer',
-        className: 'Company',
-        objectId: company
-      });
-
       user.set("data", {
         feishu: {
           unionId: userInfo.unionId,
@@ -372,7 +336,7 @@ export class FeishuUserManager {
   }
 
   /**
-   * 生成sessionToken
+   * 生成sessionToken(优化版:复用未过期的token)
    *
    * @async
    * @param user - Parse用户对象
@@ -386,14 +350,41 @@ export class FeishuUserManager {
         throw new Error('Parse不可用');
       }
 
-      const SessionClass = Parse.Object.extend('ession');
+      // 查询用户现有的有效 sessionToken
+      const query = new Parse.Query('_Session');
+      query.equalTo('user', {
+        __type: 'Pointer',
+        className: '_User',
+        objectId: user.id
+      });
+      query.greaterThan('expiresAt', new Date());
+      query.descending('expiresAt');
+
+      const existingSession = await query.first({ useMasterKey: true });
+
+      // 如果存在有效的 session,检查剩余有效期
+      if (existingSession) {
+        const expiresAt = existingSession.get('expiresAt');
+        const now = new Date();
+        const remainingTime = expiresAt.getTime() - now.getTime();
+        const twoHoursInMs = 2 * 60 * 60 * 1000; // 2小时的毫秒数
+
+        // 如果剩余有效期大于等于2小时,复用现有的 sessionToken
+        if (remainingTime >= twoHoursInMs) {
+          console.log(`复用现有SessionToken for user: ${user.id}, 剩余有效期: ${(remainingTime / 1000 / 60 / 60).toFixed(2)}小时`);
+          return existingSession;
+        }
+      }
+
+      // 创建新的 sessionToken
+      const SessionClass = Parse.Object.extend('_Session');
       const session = new SessionClass();
 
       const salt = user.id + '_' + (new Date().getTime() / 1000).toFixed();
       const md5 = crypto.createHash('md5').update(salt, 'utf8').digest('hex');
       const sessionToken = "r:" + md5;
 
-      console.log(`生成SessionToken: ${sessionToken} for user: ${user.id}`);
+      console.log(`生成SessionToken: ${sessionToken} for user: ${user.id}`);
 
       session.set("user", {
         __type: 'Pointer',