package service import ( "context" "encoding/json" "errors" "fmt" "math/rand" "net/http" "net/url" "gitea.com/texpixel/document_ai/config" model "gitea.com/texpixel/document_ai/internal/model/user" "gitea.com/texpixel/document_ai/internal/storage/cache" "gitea.com/texpixel/document_ai/internal/storage/dao" "gitea.com/texpixel/document_ai/pkg/common" "gitea.com/texpixel/document_ai/pkg/log" "gitea.com/texpixel/document_ai/pkg/sms" "golang.org/x/crypto/bcrypt" ) type UserService struct { userDao *dao.UserDao } func NewUserService() *UserService { return &UserService{ userDao: dao.NewUserDao(), } } func (svc *UserService) GetSmsCode(ctx context.Context, phone string) (string, error) { limit, err := cache.GetUserSendSmsLimit(ctx, phone) if err != nil { log.Error(ctx, "func", "GetSmsCode", "msg", "get user send sms limit error", "error", err) return "", err } if limit >= cache.UserSendSmsLimitCount { return "", errors.New("sms code send limit reached") } user, err := svc.userDao.GetByPhone(dao.DB.WithContext(ctx), phone) if err != nil { log.Error(ctx, "func", "GetSmsCode", "msg", "get user error", "error", err) return "", err } if user == nil { user = &dao.User{Phone: phone} err = svc.userDao.Create(dao.DB.WithContext(ctx), user) if err != nil { log.Error(ctx, "func", "GetSmsCode", "msg", "create user error", "error", err) return "", err } } code := fmt.Sprintf("%06d", rand.Intn(1000000)) err = sms.SendMessage(&sms.SendSmsRequest{PhoneNumbers: phone, SignName: sms.Signature, TemplateCode: sms.TemplateCode, TemplateParam: fmt.Sprintf(sms.TemplateParam, code)}) if err != nil { log.Error(ctx, "func", "GetSmsCode", "msg", "send message error", "error", err) return "", err } cacheErr := cache.SetUserSmsCode(ctx, phone, code) if cacheErr != nil { log.Error(ctx, "func", "GetSmsCode", "msg", "set user sms code error", "error", cacheErr) } cacheErr = cache.SetUserSendSmsLimit(ctx, phone) if cacheErr != nil { log.Error(ctx, "func", "GetSmsCode", "msg", "set user send sms limit error", "error", cacheErr) } return code, nil } func (svc *UserService) VerifySmsCode(ctx context.Context, phone, code string) (uid int64, err error) { user, err := svc.userDao.GetByPhone(dao.DB.WithContext(ctx), phone) if err != nil { log.Error(ctx, "func", "VerifySmsCode", "msg", "get user error", "error", err, "phone", phone) return 0, err } if user == nil { log.Error(ctx, "func", "VerifySmsCode", "msg", "user not found", "phone", phone) return 0, errors.New("user not found") } storedCode, err := cache.GetUserSmsCode(ctx, phone) if err != nil { log.Error(ctx, "func", "VerifySmsCode", "msg", "get user sms code error", "error", err) return 0, err } if storedCode != code { log.Error(ctx, "func", "VerifySmsCode", "msg", "invalid sms code", "phone", phone, "code", code, "storedCode", storedCode) return 0, errors.New("invalid sms code") } cacheErr := cache.DeleteUserSmsCode(ctx, phone) if cacheErr != nil { log.Error(ctx, "func", "VerifySmsCode", "msg", "delete user sms code error", "error", cacheErr) } return user.ID, nil } func (svc *UserService) GetUserInfo(ctx context.Context, uid int64) (*dao.User, error) { user, err := svc.userDao.GetByID(dao.DB.WithContext(ctx), uid) if err != nil { log.Error(ctx, "func", "GetUserInfo", "msg", "get user error", "error", err) return nil, err } if user == nil { log.Warn(ctx, "func", "GetUserInfo", "msg", "user not found", "uid", uid) return &dao.User{}, nil } return user, nil } func (svc *UserService) RegisterByEmail(ctx context.Context, email, password string) (uid int64, err error) { existingUser, err := svc.userDao.GetByEmail(dao.DB.WithContext(ctx), email) if err != nil { log.Error(ctx, "func", "RegisterByEmail", "msg", "get user by email error", "error", err) return 0, err } if existingUser != nil { log.Warn(ctx, "func", "RegisterByEmail", "msg", "email already registered", "email", email) return 0, common.ErrEmailExists } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { log.Error(ctx, "func", "RegisterByEmail", "msg", "hash password error", "error", err) return 0, err } user := &dao.User{ Email: email, Password: string(hashedPassword), } err = svc.userDao.Create(dao.DB.WithContext(ctx), user) if err != nil { log.Error(ctx, "func", "RegisterByEmail", "msg", "create user error", "error", err) return 0, err } return user.ID, nil } func (svc *UserService) LoginByEmail(ctx context.Context, email, password string) (uid int64, err error) { user, err := svc.userDao.GetByEmail(dao.DB.WithContext(ctx), email) if err != nil { log.Error(ctx, "func", "LoginByEmail", "msg", "get user by email error", "error", err) return 0, err } if user == nil { log.Warn(ctx, "func", "LoginByEmail", "msg", "user not found", "email", email) return 0, common.ErrEmailNotFound } err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { log.Warn(ctx, "func", "LoginByEmail", "msg", "password mismatch", "email", email) return 0, common.ErrPasswordMismatch } return user.ID, nil } type googleTokenResponse struct { AccessToken string `json:"access_token"` IDToken string `json:"id_token"` ExpiresIn int `json:"expires_in"` TokenType string `json:"token_type"` } func (svc *UserService) googleHTTPClient() *http.Client { if config.GlobalConfig.Google.Proxy == "" { return &http.Client{} } proxyURL, err := url.Parse(config.GlobalConfig.Google.Proxy) if err != nil { return &http.Client{} } return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} } func (svc *UserService) ExchangeGoogleCodeAndGetUserInfo(ctx context.Context, clientID, clientSecret, code, redirectURI string) (*model.GoogleUserInfo, error) { tokenURL := "https://oauth2.googleapis.com/token" formData := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, "code": {code}, "grant_type": {"authorization_code"}, "redirect_uri": {redirectURI}, } client := svc.googleHTTPClient() resp, err := client.PostForm(tokenURL, formData) if err != nil { log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "exchange code failed", "error", err) return nil, err } defer resp.Body.Close() var tokenResp googleTokenResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "decode token response failed", "error", err) return nil, err } if tokenResp.AccessToken == "" { log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "no access token in response") return nil, errors.New("no access token in response") } userInfo, err := svc.getGoogleUserInfo(ctx, tokenResp.AccessToken) if err != nil { log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "get user info failed", "error", err) return nil, err } return &model.GoogleUserInfo{ ID: userInfo.ID, Email: userInfo.Email, Name: userInfo.Name, }, nil } func (svc *UserService) getGoogleUserInfo(ctx context.Context, accessToken string) (*model.GoogleUserInfo, error) { req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) client := svc.googleHTTPClient() resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var userInfo model.GoogleUserInfo if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { return nil, err } return &userInfo, nil } func (svc *UserService) FindOrCreateGoogleUser(ctx context.Context, userInfo *model.GoogleUserInfo) (uid int64, err error) { existingUser, err := svc.userDao.GetByGoogleID(dao.DB.WithContext(ctx), userInfo.ID) if err != nil { log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "get user by google id error", "error", err) return 0, err } if existingUser != nil { return existingUser.ID, nil } existingUser, err = svc.userDao.GetByEmail(dao.DB.WithContext(ctx), userInfo.Email) if err != nil { log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "get user by email error", "error", err) return 0, err } if existingUser != nil { existingUser.GoogleID = userInfo.ID err = svc.userDao.Update(dao.DB.WithContext(ctx), existingUser) if err != nil { log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "update user google id error", "error", err) return 0, err } return existingUser.ID, nil } user := &dao.User{ Email: userInfo.Email, GoogleID: userInfo.ID, Username: userInfo.Name, } err = svc.userDao.Create(dao.DB.WithContext(ctx), user) if err != nil { log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "create user error", "error", err) return 0, err } return user.ID, nil }