appointment_system/backend/src/services/authService.js
2026-01-25 19:23:48 +08:00

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,
};