package service import ( "context" "encoding/json" "errors" "fmt" "net/http" "github.com/cloudprint/cloudprint-backend/config" "github.com/cloudprint/cloudprint-backend/internal/model" "github.com/cloudprint/cloudprint-backend/internal/repository" "github.com/cloudprint/cloudprint-backend/pkg/jwt" "golang.org/x/crypto/bcrypt" ) type AuthService struct { userRepo *repository.UserRepository adminRepo *repository.AdminRepository jwtManager *jwt.JWTManager redis *RedisClient wxConfig *config.WeChatConfig } type RedisClient struct { // In production, use actual Redis client } func NewAuthService( userRepo *repository.UserRepository, adminRepo *repository.AdminRepository, jwtManager *jwt.JWTManager, redis *RedisClient, wxConfig *config.WeChatConfig, ) *AuthService { return &AuthService{ userRepo: userRepo, adminRepo: adminRepo, jwtManager: jwtManager, redis: redis, wxConfig: wxConfig, } } // WeChat code exchange response type WeChatCodeResponse struct { OpenID string `json:"openid"` SessionKey string `json:"session_key"` UnionID string `json:"unionid,omitempty"` ErrCode int `json:"errcode,omitempty"` ErrMsg string `json:"errmsg,omitempty"` } func (s *AuthService) Login(ctx context.Context, code string) (*LoginResult, error) { // Exchange code for openid from WeChat openid, err := s.getWeChatOpenID(code) if err != nil { return nil, fmt.Errorf("failed to get wechat openid: %w", err) } // Find or create user user, err := s.userRepo.GetByOpenid(ctx, openid) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } isNewUser := false if user == nil { isNewUser = true user = &model.User{ Openid: openid, } if err := s.userRepo.Create(ctx, user); err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } } // Generate tokens accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, "user", "") if err != nil { return nil, fmt.Errorf("failed to generate access token: %w", err) } refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, "user") if err != nil { return nil, fmt.Errorf("failed to generate refresh token: %w", err) } return &LoginResult{ AccessToken: accessToken, RefreshToken: refreshToken, User: user, IsNewUser: isNewUser, }, nil } func (s *AuthService) getWeChatOpenID(code string) (string, error) { url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", s.wxConfig.AppID, s.wxConfig.AppSecret, code) resp, err := http.Get(url) if err != nil { return "", err } defer resp.Body.Close() var result WeChatCodeResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } if result.ErrCode != 0 { return "", fmt.Errorf("wechat error: %s", result.ErrMsg) } return result.OpenID, nil } func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (string, error) { return s.jwtManager.RefreshAccessToken(refreshToken) } func (s *AuthService) GetUserInfo(ctx context.Context, userID int32) (*model.User, error) { return s.userRepo.GetByID(ctx, userID) } func (s *AuthService) UpdateUser(ctx context.Context, userID int32, nickname, avatar string) (*model.User, error) { user, err := s.userRepo.GetByID(ctx, userID) if err != nil || user == nil { return nil, errors.New("user not found") } user.Nickname = nickname if avatar != "" { user.Avatar = avatar } if err := s.userRepo.Update(ctx, user); err != nil { return nil, err } return user, nil } type LoginResult struct { AccessToken string RefreshToken string User *model.User IsNewUser bool } // Admin login func (s *AuthService) AdminLogin(ctx context.Context, username, password string) (*AdminLoginResult, error) { admin, err := s.adminRepo.GetByUsername(ctx, username) if err != nil { return nil, fmt.Errorf("failed to get admin: %w", err) } if admin == nil { return nil, errors.New("invalid credentials") } // Verify password if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(password)); err != nil { return nil, errors.New("invalid credentials") } // Generate token accessToken, err := s.jwtManager.GenerateAccessToken(admin.ID, "admin", admin.Username) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } return &AdminLoginResult{ AccessToken: accessToken, Admin: admin, }, nil } type AdminLoginResult struct { AccessToken string Admin *model.Admin } // HashPassword creates a bcrypt hash of the password func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err }