spark-server/internal/module/auth/logic/login.go
2026-09-22 17:07:26 +08:00

128 lines
4.3 KiB
Go

package logic
import (
"context"
authentity "spark-server/internal/module/auth/entity"
userentity "spark-server/internal/module/user/entity"
"spark-server/internal/pkg/errs"
"spark-server/internal/pkg/i18n"
"spark-server/internal/pkg/reqctx"
"spark-server/internal/pkg/validate"
)
// Verifier checks email OTP and records the login device. The user module owns the implementation.
type Verifier interface {
AuthenticateEmail(ctx context.Context, email, verifyCode string, device *userentity.LoginDevice) (*userentity.User, error)
}
// LoginInput is the HTTP/TCP-shared login body. Validation tags drive client-visible messages.
type LoginInput struct {
Email string `json:"email" validate:"required,email" label:"field.email" errmsg:"required:validate.login.email_required;email:validate.login.email_invalid"`
VerifyCode string `json:"verify_code" validate:"required" label:"field.verify_code" errmsg:"required:validate.login.code_required"`
DeviceID uint64 `json:"device_id" validate:"required" label:"field.device_id" errmsg:"required:validate.login.device_required"`
DeviceType string `json:"device_type" validate:"omitempty,oneof=ios android harmony windows macos web" label:"field.device_type" errmsg:"oneof:validate.login.device_type_invalid"`
Platform string `json:"platform" validate:"omitempty,oneof=ios android harmony windows macos web" label:"field.device_type" errmsg:"oneof:validate.login.device_type_invalid"`
DeviceName string `json:"device_name" validate:"omitempty,max=128" label:"field.device_name"`
}
type RefreshInput struct {
RefreshToken string `json:"refresh_token" validate:"required" label:"field.refresh_token" errmsg:"required:validate.login.refresh_required"`
}
// SessionKicker drops the IM connection for one device after HTTP logout.
type SessionKicker interface {
KickDevice(userID int64, deviceID uint64)
}
// LoginService is the application login use-case. Handlers only bind and call this.
type LoginService struct {
Issuer Issuer
Users Verifier
Sessions SessionKicker
}
func NewLoginService(issuer Issuer, users Verifier) *LoginService {
return &LoginService{Issuer: issuer, Users: users}
}
func (s *LoginService) Login(ctx context.Context, in *LoginInput) (*authentity.IssuedToken, error) {
if s == nil || s.Issuer == nil || s.Users == nil || in == nil {
return nil, errs.ErrUnauthorized
}
if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil {
return nil, err
}
if !userentity.ValidDeviceType(loginDeviceType(in)) {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyLoginDeviceTypeRequired, nil))
}
u, err := s.Users.AuthenticateEmail(ctx, in.Email, in.VerifyCode, loginDevice(ctx, in))
if err != nil {
return nil, err
}
if u == nil || u.ID <= 0 {
return nil, errs.ErrUnauthorized
}
tok, err := s.Issuer.Issue(ctx, u.ID, in.DeviceID)
if err != nil {
return nil, err
}
tok.SparkID = u.SparkID
return tok, nil
}
func (s *LoginService) Refresh(ctx context.Context, in RefreshInput) (*authentity.IssuedToken, error) {
if s == nil || s.Issuer == nil {
return nil, errs.ErrUnauthorized
}
if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil {
return nil, err
}
return s.Issuer.Refresh(ctx, in.RefreshToken)
}
func (s *LoginService) Logout(ctx context.Context) error {
if s == nil || s.Issuer == nil {
return errs.ErrUnauthorized
}
rc := reqctx.From(ctx)
if rc == nil || rc.UserID <= 0 || rc.DeviceID == 0 {
return errs.ErrUnauthorized
}
if err := s.Issuer.Logout(ctx, rc.UserID, rc.DeviceID, rc.JTI, rc.TokenExpires); err != nil {
return err
}
if s.Sessions != nil {
s.Sessions.KickDevice(rc.UserID, rc.DeviceID)
}
return nil
}
func loginDeviceType(in *LoginInput) string {
if in == nil {
return ""
}
if t := userentity.NormalizeDeviceType(in.DeviceType); userentity.ValidDeviceType(t) {
return t
}
return userentity.NormalizeDeviceType(in.Platform)
}
func loginDevice(ctx context.Context, in *LoginInput) *userentity.LoginDevice {
if in == nil {
return &userentity.LoginDevice{}
}
d := &userentity.LoginDevice{
DeviceID: in.DeviceID,
DeviceType: loginDeviceType(in),
Platform: in.Platform,
DeviceName: in.DeviceName,
RealIP: reqctx.RealIP(ctx),
Language: reqctx.Language(ctx),
}
if rc := reqctx.From(ctx); rc != nil {
d.AppVersion = rc.AppVersion
}
return d
}