为 Spark Messenger 主路径补齐 POST /v1/friends/requests/reject,并落地法律协议/反馈/订阅/密码登录及管理页,同步 OpenAPI 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
8.0 KiB
Go
205 lines
8.0 KiB
Go
package logic
|
|
|
|
import (
|
|
"context"
|
|
|
|
authentity "spark-server/internal/module/auth/entity"
|
|
deviceentity "spark-server/internal/module/device/entity"
|
|
devicelogic "spark-server/internal/module/device/logic"
|
|
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)
|
|
}
|
|
|
|
// PasswordVerifier checks email+password (Health / Sleephealth). Optional.
|
|
type PasswordVerifier interface {
|
|
VerifyPassword(ctx context.Context, email, password string) (*userentity.User, error)
|
|
}
|
|
|
|
// DeviceRegistrar resolves physical device_id from idfv / android_id.
|
|
type DeviceRegistrar interface {
|
|
Register(ctx context.Context, in devicelogic.RegisterInput) (uint64, error)
|
|
}
|
|
|
|
// LoginInput is the HTTP login body (OTP).
|
|
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"`
|
|
AppID uint8 `json:"app_id" validate:"required,min=1" label:"field.app_id" errmsg:"required:validate.login.app_id_required"`
|
|
DeviceType uint8 `json:"device_type" validate:"omitempty,min=1,max=6" label:"field.device_type" errmsg:"min:validate.login.device_type_invalid;max:validate.login.device_type_invalid"`
|
|
Platform uint8 `json:"platform" validate:"omitempty,min=1,max=6" label:"field.device_type" errmsg:"min:validate.login.device_type_invalid;max:validate.login.device_type_invalid"`
|
|
DeviceName string `json:"device_name" validate:"omitempty,max=128" label:"field.device_name"`
|
|
IDFV string `json:"idfv" validate:"omitempty,max=36" label:"field.idfv"`
|
|
AndroidID string `json:"android_id" validate:"omitempty,max=20" label:"field.android_id"`
|
|
}
|
|
|
|
// PasswordLoginInput is POST /v1/auth/password-login body.
|
|
type PasswordLoginInput struct {
|
|
Email string `json:"email" validate:"required,email" label:"field.email" errmsg:"required:validate.login.email_required;email:validate.login.email_invalid"`
|
|
Password string `json:"password" validate:"required,min=6,max=72" label:"field.password" errmsg:"required:validate.login.password_required;min:validate.login.password_min"`
|
|
AppID uint8 `json:"app_id" validate:"required,min=1" label:"field.app_id" errmsg:"required:validate.login.app_id_required"`
|
|
DeviceType uint8 `json:"device_type" validate:"omitempty,min=1,max=6" label:"field.device_type" errmsg:"min:validate.login.device_type_invalid;max:validate.login.device_type_invalid"`
|
|
Platform uint8 `json:"platform" validate:"omitempty,min=1,max=6" label:"field.device_type" errmsg:"min:validate.login.device_type_invalid;max:validate.login.device_type_invalid"`
|
|
DeviceName string `json:"device_name" validate:"omitempty,max=128" label:"field.device_name"`
|
|
IDFV string `json:"idfv" validate:"omitempty,max=36" label:"field.idfv"`
|
|
AndroidID string `json:"android_id" validate:"omitempty,max=20" label:"field.android_id"`
|
|
}
|
|
|
|
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.
|
|
type LoginService struct {
|
|
Issuer Issuer
|
|
Users Verifier
|
|
Passwords PasswordVerifier
|
|
Devices DeviceRegistrar
|
|
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 || s.Devices == nil || in == nil {
|
|
return nil, errs.ErrUnauthorized
|
|
}
|
|
if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil {
|
|
return nil, err
|
|
}
|
|
if !deviceentity.AppID(in.AppID).Valid() {
|
|
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyLoginAppIDInvalid, nil))
|
|
}
|
|
dtype := loginResolvedDeviceType(in.DeviceType, in.Platform)
|
|
if !dtype.Valid() {
|
|
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyLoginDeviceTypeRequired, nil))
|
|
}
|
|
deviceID, err := s.Devices.Register(ctx, devicelogic.RegisterInput{
|
|
DeviceType: dtype,
|
|
DeviceName: in.DeviceName,
|
|
IDFV: in.IDFV,
|
|
AndroidID: in.AndroidID,
|
|
ClientIP: reqctx.RealIP(ctx),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ld := loginDeviceFrom(ctx, in.AppID, in.DeviceName, in.IDFV, in.AndroidID, dtype, deviceID)
|
|
u, err := s.Users.AuthenticateEmail(ctx, in.Email, in.VerifyCode, ld)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.issueForUser(ctx, u, deviceID, uint32(in.AppID))
|
|
}
|
|
|
|
func (s *LoginService) PasswordLogin(ctx context.Context, in *PasswordLoginInput) (*authentity.IssuedToken, error) {
|
|
if s == nil || s.Issuer == nil || s.Passwords == nil || s.Devices == nil || in == nil {
|
|
return nil, errs.ErrUnauthorized
|
|
}
|
|
if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil {
|
|
return nil, err
|
|
}
|
|
if !deviceentity.AppID(in.AppID).Valid() {
|
|
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyLoginAppIDInvalid, nil))
|
|
}
|
|
dtype := loginResolvedDeviceType(in.DeviceType, in.Platform)
|
|
if !dtype.Valid() {
|
|
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyLoginDeviceTypeRequired, nil))
|
|
}
|
|
u, err := s.Passwords.VerifyPassword(ctx, in.Email, in.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
deviceID, err := s.Devices.Register(ctx, devicelogic.RegisterInput{
|
|
DeviceType: dtype,
|
|
DeviceName: in.DeviceName,
|
|
IDFV: in.IDFV,
|
|
AndroidID: in.AndroidID,
|
|
ClientIP: reqctx.RealIP(ctx),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.issueForUser(ctx, u, deviceID, uint32(in.AppID))
|
|
}
|
|
|
|
func (s *LoginService) issueForUser(ctx context.Context, u *userentity.User, deviceID uint64, appID uint32) (*authentity.IssuedToken, error) {
|
|
if u == nil || u.ID <= 0 {
|
|
return nil, errs.ErrUnauthorized
|
|
}
|
|
tok, err := s.Issuer.Issue(ctx, u.ID, deviceID, appID)
|
|
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 || rc.AppID == 0 {
|
|
return errs.ErrUnauthorized
|
|
}
|
|
if err := s.Issuer.Logout(ctx, rc.UserID, rc.DeviceID, rc.AppID, rc.JTI, rc.TokenExpires); err != nil {
|
|
return err
|
|
}
|
|
if s.Sessions != nil {
|
|
s.Sessions.KickDevice(rc.UserID, rc.DeviceID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loginResolvedDeviceType(deviceType, platform uint8) deviceentity.DeviceType {
|
|
if deviceType >= 1 {
|
|
return deviceentity.DeviceTypeFromWire(deviceType)
|
|
}
|
|
if platform >= 1 {
|
|
return deviceentity.DeviceTypeFromWire(platform)
|
|
}
|
|
return deviceentity.TypeUnknown
|
|
}
|
|
|
|
func loginDeviceFrom(ctx context.Context, appID uint8, deviceName, idfv, androidID string, dtype deviceentity.DeviceType, deviceID uint64) *userentity.LoginDevice {
|
|
d := &userentity.LoginDevice{
|
|
DeviceID: deviceID,
|
|
AppID: appID,
|
|
DeviceType: dtype.PlatformName(),
|
|
DeviceName: deviceName,
|
|
IDFV: idfv,
|
|
AndroidID: androidID,
|
|
RealIP: reqctx.RealIP(ctx),
|
|
Language: reqctx.Language(ctx),
|
|
}
|
|
if rc := reqctx.From(ctx); rc != nil {
|
|
d.AppVersion = rc.AppVersion
|
|
}
|
|
return d
|
|
}
|