const axios = require('axios'); const User = require('../models/User'); const LoginHistory = require('../models/LoginHistory'); const Invitation = require('../models/Invitation'); const { generateToken, generateRefreshToken } = require('../utils/jwt'); const env = require('../config/env'); const configService = require('./configService'); /** * Authentication Service * Handles WeChat login and token management */ /** * Generate unique invitation code * @returns {string} 8-character invitation code */ const generateInvitationCode = () => { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let code = ''; for (let i = 0; i < 8; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)); } return code; }; /** * Generate random suffix for default nickname * @returns {string} 4-character random suffix (lowercase letters and digits) */ const generateNicknameSuffix = () => { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let suffix = ''; for (let i = 0; i < 4; i++) { suffix += chars.charAt(Math.floor(Math.random() * chars.length)); } return suffix; }; /** * Authenticate user with WeChat * @param {string} code - WeChat authorization code * @param {Object} userInfo - WeChat user info (nickname, avatar) * @param {string} invitationCode - Optional invitation code * @param {Object} deviceInfo - Device information * @returns {Object} User and tokens */ const wechatLogin = async (code, userInfo = {}, invitationCode = null, deviceInfo = {}) => { // Exchange code for openId and session_key from WeChat // In production, this would call WeChat API // For now, we'll simulate it let openId; if (env.nodeEnv === 'test' || env.nodeEnv === 'development') { // For testing/development, use a fixed test openId to avoid creating duplicate users // In real WeChat environment, the code would be exchanged for a consistent openId openId = 'test_user_' + (userInfo.nickname || 'default'); } else { // Production: Call WeChat API const wechatUrl = `https://api.weixin.qq.com/sns/jscode2session`; const params = { appid: env.wechat.appId, secret: env.wechat.appSecret, js_code: code, grant_type: 'authorization_code', }; const response = await axios.get(wechatUrl, { params }); if (response.data.errcode) { throw new Error(`WeChat authentication failed: ${response.data.errmsg}`); } openId = response.data.openid; } // Find or create user let user = await User.findOne({ where: { wechatOpenId: openId } }); if (!user) { // Get default avatar from config let defaultAvatar = null; try { const avatarConfig = await configService.getConfig('default_avatar'); if (avatarConfig && avatarConfig.value) { defaultAvatar = avatarConfig.value; } } catch (e) { console.error('Failed to get default avatar config:', e); } console.log('=== Creating New User ==='); console.log('OpenId:', openId); console.log('InvitationCode received:', invitationCode); // Create new user const userData = { wechatOpenId: openId, nickname: userInfo.nickname || `User_${generateNicknameSuffix()}`, avatar: userInfo.avatar || defaultAvatar, invitationCode: await generateUniqueInvitationCode(), }; // Handle invitation let inviterId = null; if (invitationCode) { console.log('Looking for inviter with code:', invitationCode); const inviter = await User.findOne({ where: { invitationCode } }); console.log('Inviter found:', inviter ? inviter.id : 'NOT FOUND'); if (inviter) { userData.invitedBy = inviter.id; inviterId = inviter.id; console.log('Setting invitedBy to:', inviter.id); } } user = await User.create(userData); console.log('New user created:', user.id, 'invitedBy:', user.invitedBy); // Record invitation relationship if (inviterId) { await Invitation.create({ inviterId, inviteeId: user.id, invitationCode, registeredAt: new Date(), rewardStatus: 'pending', }); console.log('Invitation record created'); } } else { // Update existing user info if provided if (userInfo.nickname) user.nickname = userInfo.nickname; if (userInfo.avatar) user.avatar = userInfo.avatar; // Handle invitation for existing user who hasn't been invited yet if (invitationCode && !user.invitedBy) { const inviter = await User.findOne({ where: { invitationCode } }); if (inviter && inviter.id !== user.id) { // Bind invitation relationship user.invitedBy = inviter.id; // Record invitation relationship await Invitation.create({ inviterId: inviter.id, inviteeId: user.id, invitationCode, registeredAt: new Date(), rewardStatus: 'pending', }); console.log(`User ${user.id} bound to inviter ${inviter.id} via code ${invitationCode}`); } } await user.save(); } // Generate tokens const accessToken = generateToken({ userId: user.id, type: 'user', }); const refreshToken = generateRefreshToken({ userId: user.id, type: 'user', }); // Record login history await LoginHistory.create({ userId: user.id, userType: 'user', ipAddress: deviceInfo.ipAddress || null, userAgent: deviceInfo.userAgent || null, deviceInfo: deviceInfo.device || null, loginAt: new Date(), }); return { user, accessToken, refreshToken, }; }; /** * Generate unique invitation code (check for duplicates) * @returns {string} Unique invitation code */ const generateUniqueInvitationCode = async () => { let code; let exists = true; while (exists) { code = generateInvitationCode(); const user = await User.findOne({ where: { invitationCode: code } }); exists = !!user; } return code; }; /** * Refresh access token * @param {string} refreshToken - Refresh token * @returns {Object} New access token */ const refreshAccessToken = async (refreshToken) => { const { verifyToken } = require('../utils/jwt'); // Verify refresh token let decoded; try { decoded = verifyToken(refreshToken); } catch (error) { throw new Error('Invalid or expired refresh token'); } // Check token type if (decoded.type !== 'user') { throw new Error('Invalid token type'); } // Verify user still exists and is active const user = await User.findByPk(decoded.userId); if (!user) { throw new Error('User not found'); } if (user.status === 'suspended') { throw new Error('Account suspended'); } // Generate new access token const accessToken = generateToken({ userId: user.id, type: 'user', }); return { accessToken, user, }; }; module.exports = { wechatLogin, refreshAccessToken, generateInvitationCode, generateUniqueInvitationCode, };