254 lines
6.9 KiB
JavaScript
254 lines
6.9 KiB
JavaScript
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,
|
|
};
|