| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 |
- /**
- * 飞书用户管理器模块
- *
- * 功能说明:
- * - 负责飞书OAuth2登录用户的获取或创建
- * - SessionToken管理
- * - 用户信息同步和更新
- *
- * @module user-manager
- * @author fmode
- * @date 2026
- */
- import crypto from 'crypto';
- import type { FeishuConfig } from './config.ts';
- import type { FeishuUserInfo } from './client.ts';
- /**
- * 用户会话信息接口
- */
- export interface UserSessionInfo {
- user: any;
- sessionToken: any;
- userInfo: {
- objectId: string;
- username: string;
- nickname: string;
- mobile?: string;
- email?: string;
- avatar: string;
- sessionToken: string;
- };
- }
- /**
- * 飞书用户管理器类
- * 负责用户的查找、创建、更新和sessionToken管理
- */
- export class FeishuUserManager {
- private configManager: any;
- /**
- * 构造函数
- * @param configManager - 配置管理器实例(必填)
- */
- constructor(configManager: FeishuConfig) {
- this.configManager = configManager;
- }
- /**
- * 根据飞书用户信息获取或创建用户,并生成sessionToken
- *
- * @async
- * @param userInfo - 飞书用户信息
- * @param appId - 飞书应用ID
- * @returns Promise<UserSessionInfo>
- * @throws {Error} 处理失败时抛出错误
- */
- async getUserInfoSessionToken(userInfo: FeishuUserInfo, appId: string): Promise<UserSessionInfo> {
- try {
- console.log(`处理飞书用户登录,unionId: ${userInfo.unionId}, openId: ${userInfo.openId}`);
- let user = await this.findOrCreateUser(userInfo);
- const sessionToken = await this.generateSessionToken(user);
- await this.saveUserData(user, userInfo);
- console.log(`用户登录成功,userId: ${user.id}, username: ${user.get('username')}`);
- return {
- user: user,
- sessionToken: sessionToken,
- userInfo: {
- objectId: user.id,
- username: user.get('username'),
- nickname: user.get('nickname'),
- mobile: user.get('mobile'),
- email: user.get('email'),
- avatar: user.get('avatar'),
- sessionToken: sessionToken.get('sessionToken'),
- }
- };
- } catch (error) {
- console.error('获取用户sessionToken失败:', error);
- throw error;
- }
- }
- /**
- * 查找或创建用户
- *
- * @async
- * @param userInfo - 飞书用户信息
- * @returns Promise<any> Parse用户对象
- * @throws {Error} 查找或创建失败时抛出错误
- */
- async findOrCreateUser(userInfo: FeishuUserInfo): Promise<any> {
- try {
- let username = `feishu_${userInfo.openId}`;
- let user = await this.findUserByUnionId(userInfo.unionId);
- if (!user) {
- user = await this.findUserByOpenId(userInfo.openId);
- }
- if (!user) {
- user = await this.findUserByUsername(username);
- }
- if (!user && userInfo.mobile) {
- user = await this.findUserByMobile(userInfo.mobile);
- }
- if (user) {
- console.log(`找到现有用户,更新信息: ${user.id}`);
- await this.updateUserInfo(user, userInfo);
- return user;
- }
- console.log(`创建新用户,unionId: ${userInfo.unionId}`);
- return await this.createNewUser(userInfo);
- } catch (error) {
- console.error('查找或创建用户失败:', error);
- throw error;
- }
- }
- /**
- * 生成用户默认密码
- *
- * @param username - 用户名
- * @returns 默认密码
- */
- generateDefaultPassword(username: string): string {
- if (!username || username.length < 6) {
- return username.padStart(6, '0');
- }
- return username.slice(-6);
- }
- /**
- * 通过unionId查找用户
- *
- * @async
- * @param unionId - 飞书用户unionId
- * @returns Promise<any | null>
- */
- async findUserByUnionId(unionId: string): Promise<any | null> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- const query = new Parse.Query("_User");
- query.equalTo("data.feishu.unionId", unionId);
- return await query.first({ useMasterKey: true });
- } catch (error) {
- console.error('通过unionId查找用户失败:', error);
- return null;
- }
- }
- /**
- * 通过openId查找用户
- *
- * @async
- * @param openId - 飞书用户openId
- * @returns Promise<any | null>
- */
- async findUserByOpenId(openId: string): Promise<any | null> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- const query = new Parse.Query("_User");
- query.equalTo("data.feishu.openId", openId);
- return await query.first({ useMasterKey: true });
- } catch (error) {
- console.error('通过openId查找用户失败:', error);
- return null;
- }
- }
- /**
- * 通过用户名查找用户
- *
- * @async
- * @param username - 用户名
- * @returns Promise<any | null>
- */
- async findUserByUsername(username: string): Promise<any | null> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- const query = new Parse.Query("_User");
- query.equalTo("username", username);
- return await query.first({ useMasterKey: true });
- } catch (error) {
- console.error('通过username查找用户失败:', error);
- return null;
- }
- }
- /**
- * 通过手机号查找用户
- *
- * @async
- * @param mobile - 手机号
- * @returns Promise<any | null>
- */
- async findUserByMobile(mobile: string): Promise<any | null> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- const query = new Parse.Query("_User");
- query.equalTo("mobile", mobile);
- return await query.first({ useMasterKey: true });
- } catch (error) {
- console.error('通过手机号查找用户失败:', error);
- return null;
- }
- }
- /**
- * 更新现有用户信息
- *
- * @async
- * @param user - Parse用户对象
- * @param userInfo - 飞书用户信息
- * @returns Promise<void>
- * @throws {Error} 更新失败或用户被冻结时抛出错误
- */
- async updateUserInfo(user: any, userInfo: FeishuUserInfo): Promise<void> {
- try {
- if (userInfo.name) user.set('nickname', userInfo.name);
- if (userInfo.mobile) user.set('mobile', userInfo.mobile);
- if (userInfo.avatarUrl) user.set('avatar', userInfo.avatarUrl);
- const data = user.get('data') || {};
- data.feishu = {
- unionId: userInfo.unionId,
- openId: userInfo.openId,
- name: userInfo.name,
- enName: userInfo.enName,
- avatarUrl: userInfo.avatarUrl,
- mobile: userInfo.mobile,
- email: userInfo.email,
- userId: userInfo.userId,
- lastLoginAt: new Date(),
- updatedAt: new Date()
- };
- user.set('data', data);
- if (user.get('status') === 'freeze') {
- throw new Error('该账户已被冻结');
- }
- await user.save(null, { useMasterKey: true });
- console.log(`用户信息更新成功: ${user.id}`);
- } catch (error) {
- console.error('更新用户信息失败:', error);
- throw error;
- }
- }
- /**
- * 创建新用户
- *
- * @async
- * @param userInfo - 飞书用户信息
- * @returns Promise<any> Parse用户对象
- * @throws {Error} 创建失败时抛出错误
- */
- async createNewUser(userInfo: FeishuUserInfo): Promise<any> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- const UserClass = Parse.Object.extend("_User");
- const user = new UserClass();
- const username = `feishu_${userInfo.openId}`;
- const defaultPassword = this.generateDefaultPassword(username);
- user.set("username", username);
- user.set("password", defaultPassword);
- user.set("nickname", userInfo.name || `飞书用户_${userInfo.openId.slice(-6)}`);
- user.set("mobile", userInfo.mobile);
- user.set("avatar", userInfo.avatarUrl || 'https://s1-imfile.feishucdn.com/static-resource/v1/default_avatar.png');
- user.set("type", 'user');
- user.set("status", 'normal');
- user.set("data", {
- feishu: {
- unionId: userInfo.unionId,
- openId: userInfo.openId,
- name: userInfo.name,
- enName: userInfo.enName,
- avatarUrl: userInfo.avatarUrl,
- mobile: userInfo.mobile,
- email: userInfo.email,
- userId: userInfo.userId,
- createdAt: new Date(),
- lastLoginAt: new Date(),
- }
- });
- await user.save(null, { useMasterKey: true });
- console.log(`新用户创建成功: ${user.id}, username: ${username}, 默认密码: ${defaultPassword}`);
- return user;
- } catch (error) {
- console.error('创建新用户失败:', error);
- throw error;
- }
- }
- /**
- * 生成sessionToken(优化版:复用未过期的token)
- *
- * @async
- * @param user - Parse用户对象
- * @returns Promise<any> Session对象
- * @throws {Error} 生成失败时抛出错误
- */
- async generateSessionToken(user: any): Promise<any> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- // 查询用户现有的有效 sessionToken(剩余有效期大于2小时)
- const twoHoursLater = new Date();
- twoHoursLater.setHours(twoHoursLater.getHours() + 2);
- const query = new Parse.Query('_Session');
- query.equalTo('user', {
- __type: 'Pointer',
- className: '_User',
- objectId: user.id
- });
- query.greaterThan('expiresAt', twoHoursLater);
- query.descending('expiresAt');
- const existingSession = await query.first({ useMasterKey: true });
- // 如果找到剩余有效期大于2小时的 session,直接复用
- if (existingSession) {
- console.log(`复用现有SessionToken for user: ${user.id}`);
- return existingSession;
- }
- // 创建新的 sessionToken
- 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}`);
- const expiresAt = new Date();
- expiresAt.setFullYear(expiresAt.getFullYear() + 1);
- // 使用 REST API 创建 Session,绕过 SDK 的只读属性限制
- const serverURL = Parse.serverURL || (globalThis as any).appConfig.serverURL;
- const appId = Parse.applicationId || (globalThis as any)?.appConfig.appId;
- const masterKey = Parse.masterKey || (globalThis as any)?.appConfig.masterKey;
- const response = await fetch(`${serverURL}/classes/_Session`, {
- method: 'POST',
- headers: {
- 'X-Parse-Application-Id': appId,
- 'X-Parse-Master-Key': masterKey,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- user: {
- __type: 'Pointer',
- className: '_User',
- objectId: user.id
- },
- sessionToken: sessionToken,
- expiresAt: {
- __type: 'Date',
- iso: expiresAt.toISOString()
- },
- createdWith: {
- action: "login",
- authProvider: "feishu"
- },
- restricted: false
- })
- });
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(`Session创建失败: ${response.status} ${errorText}`);
- }
- const data = await response.json();
- if (!data || !data.objectId) {
- throw new Error('Session创建失败:未返回objectId');
- }
- // 查询并返回创建的 Session 对象
- const sessionQuery = new Parse.Query('_Session');
- sessionQuery.equalTo('objectId', data.objectId);
- const savedSession = await sessionQuery.first({ useMasterKey: true });
- if (!savedSession) {
- throw new Error('Session查询失败');
- }
- return savedSession;
- } catch (error) {
- console.error('生成sessionToken失败:', error);
- throw error;
- }
- }
- /**
- * 保存用户附加信息到data字段
- *
- * @async
- * @param user - Parse用户对象
- * @param userInfo - 飞书用户信息
- * @returns Promise<void>
- */
- async saveUserData(user: any, userInfo: FeishuUserInfo): Promise<void> {
- try {
- const data = user.get('data') || {};
- if (!data.feishu) {
- data.feishu = {};
- }
- data.feishu[userInfo.openId] = {
- ...userInfo,
- lastLoginAt: new Date(),
- updatedAt: new Date()
- };
- data.feishu.unionId = userInfo.unionId;
- data.feishu.currentOpenId = userInfo.openId;
- user.set('data', data);
- await user.save(null, { useMasterKey: true });
- console.log(`用户数据保存成功: ${user.id}`);
- } catch (error) {
- console.error('保存用户数据失败:', error);
- }
- }
- /**
- * 验证sessionToken
- *
- * @async
- * @param sessionToken - 会话令牌
- * @returns Promise<any | null>
- */
- async validateSessionToken(sessionToken: string): Promise<any | null> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- const query = new Parse.Query('_Session');
- query.equalTo('sessionToken', sessionToken);
- query.greaterThan('expiresAt', new Date());
- query.include('user');
- const session = await query.first({ useMasterKey: true });
- if (session && session.get('user')) {
- return session.get('user');
- }
- return null;
- } catch (error) {
- console.error('验证sessionToken失败:', error);
- return null;
- }
- }
- /**
- * 通过unionId查找所有关联用户
- *
- * @async
- * @param unionId - 飞书统一ID
- * @returns Promise<any[]>
- */
- async findUsersByUnionId(unionId: string): Promise<any[]> {
- try {
- const Parse = (global as any).Parse;
- if (!Parse) {
- throw new Error('Parse不可用');
- }
- const query = new Parse.Query("_User");
- query.equalTo("data.feishu.unionId", unionId);
- return await query.find({ useMasterKey: true });
- } catch (error) {
- console.error('通过unionId查找用户失败:', error);
- return [];
- }
- }
- }
- /**
- * 默认导出用户管理器类
- */
- export default FeishuUserManager;
|