package auth import ( "context" "crypto/rand" "encoding/hex" "errors" "strings" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "golang.org/x/crypto/bcrypt" "github.com/yuxingu/digital-psychology/apps/api/internal/repository" ) // Service handles register/login sessions. // Temporary open mode: any non-empty phone+password can enter; missing accounts are created. type Service struct { Repo *repository.AuthRepo } // Me is the public account payload. type Me struct { ID string `json:"id"` Phone string `json:"phone"` Nickname string `json:"nickname"` } // SessionResult is returned after register/login. type SessionResult struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` User Me `json:"user"` } // Register upgrades or opens an account (same open rules as Login). func (s *Service) Register(ctx context.Context, userID uuid.UUID, deviceKey, phone, password, nickname string) (*SessionResult, error) { return s.OpenLogin(ctx, userID, deviceKey, phone, password, nickname) } // Login authenticates in open mode (no password check; auto-create). func (s *Service) Login(ctx context.Context, userID uuid.UUID, deviceKey, phone, password string) (*SessionResult, error) { return s.OpenLogin(ctx, userID, deviceKey, phone, password, "") } // OpenLogin: any phone+password accepted; persist account; issue session. func (s *Service) OpenLogin(ctx context.Context, deviceUserID uuid.UUID, deviceKey, phone, password, nickname string) (*SessionResult, error) { phone = strings.TrimSpace(phone) if phone == "" { return nil, errors.New("请填写手机号") } nickname = strings.TrimSpace(nickname) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, err } hashStr := string(hash) acc, err := s.Repo.GetByPhone(ctx, phone) if err == nil { _ = s.Repo.TouchPassword(ctx, acc.ID, hashStr) if deviceKey != "" { _ = s.Repo.BindDevice(ctx, deviceKey, acc.ID) } nick := acc.Nickname if nickname != "" { nick = nickname } return s.issue(ctx, acc.ID, acc.Phone, nick) } if !errors.Is(err, pgx.ErrNoRows) { return nil, err } // New phone: prefer upgrading anonymous device user. cur, curErr := s.Repo.GetAccount(ctx, deviceUserID) uid := deviceUserID if curErr == nil && cur.Phone == "" { if err := s.Repo.RegisterOnUser(ctx, deviceUserID, phone, hashStr, nickname); err != nil { return nil, errors.New("登录失败,请重试") } } else { uid, err = s.Repo.CreateUserWithPhone(ctx, phone, hashStr, nickname) if err != nil { return nil, errors.New("登录失败,请重试") } } if deviceKey != "" { _ = s.Repo.BindDevice(ctx, deviceKey, uid) } return s.issue(ctx, uid, phone, nickname) } // Logout revokes the current bearer session and unbinds the device from the account // so a refresh no longer resolves as logged-in via X-Device-Key (Spec R5). func (s *Service) Logout(ctx context.Context, token, deviceKey string) error { if token != "" { if err := s.Repo.RevokeSession(ctx, token); err != nil { return err } } return s.Repo.RebindDeviceAnonymous(ctx, deviceKey) } // Me returns account if registered. func (s *Service) Me(ctx context.Context, userID uuid.UUID) (*Me, error) { acc, err := s.Repo.GetAccount(ctx, userID) if err != nil { return nil, err } if acc.Phone == "" { return nil, errors.New("未登录") } return &Me{ID: acc.ID.String(), Phone: maskPhone(acc.Phone), Nickname: acc.Nickname}, nil } // ResolveSessionUser returns user id for a live token. func (s *Service) ResolveSessionUser(ctx context.Context, token string) (uuid.UUID, error) { return s.Repo.UserIDByToken(ctx, token) } // IsRegistered checks phone present. func (s *Service) IsRegistered(ctx context.Context, userID uuid.UUID) (bool, error) { return s.Repo.IsRegistered(ctx, userID) } func (s *Service) issue(ctx context.Context, userID uuid.UUID, phone, nickname string) (*SessionResult, error) { tok := "usr_" + randomHex(24) exp := time.Now().Add(30 * 24 * time.Hour) if err := s.Repo.CreateSession(ctx, userID, tok, exp); err != nil { return nil, err } return &SessionResult{ Token: tok, ExpiresAt: exp, User: Me{ID: userID.String(), Phone: maskPhone(phone), Nickname: nickname}, }, nil } func maskPhone(p string) string { if len(p) != 11 { return p } return p[:3] + "****" + p[7:] } func randomHex(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return hex.EncodeToString(b) }