187 lines
4.6 KiB
Go
187 lines
4.6 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
|
||
smsentity "spark-server/internal/module/sms/entity"
|
||
"spark-server/internal/module/user/entity"
|
||
"spark-server/internal/pkg/errs"
|
||
"spark-server/internal/pkg/i18n"
|
||
"spark-server/internal/pkg/reqctx"
|
||
)
|
||
|
||
// AuthenticateEmail verifies OTP (test01–test10@spark.com accept any code),
|
||
// creates the user on first login, and upserts the login device.
|
||
func (s *Service) AuthenticateEmail(ctx context.Context, email, verifyCode string, device *entity.LoginDevice) (*entity.User, error) {
|
||
if s == nil || s.repo == nil || s.ids == nil {
|
||
return nil, errs.ErrUnauthorized
|
||
}
|
||
email = entity.NormalizeEmail(email)
|
||
if email == "" {
|
||
return nil, errs.ErrUnauthorized
|
||
}
|
||
if err := s.checkVerifyCode(ctx, email, verifyCode); err != nil {
|
||
return nil, err
|
||
}
|
||
u, err := s.getOrCreate(ctx, email)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err := s.ensureQRCode(ctx, u); err != nil {
|
||
return nil, err
|
||
}
|
||
s.ensureDefaultAvatar(ctx, u)
|
||
if u.Status != entity.StatusActive {
|
||
return nil, errs.ErrForbidden
|
||
}
|
||
if err := s.touchDevice(ctx, u.ID, device); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := s.recordLogin(ctx, u.ID, device); err != nil {
|
||
return nil, err
|
||
}
|
||
now := time.Now()
|
||
if err := s.repo.TouchLogin(ctx, u.ID, now); err != nil {
|
||
return nil, err
|
||
}
|
||
u.LastLoginAt = now
|
||
u.LastActiveAt = now
|
||
return u, nil
|
||
}
|
||
|
||
func (s *Service) checkVerifyCode(ctx context.Context, email, verifyCode string) error {
|
||
if entity.IsTestEmail(email) {
|
||
return nil
|
||
}
|
||
if s.otp == nil {
|
||
return errs.ErrAvailable
|
||
}
|
||
return s.otp.CheckAndConsume(ctx, smsentity.ChannelEmail, smsentity.PurposeLogin, email, verifyCode)
|
||
}
|
||
|
||
func (s *Service) getOrCreate(ctx context.Context, email string) (*entity.User, error) {
|
||
u, err := s.repo.GetByEmail(ctx, email)
|
||
if err == nil {
|
||
return u, nil
|
||
}
|
||
if !errors.Is(err, errs.ErrNotFound) {
|
||
return nil, err
|
||
}
|
||
return s.createWithEmail(ctx, email)
|
||
}
|
||
|
||
func (s *Service) createWithEmail(ctx context.Context, email string) (*entity.User, error) {
|
||
id, err := s.ids.NextID()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
now := time.Now()
|
||
qr, err := entity.NewQRCode()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
nick := nicknameFromEmail(email)
|
||
u := &entity.User{
|
||
ID: id,
|
||
SparkID: entity.DefaultSparkID(id),
|
||
Nickname: nick,
|
||
Avatar: entity.LetterAvatarDataURI(nick),
|
||
Gender: entity.GenderUnknown,
|
||
Status: entity.StatusActive,
|
||
QRCode: qr,
|
||
LastLoginAt: now,
|
||
LastActiveAt: now,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
if err := s.repo.CreateWithEmail(ctx, u, email); err != nil {
|
||
if errors.Is(err, errs.ErrDuplicate) {
|
||
return s.repo.GetByEmail(ctx, email)
|
||
}
|
||
return nil, err
|
||
}
|
||
s.stampDefaultAvatar(ctx, u)
|
||
return u, nil
|
||
}
|
||
|
||
func (s *Service) ensureQRCode(ctx context.Context, u *entity.User) error {
|
||
if u == nil || entity.ValidQRCode(u.QRCode) {
|
||
return nil
|
||
}
|
||
qr, err := entity.NewQRCode()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
u.QRCode = qr
|
||
return s.repo.UpdateQRCode(ctx, u.ID, u.QRCode)
|
||
}
|
||
|
||
func (s *Service) touchDevice(ctx context.Context, userID int64, device *entity.LoginDevice) error {
|
||
if device == nil || device.DeviceID == 0 {
|
||
return errs.ErrInvalidArgument
|
||
}
|
||
typ := entity.DeviceTypeOf(device)
|
||
if !entity.ValidDeviceType(typ) {
|
||
return errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyLoginDeviceTypeRequired, nil))
|
||
}
|
||
now := time.Now()
|
||
ip := strings.TrimSpace(device.RealIP)
|
||
if ip == "" {
|
||
ip = reqctx.RealIP(ctx)
|
||
}
|
||
lang := strings.TrimSpace(device.Language)
|
||
if lang == "" {
|
||
lang = reqctx.Language(ctx)
|
||
}
|
||
return s.repo.UpsertDevice(ctx, &entity.Device{
|
||
UserID: userID,
|
||
DeviceID: device.DeviceID,
|
||
DeviceType: typ,
|
||
DeviceName: device.DeviceName,
|
||
AppVersion: device.AppVersion,
|
||
Language: lang,
|
||
LastIP: ip,
|
||
LastLoginAt: now,
|
||
CreatedAt: now,
|
||
})
|
||
}
|
||
|
||
func (s *Service) recordLogin(ctx context.Context, userID int64, device *entity.LoginDevice) error {
|
||
if device == nil {
|
||
return errs.ErrInvalidArgument
|
||
}
|
||
ip := strings.TrimSpace(device.RealIP)
|
||
if ip == "" {
|
||
ip = reqctx.RealIP(ctx)
|
||
}
|
||
lang := strings.TrimSpace(device.Language)
|
||
if lang == "" {
|
||
lang = reqctx.Language(ctx)
|
||
}
|
||
return s.repo.InsertLoginLog(ctx, &entity.LoginLog{
|
||
UserID: userID,
|
||
DeviceID: device.DeviceID,
|
||
DeviceType: entity.DeviceTypeOf(device),
|
||
DeviceName: device.DeviceName,
|
||
AppVersion: device.AppVersion,
|
||
Language: lang,
|
||
IP: ip,
|
||
CreatedAt: time.Now(),
|
||
})
|
||
}
|
||
|
||
func nicknameFromEmail(email string) string {
|
||
local, _, _ := strings.Cut(email, "@")
|
||
local = strings.TrimSpace(local)
|
||
if local == "" {
|
||
return "user"
|
||
}
|
||
if len(local) > 64 {
|
||
return local[:64]
|
||
}
|
||
return local
|
||
}
|