Files
digital-psychology/apps/api/internal/service/auth/service.go
T
jackyu66gitandCursor 7ab9add5dd
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s
feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 02:26:16 +08:00

150 lines
4.2 KiB
Go

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 bearer token.
func (s *Service) Logout(ctx context.Context, token string) error {
if token == "" {
return nil
}
return s.Repo.RevokeSession(ctx, token)
}
// 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)
}