回滚注册
This commit is contained in:
parent
d145f3608d
commit
a10a851075
@ -1542,100 +1542,6 @@ func (x *AppHeartbeatResponse) GetToken() *AuthToken {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompletePendingThirdPartyRegistrationRequest 使用 pending token 完成三方注册。
|
||||
type CompletePendingThirdPartyRegistrationRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
PendingRegistrationId string `protobuf:"bytes,2,opt,name=pending_registration_id,json=pendingRegistrationId,proto3" json:"pending_registration_id,omitempty"`
|
||||
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Avatar string `protobuf:"bytes,4,opt,name=avatar,proto3" json:"avatar,omitempty"`
|
||||
Gender string `protobuf:"bytes,5,opt,name=gender,proto3" json:"gender,omitempty"`
|
||||
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
|
||||
InviteCode string `protobuf:"bytes,7,opt,name=invite_code,json=inviteCode,proto3" json:"invite_code,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) Reset() {
|
||||
*x = CompletePendingThirdPartyRegistrationRequest{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CompletePendingThirdPartyRegistrationRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[22]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CompletePendingThirdPartyRegistrationRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CompletePendingThirdPartyRegistrationRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) GetMeta() *RequestMeta {
|
||||
if x != nil {
|
||||
return x.Meta
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) GetPendingRegistrationId() string {
|
||||
if x != nil {
|
||||
return x.PendingRegistrationId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) GetAvatar() string {
|
||||
if x != nil {
|
||||
return x.Avatar
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) GetGender() string {
|
||||
if x != nil {
|
||||
return x.Gender
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) GetCountry() string {
|
||||
if x != nil {
|
||||
return x.Country
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CompletePendingThirdPartyRegistrationRequest) GetInviteCode() string {
|
||||
if x != nil {
|
||||
return x.InviteCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proto_user_v1_auth_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_proto_user_v1_auth_proto_rawDesc = []byte{
|
||||
@ -1868,25 +1774,7 @@ var file_proto_user_v1_auth_proto_rawDesc = []byte{
|
||||
0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65,
|
||||
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x9d, 0x02, 0x0a, 0x2c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
|
||||
0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61,
|
||||
0x72, 0x74, 0x79, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65,
|
||||
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61,
|
||||
0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x36, 0x0a, 0x17, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e,
|
||||
0x67, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67,
|
||||
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1a,
|
||||
0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x76,
|
||||
0x61, 0x74, 0x61, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74,
|
||||
0x61, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63,
|
||||
0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69, 0x74,
|
||||
0x65, 0x43, 0x6f, 0x64, 0x65, 0x32, 0xdf, 0x09, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xcd, 0x08, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65,
|
||||
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61,
|
||||
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75,
|
||||
0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73,
|
||||
@ -1955,19 +1843,10 @@ var file_proto_user_v1_auth_proto_rawDesc = []byte{
|
||||
0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8f, 0x01, 0x0a, 0x25, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
|
||||
0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61,
|
||||
0x72, 0x74, 0x79, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12,
|
||||
0x3b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54,
|
||||
0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68,
|
||||
0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d,
|
||||
0x70, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70,
|
||||
0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
|
||||
0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75,
|
||||
0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@ -1982,83 +1861,78 @@ func file_proto_user_v1_auth_proto_rawDescGZIP() []byte {
|
||||
return file_proto_user_v1_auth_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
|
||||
var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
|
||||
var file_proto_user_v1_auth_proto_goTypes = []any{
|
||||
(*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest
|
||||
(*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest
|
||||
(*AuthResponse)(nil), // 2: hyapp.user.v1.AuthResponse
|
||||
(*SetPasswordRequest)(nil), // 3: hyapp.user.v1.SetPasswordRequest
|
||||
(*SetPasswordResponse)(nil), // 4: hyapp.user.v1.SetPasswordResponse
|
||||
(*QuickCreateAccountRequest)(nil), // 5: hyapp.user.v1.QuickCreateAccountRequest
|
||||
(*QuickCreateAccountResponse)(nil), // 6: hyapp.user.v1.QuickCreateAccountResponse
|
||||
(*RefreshTokenRequest)(nil), // 7: hyapp.user.v1.RefreshTokenRequest
|
||||
(*RefreshTokenResponse)(nil), // 8: hyapp.user.v1.RefreshTokenResponse
|
||||
(*LogoutRequest)(nil), // 9: hyapp.user.v1.LogoutRequest
|
||||
(*LogoutResponse)(nil), // 10: hyapp.user.v1.LogoutResponse
|
||||
(*RecordLoginBlockedRequest)(nil), // 11: hyapp.user.v1.RecordLoginBlockedRequest
|
||||
(*RecordLoginBlockedResponse)(nil), // 12: hyapp.user.v1.RecordLoginBlockedResponse
|
||||
(*CheckLoginRiskIPWhitelistRequest)(nil), // 13: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest
|
||||
(*CheckLoginRiskIPWhitelistResponse)(nil), // 14: hyapp.user.v1.CheckLoginRiskIPWhitelistResponse
|
||||
(*RegisterRiskConfig)(nil), // 15: hyapp.user.v1.RegisterRiskConfig
|
||||
(*GetRegisterRiskConfigRequest)(nil), // 16: hyapp.user.v1.GetRegisterRiskConfigRequest
|
||||
(*GetRegisterRiskConfigResponse)(nil), // 17: hyapp.user.v1.GetRegisterRiskConfigResponse
|
||||
(*UpdateRegisterRiskConfigRequest)(nil), // 18: hyapp.user.v1.UpdateRegisterRiskConfigRequest
|
||||
(*UpdateRegisterRiskConfigResponse)(nil), // 19: hyapp.user.v1.UpdateRegisterRiskConfigResponse
|
||||
(*AppHeartbeatRequest)(nil), // 20: hyapp.user.v1.AppHeartbeatRequest
|
||||
(*AppHeartbeatResponse)(nil), // 21: hyapp.user.v1.AppHeartbeatResponse
|
||||
(*CompletePendingThirdPartyRegistrationRequest)(nil), // 22: hyapp.user.v1.CompletePendingThirdPartyRegistrationRequest
|
||||
(*RequestMeta)(nil), // 23: hyapp.user.v1.RequestMeta
|
||||
(*AuthToken)(nil), // 24: hyapp.user.v1.AuthToken
|
||||
(*CompleteOnboardingResponse)(nil), // 25: hyapp.user.v1.CompleteOnboardingResponse
|
||||
(*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest
|
||||
(*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest
|
||||
(*AuthResponse)(nil), // 2: hyapp.user.v1.AuthResponse
|
||||
(*SetPasswordRequest)(nil), // 3: hyapp.user.v1.SetPasswordRequest
|
||||
(*SetPasswordResponse)(nil), // 4: hyapp.user.v1.SetPasswordResponse
|
||||
(*QuickCreateAccountRequest)(nil), // 5: hyapp.user.v1.QuickCreateAccountRequest
|
||||
(*QuickCreateAccountResponse)(nil), // 6: hyapp.user.v1.QuickCreateAccountResponse
|
||||
(*RefreshTokenRequest)(nil), // 7: hyapp.user.v1.RefreshTokenRequest
|
||||
(*RefreshTokenResponse)(nil), // 8: hyapp.user.v1.RefreshTokenResponse
|
||||
(*LogoutRequest)(nil), // 9: hyapp.user.v1.LogoutRequest
|
||||
(*LogoutResponse)(nil), // 10: hyapp.user.v1.LogoutResponse
|
||||
(*RecordLoginBlockedRequest)(nil), // 11: hyapp.user.v1.RecordLoginBlockedRequest
|
||||
(*RecordLoginBlockedResponse)(nil), // 12: hyapp.user.v1.RecordLoginBlockedResponse
|
||||
(*CheckLoginRiskIPWhitelistRequest)(nil), // 13: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest
|
||||
(*CheckLoginRiskIPWhitelistResponse)(nil), // 14: hyapp.user.v1.CheckLoginRiskIPWhitelistResponse
|
||||
(*RegisterRiskConfig)(nil), // 15: hyapp.user.v1.RegisterRiskConfig
|
||||
(*GetRegisterRiskConfigRequest)(nil), // 16: hyapp.user.v1.GetRegisterRiskConfigRequest
|
||||
(*GetRegisterRiskConfigResponse)(nil), // 17: hyapp.user.v1.GetRegisterRiskConfigResponse
|
||||
(*UpdateRegisterRiskConfigRequest)(nil), // 18: hyapp.user.v1.UpdateRegisterRiskConfigRequest
|
||||
(*UpdateRegisterRiskConfigResponse)(nil), // 19: hyapp.user.v1.UpdateRegisterRiskConfigResponse
|
||||
(*AppHeartbeatRequest)(nil), // 20: hyapp.user.v1.AppHeartbeatRequest
|
||||
(*AppHeartbeatResponse)(nil), // 21: hyapp.user.v1.AppHeartbeatResponse
|
||||
(*RequestMeta)(nil), // 22: hyapp.user.v1.RequestMeta
|
||||
(*AuthToken)(nil), // 23: hyapp.user.v1.AuthToken
|
||||
}
|
||||
var file_proto_user_v1_auth_proto_depIdxs = []int32{
|
||||
23, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
24, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
23, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
24, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
23, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
24, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
23, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 10: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 11: hyapp.user.v1.GetRegisterRiskConfigRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
22, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
22, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
22, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
22, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
22, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
22, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
22, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
22, // 10: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
22, // 11: hyapp.user.v1.GetRegisterRiskConfigRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
15, // 12: hyapp.user.v1.GetRegisterRiskConfigResponse.config:type_name -> hyapp.user.v1.RegisterRiskConfig
|
||||
23, // 13: hyapp.user.v1.UpdateRegisterRiskConfigRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
22, // 13: hyapp.user.v1.UpdateRegisterRiskConfigRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
15, // 14: hyapp.user.v1.UpdateRegisterRiskConfigResponse.config:type_name -> hyapp.user.v1.RegisterRiskConfig
|
||||
23, // 15: hyapp.user.v1.AppHeartbeatRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
24, // 16: hyapp.user.v1.AppHeartbeatResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
23, // 17: hyapp.user.v1.CompletePendingThirdPartyRegistrationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
0, // 18: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest
|
||||
1, // 19: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest
|
||||
3, // 20: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest
|
||||
5, // 21: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest
|
||||
7, // 22: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest
|
||||
9, // 23: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest
|
||||
11, // 24: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest
|
||||
13, // 25: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:input_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistRequest
|
||||
16, // 26: hyapp.user.v1.AuthService.GetRegisterRiskConfig:input_type -> hyapp.user.v1.GetRegisterRiskConfigRequest
|
||||
18, // 27: hyapp.user.v1.AuthService.UpdateRegisterRiskConfig:input_type -> hyapp.user.v1.UpdateRegisterRiskConfigRequest
|
||||
20, // 28: hyapp.user.v1.AuthService.AppHeartbeat:input_type -> hyapp.user.v1.AppHeartbeatRequest
|
||||
22, // 29: hyapp.user.v1.AuthService.CompletePendingThirdPartyRegistration:input_type -> hyapp.user.v1.CompletePendingThirdPartyRegistrationRequest
|
||||
2, // 30: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse
|
||||
2, // 31: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse
|
||||
4, // 32: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse
|
||||
6, // 33: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse
|
||||
8, // 34: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse
|
||||
10, // 35: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse
|
||||
12, // 36: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse
|
||||
14, // 37: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:output_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistResponse
|
||||
17, // 38: hyapp.user.v1.AuthService.GetRegisterRiskConfig:output_type -> hyapp.user.v1.GetRegisterRiskConfigResponse
|
||||
19, // 39: hyapp.user.v1.AuthService.UpdateRegisterRiskConfig:output_type -> hyapp.user.v1.UpdateRegisterRiskConfigResponse
|
||||
21, // 40: hyapp.user.v1.AuthService.AppHeartbeat:output_type -> hyapp.user.v1.AppHeartbeatResponse
|
||||
25, // 41: hyapp.user.v1.AuthService.CompletePendingThirdPartyRegistration:output_type -> hyapp.user.v1.CompleteOnboardingResponse
|
||||
30, // [30:42] is the sub-list for method output_type
|
||||
18, // [18:30] is the sub-list for method input_type
|
||||
18, // [18:18] is the sub-list for extension type_name
|
||||
18, // [18:18] is the sub-list for extension extendee
|
||||
0, // [0:18] is the sub-list for field type_name
|
||||
22, // 15: hyapp.user.v1.AppHeartbeatRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
23, // 16: hyapp.user.v1.AppHeartbeatResponse.token:type_name -> hyapp.user.v1.AuthToken
|
||||
0, // 17: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest
|
||||
1, // 18: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest
|
||||
3, // 19: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest
|
||||
5, // 20: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest
|
||||
7, // 21: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest
|
||||
9, // 22: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest
|
||||
11, // 23: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest
|
||||
13, // 24: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:input_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistRequest
|
||||
16, // 25: hyapp.user.v1.AuthService.GetRegisterRiskConfig:input_type -> hyapp.user.v1.GetRegisterRiskConfigRequest
|
||||
18, // 26: hyapp.user.v1.AuthService.UpdateRegisterRiskConfig:input_type -> hyapp.user.v1.UpdateRegisterRiskConfigRequest
|
||||
20, // 27: hyapp.user.v1.AuthService.AppHeartbeat:input_type -> hyapp.user.v1.AppHeartbeatRequest
|
||||
2, // 28: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse
|
||||
2, // 29: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse
|
||||
4, // 30: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse
|
||||
6, // 31: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse
|
||||
8, // 32: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse
|
||||
10, // 33: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse
|
||||
12, // 34: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse
|
||||
14, // 35: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:output_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistResponse
|
||||
17, // 36: hyapp.user.v1.AuthService.GetRegisterRiskConfig:output_type -> hyapp.user.v1.GetRegisterRiskConfigResponse
|
||||
19, // 37: hyapp.user.v1.AuthService.UpdateRegisterRiskConfig:output_type -> hyapp.user.v1.UpdateRegisterRiskConfigResponse
|
||||
21, // 38: hyapp.user.v1.AuthService.AppHeartbeat:output_type -> hyapp.user.v1.AppHeartbeatResponse
|
||||
28, // [28:39] is the sub-list for method output_type
|
||||
17, // [17:28] is the sub-list for method input_type
|
||||
17, // [17:17] is the sub-list for extension type_name
|
||||
17, // [17:17] is the sub-list for extension extendee
|
||||
0, // [0:17] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proto_user_v1_auth_proto_init() }
|
||||
@ -2073,7 +1947,7 @@ func file_proto_user_v1_auth_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_proto_user_v1_auth_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 23,
|
||||
NumMessages: 22,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@ -178,17 +178,6 @@ message AppHeartbeatResponse {
|
||||
AuthToken token = 4;
|
||||
}
|
||||
|
||||
// CompletePendingThirdPartyRegistrationRequest 使用 pending token 完成三方注册。
|
||||
message CompletePendingThirdPartyRegistrationRequest {
|
||||
RequestMeta meta = 1;
|
||||
string pending_registration_id = 2;
|
||||
string username = 3;
|
||||
string avatar = 4;
|
||||
string gender = 5;
|
||||
string country = 6;
|
||||
string invite_code = 7;
|
||||
}
|
||||
|
||||
// AuthService 承载登录注册和 token 生命周期能力。
|
||||
service AuthService {
|
||||
rpc LoginPassword(LoginPasswordRequest) returns (AuthResponse);
|
||||
@ -202,5 +191,4 @@ service AuthService {
|
||||
rpc GetRegisterRiskConfig(GetRegisterRiskConfigRequest) returns (GetRegisterRiskConfigResponse);
|
||||
rpc UpdateRegisterRiskConfig(UpdateRegisterRiskConfigRequest) returns (UpdateRegisterRiskConfigResponse);
|
||||
rpc AppHeartbeat(AppHeartbeatRequest) returns (AppHeartbeatResponse);
|
||||
rpc CompletePendingThirdPartyRegistration(CompletePendingThirdPartyRegistrationRequest) returns (CompleteOnboardingResponse);
|
||||
}
|
||||
|
||||
@ -19,18 +19,17 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword"
|
||||
AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty"
|
||||
AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword"
|
||||
AuthService_QuickCreateAccount_FullMethodName = "/hyapp.user.v1.AuthService/QuickCreateAccount"
|
||||
AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken"
|
||||
AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout"
|
||||
AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked"
|
||||
AuthService_CheckLoginRiskIPWhitelist_FullMethodName = "/hyapp.user.v1.AuthService/CheckLoginRiskIPWhitelist"
|
||||
AuthService_GetRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/GetRegisterRiskConfig"
|
||||
AuthService_UpdateRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/UpdateRegisterRiskConfig"
|
||||
AuthService_AppHeartbeat_FullMethodName = "/hyapp.user.v1.AuthService/AppHeartbeat"
|
||||
AuthService_CompletePendingThirdPartyRegistration_FullMethodName = "/hyapp.user.v1.AuthService/CompletePendingThirdPartyRegistration"
|
||||
AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword"
|
||||
AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty"
|
||||
AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword"
|
||||
AuthService_QuickCreateAccount_FullMethodName = "/hyapp.user.v1.AuthService/QuickCreateAccount"
|
||||
AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken"
|
||||
AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout"
|
||||
AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked"
|
||||
AuthService_CheckLoginRiskIPWhitelist_FullMethodName = "/hyapp.user.v1.AuthService/CheckLoginRiskIPWhitelist"
|
||||
AuthService_GetRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/GetRegisterRiskConfig"
|
||||
AuthService_UpdateRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/UpdateRegisterRiskConfig"
|
||||
AuthService_AppHeartbeat_FullMethodName = "/hyapp.user.v1.AuthService/AppHeartbeat"
|
||||
)
|
||||
|
||||
// AuthServiceClient is the client API for AuthService service.
|
||||
@ -50,7 +49,6 @@ type AuthServiceClient interface {
|
||||
GetRegisterRiskConfig(ctx context.Context, in *GetRegisterRiskConfigRequest, opts ...grpc.CallOption) (*GetRegisterRiskConfigResponse, error)
|
||||
UpdateRegisterRiskConfig(ctx context.Context, in *UpdateRegisterRiskConfigRequest, opts ...grpc.CallOption) (*UpdateRegisterRiskConfigResponse, error)
|
||||
AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error)
|
||||
CompletePendingThirdPartyRegistration(ctx context.Context, in *CompletePendingThirdPartyRegistrationRequest, opts ...grpc.CallOption) (*CompleteOnboardingResponse, error)
|
||||
}
|
||||
|
||||
type authServiceClient struct {
|
||||
@ -171,16 +169,6 @@ func (c *authServiceClient) AppHeartbeat(ctx context.Context, in *AppHeartbeatRe
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) CompletePendingThirdPartyRegistration(ctx context.Context, in *CompletePendingThirdPartyRegistrationRequest, opts ...grpc.CallOption) (*CompleteOnboardingResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CompleteOnboardingResponse)
|
||||
err := c.cc.Invoke(ctx, AuthService_CompletePendingThirdPartyRegistration_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AuthServiceServer is the server API for AuthService service.
|
||||
// All implementations must embed UnimplementedAuthServiceServer
|
||||
// for forward compatibility.
|
||||
@ -198,7 +186,6 @@ type AuthServiceServer interface {
|
||||
GetRegisterRiskConfig(context.Context, *GetRegisterRiskConfigRequest) (*GetRegisterRiskConfigResponse, error)
|
||||
UpdateRegisterRiskConfig(context.Context, *UpdateRegisterRiskConfigRequest) (*UpdateRegisterRiskConfigResponse, error)
|
||||
AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error)
|
||||
CompletePendingThirdPartyRegistration(context.Context, *CompletePendingThirdPartyRegistrationRequest) (*CompleteOnboardingResponse, error)
|
||||
mustEmbedUnimplementedAuthServiceServer()
|
||||
}
|
||||
|
||||
@ -242,9 +229,6 @@ func (UnimplementedAuthServiceServer) UpdateRegisterRiskConfig(context.Context,
|
||||
func (UnimplementedAuthServiceServer) AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AppHeartbeat not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) CompletePendingThirdPartyRegistration(context.Context, *CompletePendingThirdPartyRegistrationRequest) (*CompleteOnboardingResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CompletePendingThirdPartyRegistration not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
|
||||
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@ -464,24 +448,6 @@ func _AuthService_AppHeartbeat_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_CompletePendingThirdPartyRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CompletePendingThirdPartyRegistrationRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).CompletePendingThirdPartyRegistration(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AuthService_CompletePendingThirdPartyRegistration_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).CompletePendingThirdPartyRegistration(ctx, req.(*CompletePendingThirdPartyRegistrationRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -533,10 +499,6 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "AppHeartbeat",
|
||||
Handler: _AuthService_AppHeartbeat_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CompletePendingThirdPartyRegistration",
|
||||
Handler: _AuthService_CompletePendingThirdPartyRegistration_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/user/v1/auth.proto",
|
||||
|
||||
@ -15,10 +15,6 @@ type contextKey string
|
||||
const (
|
||||
userContextKey contextKey = "gateway_user_id"
|
||||
claimsContextKey contextKey = "gateway_claims"
|
||||
// TokenTypeAccess 是真实用户 access token。
|
||||
TokenTypeAccess = "access"
|
||||
// TokenTypePendingRegistration 是只允许完成注册的 pending token。
|
||||
TokenTypePendingRegistration = "pending_registration"
|
||||
)
|
||||
|
||||
// Claims 是 gateway 从 access token 中消费的最小用户状态快照。
|
||||
@ -27,8 +23,6 @@ type Claims struct {
|
||||
AppCode string
|
||||
UserID int64
|
||||
SessionID string
|
||||
TokenType string
|
||||
PendingRegistrationID string
|
||||
ProfileCompleted bool
|
||||
OnboardingStatus string
|
||||
AccessTokenExpiresAtMS int64
|
||||
@ -78,40 +72,6 @@ func (v *Verifier) Verify(header string) (Claims, error) {
|
||||
return Claims{}, fmt.Errorf("invalid claims")
|
||||
}
|
||||
|
||||
tokenType, _ := claims["typ"].(string)
|
||||
tokenType = strings.TrimSpace(tokenType)
|
||||
if tokenType == "" {
|
||||
// 旧测试 token 没有 typ 时按真实用户 access token 处理。
|
||||
tokenType = TokenTypeAccess
|
||||
}
|
||||
profileCompleted, _ := claims["profile_completed"].(bool)
|
||||
onboardingStatus, _ := claims["onboarding_status"].(string)
|
||||
sessionID, _ := claims["sid"].(string)
|
||||
appCode, _ := claims["app_code"].(string)
|
||||
expiresAtMS := claimUnixSecondsMS(claims["exp"])
|
||||
if tokenType == TokenTypePendingRegistration {
|
||||
pendingID, _ := claims["pending_registration_id"].(string)
|
||||
pendingID = strings.TrimSpace(pendingID)
|
||||
if pendingID == "" {
|
||||
return Claims{}, fmt.Errorf("missing pending_registration_id")
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
sessionID = pendingID
|
||||
}
|
||||
return Claims{
|
||||
AppCode: appcode.Normalize(appCode),
|
||||
SessionID: strings.TrimSpace(sessionID),
|
||||
TokenType: tokenType,
|
||||
PendingRegistrationID: pendingID,
|
||||
ProfileCompleted: false,
|
||||
OnboardingStatus: strings.TrimSpace(onboardingStatus),
|
||||
AccessTokenExpiresAtMS: expiresAtMS,
|
||||
}, nil
|
||||
}
|
||||
if tokenType != TokenTypeAccess {
|
||||
return Claims{}, fmt.Errorf("invalid token type")
|
||||
}
|
||||
|
||||
rawUserID, ok := claims["user_id"].(string)
|
||||
if !ok || strings.TrimSpace(rawUserID) == "" {
|
||||
return Claims{}, fmt.Errorf("missing user_id")
|
||||
@ -121,11 +81,16 @@ func (v *Verifier) Verify(header string) (Claims, error) {
|
||||
return Claims{}, fmt.Errorf("invalid user_id")
|
||||
}
|
||||
|
||||
profileCompleted, _ := claims["profile_completed"].(bool)
|
||||
onboardingStatus, _ := claims["onboarding_status"].(string)
|
||||
sessionID, _ := claims["sid"].(string)
|
||||
appCode, _ := claims["app_code"].(string)
|
||||
expiresAtMS := claimUnixSecondsMS(claims["exp"])
|
||||
|
||||
return Claims{
|
||||
AppCode: appcode.Normalize(appCode),
|
||||
UserID: userID,
|
||||
SessionID: strings.TrimSpace(sessionID),
|
||||
TokenType: tokenType,
|
||||
ProfileCompleted: profileCompleted,
|
||||
OnboardingStatus: strings.TrimSpace(onboardingStatus),
|
||||
AccessTokenExpiresAtMS: expiresAtMS,
|
||||
@ -185,18 +150,6 @@ func SessionIDFromContext(ctx context.Context) string {
|
||||
return strings.TrimSpace(claims.SessionID)
|
||||
}
|
||||
|
||||
// IsPendingRegistrationFromContext 表示当前请求使用的是待完成注册 token。
|
||||
func IsPendingRegistrationFromContext(ctx context.Context) bool {
|
||||
claims, _ := ctx.Value(claimsContextKey).(Claims)
|
||||
return claims.TokenType == TokenTypePendingRegistration
|
||||
}
|
||||
|
||||
// PendingRegistrationIDFromContext 提取 pending token 对应的注册态 ID。
|
||||
func PendingRegistrationIDFromContext(ctx context.Context) string {
|
||||
claims, _ := ctx.Value(claimsContextKey).(Claims)
|
||||
return strings.TrimSpace(claims.PendingRegistrationID)
|
||||
}
|
||||
|
||||
// ProfileCompletedFromContext 返回 gateway profile gate 的判断依据。
|
||||
func ProfileCompletedFromContext(ctx context.Context) bool {
|
||||
claims, _ := ctx.Value(claimsContextKey).(Claims)
|
||||
|
||||
@ -19,7 +19,6 @@ type UserAuthClient interface {
|
||||
RecordLoginBlocked(ctx context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error)
|
||||
CheckLoginRiskIPWhitelist(ctx context.Context, req *userv1.CheckLoginRiskIPWhitelistRequest) (*userv1.CheckLoginRiskIPWhitelistResponse, error)
|
||||
AppHeartbeat(ctx context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error)
|
||||
CompletePendingThirdPartyRegistration(ctx context.Context, req *userv1.CompletePendingThirdPartyRegistrationRequest) (*userv1.CompleteOnboardingResponse, error)
|
||||
}
|
||||
|
||||
// UserIdentityClient 抽象 gateway 对 user-service 短号能力的依赖。
|
||||
@ -284,10 +283,6 @@ func (c *grpcUserAuthClient) AppHeartbeat(ctx context.Context, req *userv1.AppHe
|
||||
return c.client.AppHeartbeat(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserAuthClient) CompletePendingThirdPartyRegistration(ctx context.Context, req *userv1.CompletePendingThirdPartyRegistrationRequest) (*userv1.CompleteOnboardingResponse, error) {
|
||||
return c.client.CompletePendingThirdPartyRegistration(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserIdentityClient) GetUserIdentity(ctx context.Context, req *userv1.GetUserIdentityRequest) (*userv1.GetUserIdentityResponse, error) {
|
||||
return c.client.GetUserIdentity(ctx, req)
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package appapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
"io"
|
||||
@ -107,7 +106,7 @@ func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
objectKey := buildUploadObjectKey(policy, uploadOwnerSegment(request.Context()), header.Filename, contentType, time.Now().UTC())
|
||||
objectKey := buildUploadObjectKey(policy, auth.UserIDFromContext(request.Context()), header.Filename, contentType, time.Now().UTC())
|
||||
objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, io.MultiReader(bytes.NewReader(head), file), header.Size, contentType)
|
||||
if err != nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
@ -181,15 +180,7 @@ func isAvatarContentType(contentType string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func uploadOwnerSegment(ctx context.Context) string {
|
||||
if auth.IsPendingRegistrationFromContext(ctx) {
|
||||
// pending 注册还没有真实 user_id;头像只能挂到 pending 命名空间,完成注册后 URL 会写入新用户资料。
|
||||
return "pending/" + auth.PendingRegistrationIDFromContext(ctx)
|
||||
}
|
||||
return fmt.Sprintf("%d", auth.UserIDFromContext(ctx))
|
||||
}
|
||||
|
||||
func buildUploadObjectKey(policy uploadPolicy, ownerSegment string, filename string, contentType string, now time.Time) string {
|
||||
func buildUploadObjectKey(policy uploadPolicy, userID int64, filename string, contentType string, now time.Time) string {
|
||||
date := now.UTC().Format("20060102")
|
||||
extension := normalizedUploadExtension(filename, contentType)
|
||||
if extension == "" && policy.RequireImage {
|
||||
@ -197,7 +188,7 @@ func buildUploadObjectKey(policy uploadPolicy, ownerSegment string, filename str
|
||||
}
|
||||
|
||||
idPrefix := strings.TrimSuffix(policy.Kind, "s")
|
||||
return fmt.Sprintf("app/%s/%s/%s/%s%s", policy.Kind, strings.Trim(ownerSegment, "/"), date, idgen.New(idPrefix), extension)
|
||||
return fmt.Sprintf("app/%s/%d/%s/%s%s", policy.Kind, userID, date, idgen.New(idPrefix), extension)
|
||||
}
|
||||
|
||||
func normalizedUploadExtension(filename string, contentType string) string {
|
||||
|
||||
@ -51,16 +51,6 @@ func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) ht
|
||||
}
|
||||
|
||||
ctx := auth.WithClaims(request.Context(), claims)
|
||||
if claims.TokenType == auth.TokenTypePendingRegistration {
|
||||
if !pendingRegistrationAllowedPath(request.URL.Path) {
|
||||
// pending token 是“已验证三方身份但还不是用户”的注册态凭证;拒绝业务接口时不能返回 401/403,
|
||||
// 否则旧 Flutter 会把它当认证失败触发 refresh,pending session 又没有 refresh token,最终清登录态。
|
||||
httpkit.WriteError(writer, request.WithContext(ctx), http.StatusPreconditionRequired, httpkit.CodeProfileRequired, "profile required")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(writer, request.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
revoked, err := h.revokedSession(ctx, claims.AppCode, claims.SessionID)
|
||||
if err != nil {
|
||||
logx.Warn(ctx, "session_denylist_read_failed", slog.String("component", "gateway_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error()))
|
||||
@ -73,15 +63,6 @@ func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) ht
|
||||
})
|
||||
}
|
||||
|
||||
func pendingRegistrationAllowedPath(path string) bool {
|
||||
switch strings.TrimSpace(path) {
|
||||
case "/api/v1/files/avatar/upload", "/api/v1/users/me/onboarding/complete":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) withOptionalAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
header := strings.TrimSpace(request.Header.Get("Authorization"))
|
||||
|
||||
@ -296,7 +296,6 @@ type fakeUserAuthClient struct {
|
||||
lastBlocked *userv1.RecordLoginBlockedRequest
|
||||
lastWhitelist *userv1.CheckLoginRiskIPWhitelistRequest
|
||||
lastAppHeartbeat *userv1.AppHeartbeatRequest
|
||||
lastPendingComplete *userv1.CompletePendingThirdPartyRegistrationRequest
|
||||
loginPasswordCount int
|
||||
loginThirdCount int
|
||||
refreshCount int
|
||||
@ -305,7 +304,6 @@ type fakeUserAuthClient struct {
|
||||
whitelisted bool
|
||||
loginErr error
|
||||
loginThirdResp *userv1.AuthResponse
|
||||
pendingCompleteErr error
|
||||
}
|
||||
|
||||
type fakeUserProfileClient struct {
|
||||
@ -745,41 +743,6 @@ func (f *fakeUserAuthClient) AppHeartbeat(_ context.Context, req *userv1.AppHear
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserAuthClient) CompletePendingThirdPartyRegistration(_ context.Context, req *userv1.CompletePendingThirdPartyRegistrationRequest) (*userv1.CompleteOnboardingResponse, error) {
|
||||
f.lastPendingComplete = req
|
||||
if f.pendingCompleteErr != nil {
|
||||
return nil, f.pendingCompleteErr
|
||||
}
|
||||
return &userv1.CompleteOnboardingResponse{
|
||||
User: &userv1.User{
|
||||
UserId: 10088,
|
||||
DisplayUserId: "168888",
|
||||
Username: req.GetUsername(),
|
||||
Avatar: req.GetAvatar(),
|
||||
Gender: req.GetGender(),
|
||||
Country: req.GetCountry(),
|
||||
RegionId: 66,
|
||||
ProfileCompleted: true,
|
||||
ProfileCompletedAtMs: 1_778_000_006_000,
|
||||
OnboardingStatus: "completed",
|
||||
},
|
||||
Token: &userv1.AuthToken{
|
||||
UserId: 10088,
|
||||
DisplayUserId: "168888",
|
||||
SessionId: "sess-completed",
|
||||
AccessToken: "access-pending-completed",
|
||||
RefreshToken: "refresh-pending-completed",
|
||||
ExpiresInSec: 1800,
|
||||
TokenType: "Bearer",
|
||||
ProfileCompleted: true,
|
||||
OnboardingStatus: "completed",
|
||||
},
|
||||
Invite: &userv1.InviteBinding{Bound: true, InviteCode: req.GetInviteCode(), InviterUserId: 10001},
|
||||
ProfileCompleted: true,
|
||||
OnboardingStatus: "completed",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserProfileClient) GetUser(_ context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
|
||||
f.lastGet = req
|
||||
f.getRequests = append(f.getRequests, req)
|
||||
@ -5497,52 +5460,6 @@ func TestThirdPartyLoginUsesPublicEnvelopeAndPropagatesRequestID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestThirdPartyLoginPendingResponseKeepsFlutterCompatibleEmptyUserIDs(t *testing.T) {
|
||||
isNewUser := true
|
||||
userClient := &fakeUserAuthClient{
|
||||
loginThirdResp: &userv1.AuthResponse{
|
||||
Token: &userv1.AuthToken{
|
||||
AppCode: "lalu",
|
||||
SessionId: "tpr-test",
|
||||
AccessToken: "pending-access",
|
||||
ExpiresInSec: 1800,
|
||||
TokenType: "Bearer",
|
||||
ProfileCompleted: false,
|
||||
OnboardingStatus: "profile_required",
|
||||
},
|
||||
IsNewUser: isNewUser,
|
||||
ProfileCompleted: false,
|
||||
OnboardingStatus: "profile_required",
|
||||
},
|
||||
}
|
||||
router := NewHandler(&fakeRoomClient{}, userClient).Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", bytes.NewReader([]byte(`{"provider":"firebase","credential":"firebase-id-token-1","device_id":"dev-1","platform":"ios"}`)))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var envelope httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data, ok := envelope.Data.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("data must be object: %#v", envelope.Data)
|
||||
}
|
||||
if data["access_token"] != "pending-access" || data["profile_completed"] != false || data["is_new_user"] != true {
|
||||
t.Fatalf("pending auth flags mismatch: %#v", data)
|
||||
}
|
||||
if _, ok := data["user_id"]; ok {
|
||||
t.Fatalf("pending response must omit user_id instead of returning 0: %#v", data)
|
||||
}
|
||||
if _, ok := data["display_user_id"]; ok {
|
||||
t.Fatalf("pending response must omit display_user_id before short id allocation: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthClientIPPrefersForwardedPublicIPOverProxyRealIP(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", nil)
|
||||
request.RemoteAddr = "10.0.0.9:54123"
|
||||
@ -8472,45 +8389,6 @@ func TestCompleteOnboardingAllowsIncompleteProfileAndUsesAuthenticatedUserID(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingRegistrationCompleteUsesAuthClientAndDoesNotRequireUserID(t *testing.T) {
|
||||
userClient := &fakeUserAuthClient{}
|
||||
profileClient := &fakeUserProfileClient{}
|
||||
router := NewHandlerWithClients(&fakeRoomClient{}, userClient, nil, profileClient).Routes(auth.NewVerifier("secret"))
|
||||
body := []byte(`{"username":"hy","avatar":"https://cdn.example/a.png","gender":"male","country":"SG","invite_code":"A1B2C3"}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/users/me/onboarding/complete", bytes.NewReader(body))
|
||||
request.Header.Set("Authorization", "Bearer "+signPendingRegistrationToken(t, "secret", "tpr-test"))
|
||||
request.Header.Set("X-Request-ID", "req-pending-onboarding")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if profileClient.lastComplete != nil {
|
||||
t.Fatalf("pending registration must not call profile CompleteOnboarding: %+v", profileClient.lastComplete)
|
||||
}
|
||||
if userClient.lastPendingComplete == nil || userClient.lastPendingComplete.GetPendingRegistrationId() != "tpr-test" {
|
||||
t.Fatalf("pending complete request was not sent: %+v", userClient.lastPendingComplete)
|
||||
}
|
||||
if userClient.lastPendingComplete.GetMeta().GetSessionId() != "tpr-test" || userClient.lastPendingComplete.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-pending-onboarding") {
|
||||
t.Fatalf("pending complete meta mismatch: %+v", userClient.lastPendingComplete.GetMeta())
|
||||
}
|
||||
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data, ok := response.Data.(map[string]any)
|
||||
if response.Code != httpkit.CodeOK || !ok || data["profile_completed"] != true {
|
||||
t.Fatalf("unexpected pending complete response: %+v", response)
|
||||
}
|
||||
token, ok := data["token"].(map[string]any)
|
||||
if !ok || token["access_token"] != "access-pending-completed" || token["user_id"] != "10088" || token["profile_completed"] != true {
|
||||
t.Fatalf("pending complete must return real user token: %+v", data["token"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteOnboardingReturnsInvalidInviteCodeMessage(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
completeErr: xerr.ToGRPCError(xerr.New(xerr.InvalidInviteCode, "invalid invite code")),
|
||||
@ -8556,18 +8434,6 @@ func TestProfileMutationEndpointsRequireCompletedProfileToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingRegistrationTokenIsRejectedOutsideRegistrationWhitelist(t *testing.T) {
|
||||
router := NewHandlerWithClients(&fakeRoomClient{}, &fakeUserAuthClient{}, nil, &fakeUserProfileClient{}).Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/users/me/display-id/change", bytes.NewReader([]byte(`{"display_user_id":"168888"}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signPendingRegistrationToken(t, "secret", "tpr-test"))
|
||||
request.Header.Set("X-Request-ID", "req-pending-denied")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
assertEnvelope(t, recorder, http.StatusPreconditionRequired, httpkit.CodeProfileRequired, "req-pending-denied")
|
||||
}
|
||||
|
||||
func TestProfileGateRejectsIncompleteUsersForRoomIMRTCPaidCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -8700,29 +8566,6 @@ func TestUploadAvatarAllowsIncompleteProfileAndWritesObject(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadAvatarWithPendingRegistrationTokenWritesPendingObjectKey(t *testing.T) {
|
||||
uploader := &fakeObjectUploader{}
|
||||
handler := NewHandler(&fakeRoomClient{})
|
||||
handler.SetObjectUploader(uploader)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
payload := append([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}, bytes.Repeat([]byte{1}, 32)...)
|
||||
body, contentType := multipartUploadBody(t, "file", "avatar.png", "image/png", payload)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/files/avatar/upload", body)
|
||||
request.Header.Set("Content-Type", contentType)
|
||||
request.Header.Set("Authorization", "Bearer "+signPendingRegistrationToken(t, "secret", "tpr-test"))
|
||||
request.Header.Set("X-Request-ID", "req-upload-pending-avatar")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if !strings.HasPrefix(uploader.lastKey, "app/avatars/pending/tpr-test/") || !strings.HasSuffix(uploader.lastKey, ".png") {
|
||||
t.Fatalf("pending avatar must not use user_id=0 key: %q", uploader.lastKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadAvatarRejectsSpoofedImageContentType(t *testing.T) {
|
||||
uploader := &fakeObjectUploader{}
|
||||
handler := NewHandler(&fakeRoomClient{})
|
||||
@ -9646,27 +9489,6 @@ func signGatewayTokenWithExpiresAt(t *testing.T, secret string, userID int64, pr
|
||||
return signed
|
||||
}
|
||||
|
||||
func signPendingRegistrationToken(t *testing.T, secret string, pendingID string) string {
|
||||
t.Helper()
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"app_code": "lalu",
|
||||
"sub": pendingID,
|
||||
"sid": pendingID,
|
||||
"typ": "pending_registration",
|
||||
"pending_registration_id": pendingID,
|
||||
"profile_completed": false,
|
||||
"onboarding_status": "profile_required",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
})
|
||||
signed, err := token.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
t.Fatalf("sign pending token failed: %v", err)
|
||||
}
|
||||
|
||||
return signed
|
||||
}
|
||||
|
||||
func multipartUploadBody(t *testing.T, fieldName string, filename string, contentType string, payload []byte) (*bytes.Buffer, string) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@ -57,7 +57,6 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
ObjectUploader: h.objectUploader,
|
||||
})
|
||||
userAPI := userapi.New(userapi.Config{
|
||||
UserAuthClient: h.userClient,
|
||||
UserIdentityClient: h.userIdentityClient,
|
||||
UserProfileClient: h.userProfileClient,
|
||||
UserSocialClient: h.userSocialClient,
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
// Handler owns user-facing HTTP endpoints and relationship endpoints.
|
||||
// It delegates user facts to user-service and only translates HTTP protocol shape.
|
||||
type Handler struct {
|
||||
userAuthClient client.UserAuthClient
|
||||
userIdentityClient client.UserIdentityClient
|
||||
userProfileClient client.UserProfileClient
|
||||
userSocialClient client.UserSocialClient
|
||||
@ -24,7 +23,6 @@ type Handler struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
UserAuthClient client.UserAuthClient
|
||||
UserIdentityClient client.UserIdentityClient
|
||||
UserProfileClient client.UserProfileClient
|
||||
UserSocialClient client.UserSocialClient
|
||||
@ -39,7 +37,6 @@ type Config struct {
|
||||
|
||||
func New(config Config) *Handler {
|
||||
return &Handler{
|
||||
userAuthClient: config.UserAuthClient,
|
||||
userIdentityClient: config.UserIdentityClient,
|
||||
userProfileClient: config.UserProfileClient,
|
||||
userSocialClient: config.UserSocialClient,
|
||||
|
||||
@ -260,10 +260,6 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
if auth.IsPendingRegistrationFromContext(request.Context()) {
|
||||
h.completePendingThirdPartyRegistration(writer, request, body.Username, body.Avatar, body.Gender, body.Country, body.InviteCode)
|
||||
return
|
||||
}
|
||||
if h.userProfileClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
@ -290,37 +286,6 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) completePendingThirdPartyRegistration(writer http.ResponseWriter, request *http.Request, username string, avatar string, gender string, country string, inviteCode string) {
|
||||
if h.userAuthClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
pendingID := auth.PendingRegistrationIDFromContext(request.Context())
|
||||
if pendingID == "" {
|
||||
httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
resp, err := h.userAuthClient.CompletePendingThirdPartyRegistration(request.Context(), &userv1.CompletePendingThirdPartyRegistrationRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
PendingRegistrationId: pendingID,
|
||||
Username: username,
|
||||
Avatar: avatar,
|
||||
Gender: gender,
|
||||
Country: country,
|
||||
InviteCode: inviteCode,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpkit.WriteOK(writer, request, completeOnboardingData{
|
||||
userProfileData: profileData(resp.GetUser(), 0),
|
||||
Invite: inviteBindingDataFromProto(resp.GetInvite()),
|
||||
Token: authData(resp.GetToken(), nil),
|
||||
})
|
||||
}
|
||||
|
||||
// getMyProfile 返回当前登录用户的基础资料。
|
||||
func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.userProfileClient == nil {
|
||||
|
||||
@ -1116,44 +1116,6 @@ CREATE TABLE IF NOT EXISTS third_party_identities (
|
||||
KEY idx_third_party_user_id (app_code, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='第三方身份表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pending_third_party_registrations (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
pending_id VARCHAR(96) NOT NULL COMMENT '待完成注册 ID',
|
||||
provider VARCHAR(64) NOT NULL COMMENT '提供方',
|
||||
provider_subject VARCHAR(191) NOT NULL COMMENT '提供方主体',
|
||||
provider_union_id VARCHAR(191) NULL COMMENT '提供方联合 ID',
|
||||
username VARCHAR(64) NULL COMMENT '首登暂存用户名',
|
||||
gender VARCHAR(32) NULL COMMENT '首登暂存性别',
|
||||
country VARCHAR(64) NULL COMMENT '首登暂存国家',
|
||||
invite_code VARCHAR(64) NULL COMMENT '暂存邀请码',
|
||||
register_ip VARCHAR(64) NULL COMMENT '注册入口 IP',
|
||||
register_user_agent VARCHAR(512) NULL COMMENT '注册用户代理',
|
||||
country_by_ip VARCHAR(64) NULL COMMENT 'IP 解析国家',
|
||||
device_id VARCHAR(128) NULL COMMENT '注册设备 ID',
|
||||
device VARCHAR(128) NULL COMMENT '注册设备',
|
||||
os_version VARCHAR(64) NULL COMMENT 'os版本',
|
||||
avatar VARCHAR(512) NULL COMMENT '首登暂存头像',
|
||||
birth_date VARCHAR(32) NULL COMMENT '首登暂存生日',
|
||||
app_version VARCHAR(64) NULL COMMENT '应用版本',
|
||||
build_number VARCHAR(64) NULL COMMENT '构建编号',
|
||||
source VARCHAR(64) NULL COMMENT '来源',
|
||||
install_channel VARCHAR(64) NULL COMMENT '安装渠道',
|
||||
campaign VARCHAR(128) NULL COMMENT '投放活动',
|
||||
platform VARCHAR(16) NULL COMMENT '平台',
|
||||
language VARCHAR(32) NULL COMMENT '语言',
|
||||
timezone VARCHAR(64) NULL COMMENT '时区',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '待完成注册状态',
|
||||
completed_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '完成后的用户 ID',
|
||||
expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '过期时间,UTC epoch ms',
|
||||
completed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '完成时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pending_id),
|
||||
UNIQUE KEY uk_pending_third_party_subject (app_code, provider, provider_subject),
|
||||
KEY idx_pending_third_party_status (app_code, status, expires_at_ms),
|
||||
KEY idx_pending_third_party_completed_user (app_code, completed_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方待完成注册表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_sessions (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
session_id VARCHAR(96) NOT NULL PRIMARY KEY COMMENT '会话 ID',
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS pending_third_party_registrations (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
pending_id VARCHAR(96) NOT NULL COMMENT '待完成注册 ID',
|
||||
provider VARCHAR(64) NOT NULL COMMENT '提供方',
|
||||
provider_subject VARCHAR(191) NOT NULL COMMENT '提供方主体',
|
||||
provider_union_id VARCHAR(191) NULL COMMENT '提供方联合 ID',
|
||||
username VARCHAR(64) NULL COMMENT '首登暂存用户名',
|
||||
gender VARCHAR(32) NULL COMMENT '首登暂存性别',
|
||||
country VARCHAR(64) NULL COMMENT '首登暂存国家',
|
||||
invite_code VARCHAR(64) NULL COMMENT '暂存邀请码',
|
||||
register_ip VARCHAR(64) NULL COMMENT '注册入口 IP',
|
||||
register_user_agent VARCHAR(512) NULL COMMENT '注册用户代理',
|
||||
country_by_ip VARCHAR(64) NULL COMMENT 'IP 解析国家',
|
||||
device_id VARCHAR(128) NULL COMMENT '注册设备 ID',
|
||||
device VARCHAR(128) NULL COMMENT '注册设备',
|
||||
os_version VARCHAR(64) NULL COMMENT 'os版本',
|
||||
avatar VARCHAR(512) NULL COMMENT '首登暂存头像',
|
||||
birth_date VARCHAR(32) NULL COMMENT '首登暂存生日',
|
||||
app_version VARCHAR(64) NULL COMMENT '应用版本',
|
||||
build_number VARCHAR(64) NULL COMMENT '构建编号',
|
||||
source VARCHAR(64) NULL COMMENT '来源',
|
||||
install_channel VARCHAR(64) NULL COMMENT '安装渠道',
|
||||
campaign VARCHAR(128) NULL COMMENT '投放活动',
|
||||
platform VARCHAR(16) NULL COMMENT '平台',
|
||||
language VARCHAR(32) NULL COMMENT '语言',
|
||||
timezone VARCHAR(64) NULL COMMENT '时区',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '待完成注册状态',
|
||||
completed_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '完成后的用户 ID',
|
||||
expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '过期时间,UTC epoch ms',
|
||||
completed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '完成时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pending_id),
|
||||
UNIQUE KEY uk_pending_third_party_subject (app_code, provider, provider_subject),
|
||||
KEY idx_pending_third_party_status (app_code, status, expires_at_ms),
|
||||
KEY idx_pending_third_party_completed_user (app_code, completed_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方待完成注册表';
|
||||
@ -101,13 +101,6 @@ type ThirdPartyIdentity struct {
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
const (
|
||||
// PendingThirdPartyRegistrationStatusPending 表示三方身份已验证,但还没有完整资料,不能创建 users。
|
||||
PendingThirdPartyRegistrationStatusPending = "pending"
|
||||
// PendingThirdPartyRegistrationStatusCompleted 表示 pending 已经消费成真实用户,不能再次创建账号。
|
||||
PendingThirdPartyRegistrationStatusCompleted = "completed"
|
||||
)
|
||||
|
||||
// LoginAudit 是登录注册链路的审计记录。
|
||||
// 审计不能包含密码、三方 credential 或 refresh token 原文。
|
||||
type LoginAudit struct {
|
||||
@ -199,32 +192,3 @@ type ThirdPartyRegistration struct {
|
||||
// Timezone 是客户端 IANA 时区,例如 America/Los_Angeles。
|
||||
Timezone string
|
||||
}
|
||||
|
||||
// PendingThirdPartyRegistration 是旧客户端兼容注册态。
|
||||
// 它只保存已经验证过的 provider subject 和注册来源快照,不占用 user_id、短号、IM 账号或注册奖励。
|
||||
type PendingThirdPartyRegistration struct {
|
||||
// AppCode 是 pending 所属 App,必须和 access token claim 一致。
|
||||
AppCode string
|
||||
// PendingID 是 pending token 的服务端定位键,不是用户 ID,不能暴露成业务身份。
|
||||
PendingID string
|
||||
// Provider 是三方平台标识。
|
||||
Provider string
|
||||
// ProviderSubject 是 provider 内稳定身份;完成注册时会写入 third_party_identities。
|
||||
ProviderSubject string
|
||||
// ProviderUnionID 是 provider 可选联合身份。
|
||||
ProviderUnionID string
|
||||
// Registration 保存首登设备、渠道、IP 和可选邀请码等来源快照。
|
||||
Registration ThirdPartyRegistration
|
||||
// Status 表示 pending 是否仍可消费。
|
||||
Status string
|
||||
// CompletedUserID 是 pending 被消费后的真实用户 ID,未完成时为 0。
|
||||
CompletedUserID int64
|
||||
// ExpiresAtMs 是 pending 持久化保留期限,UTC epoch ms。
|
||||
ExpiresAtMs int64
|
||||
// CompletedAtMs 是 pending 成功消费时间,UTC epoch ms。
|
||||
CompletedAtMs int64
|
||||
// CreatedAtMs 是 pending 创建时间,UTC epoch ms。
|
||||
CreatedAtMs int64
|
||||
// UpdatedAtMs 是 pending 最近更新时间,UTC epoch ms。
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
@ -17,10 +17,6 @@ const (
|
||||
hashAlgBcrypt = "bcrypt"
|
||||
// tokenType 是对外 token 类型。
|
||||
tokenType = "Bearer"
|
||||
// accessTokenTypeUser 是真实用户会话 token,gateway 会从中读取 user_id 和 session_id。
|
||||
accessTokenTypeUser = "access"
|
||||
// accessTokenTypePendingRegistration 是旧 Flutter 兼容注册 token,只能访问头像上传和资料完成接口。
|
||||
accessTokenTypePendingRegistration = "pending_registration"
|
||||
// loginPassword 是密码登录审计类型。
|
||||
loginPassword = "password"
|
||||
// loginThird 是三方登录审计类型。
|
||||
@ -66,18 +62,12 @@ type AuthRepository interface {
|
||||
UpdateSessionHeartbeat(ctx context.Context, sessionID string, userID int64, heartbeatAtMs int64, requestID string) (authdomain.Session, error)
|
||||
// FindThirdPartyIdentity 查找 provider + subject 的绑定。
|
||||
FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error)
|
||||
// UpsertPendingThirdPartyRegistration 保存或刷新已验证但未补齐资料的三方注册态,不创建 users 和短号。
|
||||
UpsertPendingThirdPartyRegistration(ctx context.Context, pending authdomain.PendingThirdPartyRegistration) (authdomain.PendingThirdPartyRegistration, error)
|
||||
// FindPendingThirdPartyRegistration 按 pending_id 读取待完成注册态,用于 pending token 的资料提交。
|
||||
FindPendingThirdPartyRegistration(ctx context.Context, appCode string, pendingID string) (authdomain.PendingThirdPartyRegistration, error)
|
||||
// CountUsersByRegisterDeviceID 统计当前 App 下已使用同一注册设备号的账号数量。
|
||||
CountUsersByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error)
|
||||
// ListDisplayUserIDsByRegisterDeviceID 按原注册顺序列出同设备已注册账号短号,用于上限判断和失败提示。
|
||||
ListDisplayUserIDsByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) ([]string, error)
|
||||
// CreateThirdPartyUser 原子创建用户、默认短号、三方身份和首个 session。
|
||||
CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, invite invitedomain.BindCommand, maxAccountsPerDevice int32) error
|
||||
// CompletePendingThirdPartyUser 原子消费 pending、创建完整用户、默认短号、三方身份和首个 session。
|
||||
CompletePendingThirdPartyUser(ctx context.Context, pendingID string, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, invite invitedomain.BindCommand, maxAccountsPerDevice int32) (userdomain.User, error)
|
||||
// GetRegisterRiskConfig 读取当前 App 的注册风控配置。
|
||||
GetRegisterRiskConfig(ctx context.Context, appCode string) (authdomain.RegisterRiskConfig, error)
|
||||
// UpsertRegisterRiskConfig 保存当前 App 的注册风控配置。
|
||||
|
||||
@ -309,8 +309,8 @@ func TestThirdPartyUserSetsPasswordThenLogsIn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingThirdPartyCompletionCreatesUserAndEmitsUserRegisteredOutboxWithSelectedCountry(t *testing.T) {
|
||||
// UserRegistered 是“用户填完注册资料”事实;三方首登 pending 不能提前创建 users 或写未知国家 cohort。
|
||||
func TestCompleteOnboardingEmitsUserRegisteredOutboxWithSelectedCountry(t *testing.T) {
|
||||
// UserRegistered 是“注册页资料完成”事实,不是三方登录占位账号创建事实;统计国家必须来自最终提交的国家。
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
country := seedCountry(t, repository, "AE")
|
||||
@ -325,27 +325,22 @@ func TestPendingThirdPartyCompletionCreatesUserAndEmitsUserRegisteredOutboxWithS
|
||||
if err != nil {
|
||||
t.Fatalf("LoginThirdParty failed: %v", err)
|
||||
}
|
||||
if !isNewUser || token.UserID != 0 || token.DisplayUserID != "" || token.AccessToken == "" || token.RefreshToken != "" || token.ProfileCompleted {
|
||||
t.Fatalf("third-party login should return pending token without user: token=%+v isNew=%v", token, isNewUser)
|
||||
}
|
||||
if _, lookupErr := repository.GetUser(ctx, 900031); !xerr.IsCode(lookupErr, xerr.NotFound) {
|
||||
t.Fatalf("pending third-party login must not create users row, lookupErr=%v", lookupErr)
|
||||
if !isNewUser || token.ProfileCompleted {
|
||||
t.Fatalf("third-party login should create profile-required user: token=%+v isNew=%v", token, isNewUser)
|
||||
}
|
||||
assertUserRegisteredOutboxCount(t, repository, token.UserID, 0)
|
||||
|
||||
completedAtMS := int64(2500)
|
||||
loginAt = time.UnixMilli(completedAtMS)
|
||||
user, completedToken, err := authSvc.CompletePendingThirdPartyRegistration(ctx, token.SessionID, "Amina", "https://cdn.example/amina.png", "female", "ae", "", authservice.Meta{AppCode: "lalu", RequestID: "req-complete-onboarding"})
|
||||
userSvc := newUserService(repository, userservice.WithClock(func() time.Time { return time.UnixMilli(completedAtMS).UTC() }))
|
||||
user, err := userSvc.CompleteOnboarding(ctx, token.UserID, "Amina", "https://cdn.example/amina.png", "female", "ae", "", "req-complete-onboarding")
|
||||
if err != nil {
|
||||
t.Fatalf("CompletePendingThirdPartyRegistration failed: %v", err)
|
||||
t.Fatalf("CompleteOnboarding failed: %v", err)
|
||||
}
|
||||
if completedToken.UserID != user.UserID || completedToken.RefreshToken == "" || !completedToken.ProfileCompleted {
|
||||
t.Fatalf("completed token mismatch: token=%+v user=%+v", completedToken, user)
|
||||
}
|
||||
if user.UserID != 900031 || !user.ProfileCompleted || user.Country != "AE" || user.RegionID != region.RegionID || user.ProfileCompletedAtMs != completedAtMS {
|
||||
if !user.ProfileCompleted || user.Country != "AE" || user.RegionID != region.RegionID || user.ProfileCompletedAtMs != completedAtMS {
|
||||
t.Fatalf("completed user snapshot mismatch: %+v", user)
|
||||
}
|
||||
assertUserRegisteredOutboxCount(t, repository, user.UserID, 1)
|
||||
assertUserRegisteredOutboxFact(t, repository, user.UserID, country.CountryID, region.RegionID, completedAtMS)
|
||||
assertUserRegisteredOutboxCount(t, repository, token.UserID, 1)
|
||||
assertUserRegisteredOutboxFact(t, repository, token.UserID, country.CountryID, region.RegionID, completedAtMS)
|
||||
}
|
||||
|
||||
func TestQuickCreateAccountCreatesCompletedPasswordLogin(t *testing.T) {
|
||||
@ -1047,57 +1042,10 @@ func TestThirdPartyRegisterRejectsReusedDeviceID(t *testing.T) {
|
||||
// 第二个 provider subject 会创建新账号,必须被注册设备唯一性拦截。
|
||||
t.Fatalf("expected DEVICE_ALREADY_REGISTERED for reused device, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "you device already register 1 account 163000, login failed") {
|
||||
t.Fatalf("device registered error should include account count and display id, got %v", err)
|
||||
}
|
||||
if _, lookupErr := repository.GetUser(ctx, 900021); !xerr.IsCode(lookupErr, xerr.NotFound) {
|
||||
t.Fatalf("failed registration must not create empty user, lookupErr=%v", lookupErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThirdPartyRegisterAllowsUntilConfiguredDeviceLimit(t *testing.T) {
|
||||
// 三方注册仍遵守 max_accounts_per_device:未达到上限时放行,达到上限时才失败并展示全部短号。
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
seedCountry(t, repository, "SG")
|
||||
now := time.UnixMilli(2000)
|
||||
svc := newAuthService(repository, &now, []int64{900030, 900031, 900032}, []string{"100030", "100031", "100032"})
|
||||
if _, err := svc.UpdateRegisterRiskConfig(ctx, 2, 700001); err != nil {
|
||||
t.Fatalf("UpdateRegisterRiskConfig failed: %v", err)
|
||||
}
|
||||
registration := thirdPartyRegistration("android")
|
||||
registration.DeviceID = "device-two-account-limit"
|
||||
|
||||
firstToken, isNewUser, err := svc.LoginThirdParty(ctx, "wechat", "openid-limit-first", registration, authservice.Meta{RequestID: "req-limit-first"})
|
||||
if err != nil {
|
||||
t.Fatalf("first LoginThirdParty failed: %v", err)
|
||||
}
|
||||
if !isNewUser || firstToken.UserID != 900030 {
|
||||
t.Fatalf("unexpected first registration: token=%+v isNew=%v", firstToken, isNewUser)
|
||||
}
|
||||
|
||||
secondToken, isNewUser, err := svc.LoginThirdParty(ctx, "wechat", "openid-limit-second", registration, authservice.Meta{RequestID: "req-limit-second"})
|
||||
if err != nil {
|
||||
t.Fatalf("second LoginThirdParty should be allowed below configured limit: %v", err)
|
||||
}
|
||||
if !isNewUser || secondToken.UserID != 900031 {
|
||||
t.Fatalf("unexpected second registration: token=%+v isNew=%v", secondToken, isNewUser)
|
||||
}
|
||||
|
||||
_, _, err = svc.LoginThirdParty(ctx, "wechat", "openid-limit-third", registration, authservice.Meta{RequestID: "req-limit-third"})
|
||||
if !xerr.IsCode(err, xerr.DeviceAlreadyRegistered) {
|
||||
t.Fatalf("expected DEVICE_ALREADY_REGISTERED after reaching configured limit, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "you device already register 2 account 163000,163001, login failed") {
|
||||
t.Fatalf("limit error should include count and all display ids, got %v", err)
|
||||
}
|
||||
if _, lookupErr := repository.GetUser(ctx, 900032); !xerr.IsCode(lookupErr, xerr.NotFound) {
|
||||
t.Fatalf("failed over-limit registration must not create empty user, lookupErr=%v", lookupErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThirdPartyRegisterWithMinimalProfileCreatesOnlyPendingRegistration(t *testing.T) {
|
||||
// 三方首次登录只验证认证材料、设备和平台;公开资料缺失时只返回 pending token,不创建空 users。
|
||||
func TestThirdPartyRegisterAllowsMinimalProfileFields(t *testing.T) {
|
||||
// 三方首次登录只需要认证材料、设备和平台;公开资料由后续 onboarding 原子提交。
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := time.UnixMilli(1000)
|
||||
@ -1110,48 +1058,8 @@ func TestThirdPartyRegisterWithMinimalProfileCreatesOnlyPendingRegistration(t *t
|
||||
if err != nil {
|
||||
t.Fatalf("LoginThirdParty with minimal registration failed: %v", err)
|
||||
}
|
||||
if !isNewUser || token.UserID != 0 || token.DisplayUserID != "" || token.RefreshToken != "" || token.AccessToken == "" || token.ProfileCompleted || token.OnboardingStatus != string(userdomain.OnboardingStatusProfileRequired) {
|
||||
t.Fatalf("minimal third-party registration must return pending token only: token=%+v isNew=%v", token, isNewUser)
|
||||
}
|
||||
if _, err := repository.GetUser(ctx, 900012); !xerr.IsCode(err, xerr.NotFound) {
|
||||
t.Fatalf("minimal third-party registration must not create users row, got %v", err)
|
||||
}
|
||||
pending, err := repository.FindPendingThirdPartyRegistration(ctx, "lalu", token.SessionID)
|
||||
if err != nil {
|
||||
t.Fatalf("pending registration was not persisted: %v", err)
|
||||
}
|
||||
if pending.Provider != "wechat" || pending.ProviderSubject != "openid-minimal" || pending.Registration.DeviceID != "dev-ios" {
|
||||
t.Fatalf("pending registration snapshot mismatch: %+v", pending)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingThirdPartyCompletionRejectsMissingProfileFieldsWithoutCreatingUser(t *testing.T) {
|
||||
// 资料完成接口收到缺字段请求时只返回参数错误,不能因为 pending 已存在就提前创建 users。
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := time.UnixMilli(1000)
|
||||
svc := newAuthService(repository, &now, []int64{900013}, []string{"100013"})
|
||||
|
||||
token, _, err := svc.LoginThirdParty(ctx, "wechat", "openid-pending-missing", authdomain.ThirdPartyRegistration{
|
||||
DeviceID: "dev-ios",
|
||||
Platform: "ios",
|
||||
}, authservice.Meta{AppCode: "lalu", RequestID: "req-pending-login"})
|
||||
if err != nil {
|
||||
t.Fatalf("LoginThirdParty failed: %v", err)
|
||||
}
|
||||
_, _, err = svc.CompletePendingThirdPartyRegistration(ctx, token.SessionID, "Amina", "https://cdn.example/amina.png", "female", "", "", authservice.Meta{AppCode: "lalu", RequestID: "req-pending-missing"})
|
||||
if !xerr.IsCode(err, xerr.InvalidArgument) || !strings.Contains(err.Error(), "country") {
|
||||
t.Fatalf("missing country must return INVALID_ARGUMENT with field name, got %v", err)
|
||||
}
|
||||
if _, lookupErr := repository.GetUser(ctx, 900013); !xerr.IsCode(lookupErr, xerr.NotFound) {
|
||||
t.Fatalf("missing profile completion must not create user, lookupErr=%v", lookupErr)
|
||||
}
|
||||
pending, err := repository.FindPendingThirdPartyRegistration(ctx, "lalu", token.SessionID)
|
||||
if err != nil {
|
||||
t.Fatalf("pending registration should remain after invalid completion: %v", err)
|
||||
}
|
||||
if pending.Status != authdomain.PendingThirdPartyRegistrationStatusPending {
|
||||
t.Fatalf("pending status changed after invalid completion: %+v", pending)
|
||||
if !isNewUser || token.ProfileCompleted || token.OnboardingStatus != string(userdomain.OnboardingStatusProfileRequired) {
|
||||
t.Fatalf("minimal third-party registration must create profile-required user: token=%+v isNew=%v", token, isNewUser)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1215,9 +1123,6 @@ func TestThirdPartyRegisterStoresRegistrationProfile(t *testing.T) {
|
||||
if !isNewUser {
|
||||
t.Fatalf("first third-party login must create user")
|
||||
}
|
||||
if !token.ProfileCompleted || token.OnboardingStatus != string(userdomain.OnboardingStatusCompleted) {
|
||||
t.Fatalf("complete third-party registration must return completed token: %+v", token)
|
||||
}
|
||||
|
||||
user, err := repository.GetUser(ctx, token.UserID)
|
||||
if err != nil {
|
||||
@ -1238,7 +1143,6 @@ func TestThirdPartyRegisterStoresRegistrationProfile(t *testing.T) {
|
||||
if imImporter.userID != token.UserID || imImporter.nickname != "hy" || imImporter.faceURL != "https://cdn.example/avatar.png" {
|
||||
t.Fatalf("third-party registration must import IM account: %+v", imImporter)
|
||||
}
|
||||
assertUserRegisteredOutboxCount(t, repository, token.UserID, 1)
|
||||
}
|
||||
|
||||
func TestThirdPartyRegisterAssignsRegionAndExistingLoginKeepsOriginalCountry(t *testing.T) {
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/logx"
|
||||
"hyapp/pkg/xerr"
|
||||
authdomain "hyapp/services/user-service/internal/domain/auth"
|
||||
@ -92,11 +91,6 @@ func (s *Service) LoginThirdParty(ctx context.Context, provider string, credenti
|
||||
if err != nil {
|
||||
return authdomain.Token{}, false, err
|
||||
}
|
||||
if err := validateThirdPartyRegistrationRequiredForCreate(registration); err != nil {
|
||||
// 旧 Flutter 首次三方登录不会提交昵称、头像、性别和国家;此时只保存已验证 provider subject,
|
||||
// 返回 profile_required pending token 让客户端继续走原资料页,禁止提前创建 users 空账号。
|
||||
return s.createPendingThirdPartyRegistration(ctx, provider, profile, registration, meta)
|
||||
}
|
||||
|
||||
return s.createThirdPartyUser(ctx, provider, profile, registration, meta, maxAccountsPerDevice)
|
||||
}
|
||||
@ -108,20 +102,19 @@ func (s *Service) ensureDeviceCanRegisterThirdParty(ctx context.Context, registr
|
||||
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
|
||||
return 0, err
|
||||
}
|
||||
displayUserIDs, err := s.authRepository.ListDisplayUserIDsByRegisterDeviceID(ctx, registration.AppCode, registration.DeviceID)
|
||||
count, err := s.authRepository.CountUsersByRegisterDeviceID(ctx, registration.AppCode, registration.DeviceID)
|
||||
if err != nil {
|
||||
// 设备查重和短号列表必须来自同一读模型;读取失败不能退化为注册,否则会再次制造空资料账号。
|
||||
// 设备查重失败不能退化为注册,否则并发和依赖异常都会突破一机一号约束。
|
||||
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
|
||||
return 0, err
|
||||
}
|
||||
count := len(displayUserIDs)
|
||||
if count < int(config.MaxAccountsPerDevice) {
|
||||
if count < int64(config.MaxAccountsPerDevice) {
|
||||
return config.MaxAccountsPerDevice, nil
|
||||
}
|
||||
|
||||
// 已达到配置上限时拒绝创建新账号;错误文案只列当前设备已注册短号,不暴露内部 user_id。
|
||||
// 已达到配置上限时拒绝创建新账号;审计不写已有 user_id,避免客户端枚举设备归属。
|
||||
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.DeviceAlreadyRegistered))
|
||||
return 0, xerr.New(xerr.DeviceAlreadyRegistered, xerr.DeviceAlreadyRegisteredMessage(count, displayUserIDs))
|
||||
return 0, xerr.New(xerr.DeviceAlreadyRegistered, "device already registered")
|
||||
}
|
||||
|
||||
func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdomain.ThirdPartyIdentity, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) {
|
||||
@ -162,7 +155,6 @@ func (s *Service) createThirdPartyUser(ctx context.Context, provider string, pro
|
||||
// 三方首次登录要同时生成 user_id 和默认短号,短号冲突时有限重试。
|
||||
user, displayIdentity := s.newUserWithIdentity(attempt)
|
||||
user = applyThirdPartyRegistration(user, registration)
|
||||
user = completeRegistrationUser(user, user.CreatedAtMs)
|
||||
displayUserID, err := s.authRepository.AllocateDisplayUserID(ctx, registration.AppCode)
|
||||
if err != nil {
|
||||
return authdomain.Token{}, false, err
|
||||
@ -239,196 +231,6 @@ func (s *Service) createThirdPartyUser(ctx context.Context, provider string, pro
|
||||
return authdomain.Token{}, false, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed")
|
||||
}
|
||||
|
||||
func completeRegistrationUser(user userdomain.User, completedAtMs int64) userdomain.User {
|
||||
// 能进入真实建号事务的注册资料已经包含 username/avatar/gender/country;
|
||||
// 因此 users 必须直接落 completed,UserRegistered outbox 才能携带正确国家 cohort。
|
||||
user.ProfileCompleted = true
|
||||
user.ProfileCompletedAtMs = completedAtMs
|
||||
user.OnboardingStatus = userdomain.OnboardingStatusCompleted
|
||||
user.UpdatedAtMs = completedAtMs
|
||||
return user
|
||||
}
|
||||
|
||||
func (s *Service) createPendingThirdPartyRegistration(ctx context.Context, provider string, profile authdomain.ThirdPartyProfile, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) {
|
||||
nowMs := s.now().UnixMilli()
|
||||
pending := authdomain.PendingThirdPartyRegistration{
|
||||
AppCode: registration.AppCode,
|
||||
PendingID: idgen.New("tpr"),
|
||||
Provider: provider,
|
||||
ProviderSubject: profile.ProviderSubject,
|
||||
ProviderUnionID: profile.ProviderUnionID,
|
||||
Registration: registration,
|
||||
Status: authdomain.PendingThirdPartyRegistrationStatusPending,
|
||||
ExpiresAtMs: nowMs + s.cfg.RefreshTokenTTLSec*1000,
|
||||
CreatedAtMs: nowMs,
|
||||
UpdatedAtMs: nowMs,
|
||||
CompletedUserID: 0,
|
||||
CompletedAtMs: 0,
|
||||
}
|
||||
// provider subject 已通过三方 verifier;pending 只持久化 subject 和来源快照,不保存 credential 原文。
|
||||
pending, err := s.authRepository.UpsertPendingThirdPartyRegistration(ctx, pending)
|
||||
if err != nil {
|
||||
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
|
||||
return authdomain.Token{}, false, err
|
||||
}
|
||||
if pending.Status == authdomain.PendingThirdPartyRegistrationStatusCompleted {
|
||||
// 理论上已完成 pending 会先命中 third_party_identities;若读模型短暂不一致,拒绝再次创建 users。
|
||||
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.Conflict))
|
||||
return authdomain.Token{}, false, xerr.New(xerr.Conflict, "pending registration already completed")
|
||||
}
|
||||
token, err := s.issuePendingRegistrationToken(pending)
|
||||
if err != nil {
|
||||
return authdomain.Token{}, false, err
|
||||
}
|
||||
s.audit(ctx, meta, 0, loginThird, provider, resultSuccess, "")
|
||||
return token, true, nil
|
||||
}
|
||||
|
||||
func (s *Service) CompletePendingThirdPartyRegistration(ctx context.Context, pendingID string, username string, avatar string, gender string, country string, inviteCode string, meta Meta) (userdomain.User, authdomain.Token, error) {
|
||||
meta.AppCode = appcode.Normalize(meta.AppCode)
|
||||
pendingID = strings.TrimSpace(pendingID)
|
||||
if pendingID == "" {
|
||||
return userdomain.User{}, authdomain.Token{}, xerr.New(xerr.InvalidArgument, "pending_registration_id is required")
|
||||
}
|
||||
if s.authRepository == nil {
|
||||
return userdomain.User{}, authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
|
||||
}
|
||||
if s.userRepository == nil {
|
||||
return userdomain.User{}, authdomain.Token{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||||
}
|
||||
pending, err := s.authRepository.FindPendingThirdPartyRegistration(ctx, meta.AppCode, pendingID)
|
||||
if err != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
if pending.Status != authdomain.PendingThirdPartyRegistrationStatusPending {
|
||||
return userdomain.User{}, authdomain.Token{}, xerr.New(xerr.Conflict, "pending registration already completed")
|
||||
}
|
||||
nowMs := s.now().UnixMilli()
|
||||
if pending.ExpiresAtMs > 0 && pending.ExpiresAtMs <= nowMs {
|
||||
return userdomain.User{}, authdomain.Token{}, xerr.New(xerr.SessionExpired, "pending registration expired")
|
||||
}
|
||||
|
||||
registration := pending.Registration
|
||||
registration.Username = strings.TrimSpace(username)
|
||||
registration.Avatar = strings.TrimSpace(avatar)
|
||||
registration.Gender = strings.TrimSpace(gender)
|
||||
registration.Country = strings.ToUpper(strings.TrimSpace(country))
|
||||
if normalizedInvite := invitedomain.NormalizeCode(inviteCode); normalizedInvite != "" {
|
||||
registration.InviteCode = normalizedInvite
|
||||
}
|
||||
registration = normalizeThirdPartyRegistration(registration, meta)
|
||||
if err := validateThirdPartyRegistration(registration); err != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
if err := validateThirdPartyRegistrationRequiredForCreate(registration); err != nil {
|
||||
// pending 必须保持待完成状态,方便旧 Flutter 用户补完缺失项后用同一 token 再提交。
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
maxAccountsPerDevice, err := s.ensureDeviceCanRegisterThirdParty(ctx, registration, meta, pending.Provider)
|
||||
if err != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
registration, err = s.resolveRegistrationCountry(ctx, registration)
|
||||
if err != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
|
||||
var user userdomain.User
|
||||
var session authdomain.Session
|
||||
var refreshToken string
|
||||
var lastErr error
|
||||
completed := false
|
||||
for attempt := 0; attempt < s.allocateMaxAttempts; attempt++ {
|
||||
var displayIdentity userdomain.Identity
|
||||
user, displayIdentity = s.newUserWithIdentity(attempt)
|
||||
user = applyThirdPartyRegistration(user, registration)
|
||||
user = completeRegistrationUser(user, nowMs)
|
||||
displayUserID, allocErr := s.authRepository.AllocateDisplayUserID(ctx, registration.AppCode)
|
||||
if allocErr != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, allocErr
|
||||
}
|
||||
user.DefaultDisplayUserID = displayUserID
|
||||
user.CurrentDisplayUserID = displayUserID
|
||||
displayIdentity.AppCode = registration.AppCode
|
||||
displayIdentity.DisplayUserID = displayUserID
|
||||
displayIdentity.DefaultDisplayUserID = displayUserID
|
||||
if !userdomain.ValidDisplayUserID(displayIdentity.DisplayUserID) {
|
||||
continue
|
||||
}
|
||||
|
||||
var sessionErr error
|
||||
session, refreshToken, sessionErr = s.newSession(registration.AppCode, user.UserID, registration.DeviceID)
|
||||
if sessionErr != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, sessionErr
|
||||
}
|
||||
thirdParty := authdomain.ThirdPartyIdentity{
|
||||
AppCode: registration.AppCode,
|
||||
UserID: user.UserID,
|
||||
Provider: pending.Provider,
|
||||
ProviderSubject: pending.ProviderSubject,
|
||||
ProviderUnionID: pending.ProviderUnionID,
|
||||
CreatedAtMs: user.CreatedAtMs,
|
||||
UpdatedAtMs: user.UpdatedAtMs,
|
||||
}
|
||||
inviteBind := invitedomain.BindCommand{
|
||||
AppCode: registration.AppCode,
|
||||
InvitedUserID: user.UserID,
|
||||
InvitedRegionID: registration.RegionID,
|
||||
InviteCode: registration.InviteCode,
|
||||
Source: "pending_third_party_registration",
|
||||
RequestID: meta.RequestID,
|
||||
BoundAtMs: user.CreatedAtMs,
|
||||
}
|
||||
user, err = s.authRepository.CompletePendingThirdPartyUser(ctx, pendingID, user, displayIdentity, thirdParty, session, inviteBind, maxAccountsPerDevice)
|
||||
if err == nil {
|
||||
lastErr = nil
|
||||
completed = true
|
||||
break
|
||||
}
|
||||
if xerr.IsCode(err, xerr.DisplayUserIDExists) {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if xerr.IsCode(err, xerr.Conflict) {
|
||||
if identity, findErr := s.authRepository.FindThirdPartyIdentity(ctx, pending.Provider, pending.ProviderSubject); findErr == nil {
|
||||
token, _, loginErr := s.loginExistingThirdParty(ctx, identity, registration, meta)
|
||||
if loginErr != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, loginErr
|
||||
}
|
||||
existing, userErr := s.freshUser(ctx, identity.UserID, s.now().UnixMilli(), meta.RequestID)
|
||||
return existing, token, userErr
|
||||
}
|
||||
}
|
||||
s.audit(ctx, meta, 0, loginThird, pending.Provider, resultFailed, string(xerr.CodeOf(err)))
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
if lastErr != nil {
|
||||
s.audit(ctx, meta, 0, loginThird, pending.Provider, resultFailed, string(xerr.DisplayUserIDAllocateFailed))
|
||||
return userdomain.User{}, authdomain.Token{}, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed")
|
||||
}
|
||||
if !completed {
|
||||
s.audit(ctx, meta, 0, loginThird, pending.Provider, resultFailed, string(xerr.DisplayUserIDAllocateFailed))
|
||||
return userdomain.User{}, authdomain.Token{}, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed")
|
||||
}
|
||||
inviteBinding := user.InviteBinding
|
||||
fresh, err := s.freshUser(ctx, user.UserID, s.now().UnixMilli(), meta.RequestID)
|
||||
if err != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
fresh.InviteBinding = inviteBinding
|
||||
user = fresh
|
||||
token, err := s.issueToken(user, session.SessionID, refreshToken)
|
||||
if err != nil {
|
||||
return userdomain.User{}, authdomain.Token{}, err
|
||||
}
|
||||
s.audit(ctx, meta, user.UserID, loginThird, pending.Provider, resultSuccess, "")
|
||||
s.importIMAccountAfterCommit(ctx, user, meta)
|
||||
s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(pending.Provider), registration.Platform, registration.Language, registration.Timezone)
|
||||
s.issueRegistrationRewardAfterCommit(ctx, registration.AppCode, user.UserID, meta)
|
||||
s.issueRegistrationLevelBadgesAfterCommit(ctx, registration.AppCode, user.UserID, meta)
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (s *Service) issueRegistrationRewardAfterCommit(ctx context.Context, appCode string, userID int64, meta Meta) {
|
||||
if s.registrationRewardIssuer == nil {
|
||||
return
|
||||
@ -539,22 +341,9 @@ func validateThirdPartyRegistration(registration authdomain.ThirdPartyRegistrati
|
||||
}
|
||||
|
||||
func validateThirdPartyRegistrationRequiredForCreate(registration authdomain.ThirdPartyRegistration) error {
|
||||
missing := make([]string, 0, 4)
|
||||
if strings.TrimSpace(registration.Username) == "" {
|
||||
missing = append(missing, "username")
|
||||
}
|
||||
if strings.TrimSpace(registration.Avatar) == "" {
|
||||
missing = append(missing, "avatar")
|
||||
}
|
||||
if strings.TrimSpace(registration.Gender) == "" {
|
||||
missing = append(missing, "gender")
|
||||
}
|
||||
if strings.TrimSpace(registration.Country) == "" {
|
||||
missing = append(missing, "country")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
if registration.Username == "" || registration.Country == "" || registration.Gender == "" || registration.Avatar == "" {
|
||||
// 注册即落用户主数据;最小公开资料不完整时拒绝创建,避免后台出现空昵称、空头像或 GLOBAL 国家用户。
|
||||
return xerr.New(xerr.InvalidArgument, strings.Join(missing, ", ")+" are required")
|
||||
return xerr.New(xerr.InvalidArgument, "username, country, gender and avatar are required")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -210,7 +210,7 @@ func (s *Service) issueToken(user userdomain.User, sessionID string, refreshToke
|
||||
"profile_completed": user.ProfileCompleted,
|
||||
"onboarding_status": onboardingStatus,
|
||||
"sid": sessionID,
|
||||
"typ": accessTokenTypeUser,
|
||||
"typ": "access",
|
||||
"iat": now.Unix(),
|
||||
"exp": expiresAt.Unix(),
|
||||
}
|
||||
@ -239,42 +239,6 @@ func (s *Service) issueToken(user userdomain.User, sessionID string, refreshToke
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) issuePendingRegistrationToken(pending authdomain.PendingThirdPartyRegistration) (authdomain.Token, error) {
|
||||
now := s.now()
|
||||
expiresAt := now.Add(time.Duration(s.cfg.AccessTokenTTLSec) * time.Second)
|
||||
pendingID := strings.TrimSpace(pending.PendingID)
|
||||
// pending token 只证明“provider subject 已验证且等待补资料”,不是用户会话;
|
||||
// 不写 user_id,防止 gateway 或下游服务把它当真实用户参与业务。
|
||||
claims := jwt.MapClaims{
|
||||
"iss": s.cfg.Issuer,
|
||||
"app_code": appcode.Normalize(pending.AppCode),
|
||||
"sub": pendingID,
|
||||
"pending_registration_id": pendingID,
|
||||
"profile_completed": false,
|
||||
"onboarding_status": string(userdomain.OnboardingStatusProfileRequired),
|
||||
"sid": pendingID,
|
||||
"typ": accessTokenTypePendingRegistration,
|
||||
"iat": now.Unix(),
|
||||
"exp": expiresAt.Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
accessToken, err := token.SignedString([]byte(s.cfg.SigningSecret))
|
||||
if err != nil {
|
||||
return authdomain.Token{}, err
|
||||
}
|
||||
|
||||
return authdomain.Token{
|
||||
AppCode: appcode.Normalize(pending.AppCode),
|
||||
ProfileCompleted: false,
|
||||
OnboardingStatus: string(userdomain.OnboardingStatusProfileRequired),
|
||||
SessionID: pendingID,
|
||||
AccessToken: accessToken,
|
||||
ExpiresInSec: s.cfg.AccessTokenTTLSec,
|
||||
TokenType: tokenType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tokenOnboardingStatus(user userdomain.User) string {
|
||||
// 旧测试数据或手工种子可能没有显式状态;token 按 profile_completed 推导安全默认值。
|
||||
if user.OnboardingStatus != "" {
|
||||
|
||||
@ -38,7 +38,6 @@ func (r *Repository) FindUserIDByRegisterDeviceID(ctx context.Context, appCode s
|
||||
SELECT user_id
|
||||
FROM users
|
||||
WHERE app_code = ? AND register_device_id = ?
|
||||
ORDER BY created_at_ms ASC, user_id ASC
|
||||
LIMIT 1
|
||||
`, appcode.Normalize(appCode), deviceID).Scan(&userID)
|
||||
if err == sql.ErrNoRows {
|
||||
@ -52,34 +51,6 @@ func (r *Repository) FindUserIDByRegisterDeviceID(ctx context.Context, appCode s
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListDisplayUserIDsByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) ([]string, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT current_display_user_id
|
||||
FROM users
|
||||
WHERE app_code = ? AND register_device_id = ?
|
||||
ORDER BY created_at_ms ASC, user_id ASC
|
||||
`, appcode.Normalize(appCode), deviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
displayUserIDs := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var displayUserID string
|
||||
if err := rows.Scan(&displayUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 这里不去重,账号数量以 users 行为准;文案 helper 会只清洗展示列表。
|
||||
displayUserIDs = append(displayUserIDs, displayUserID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return displayUserIDs, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CountUsersByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
@ -93,63 +64,30 @@ func (r *Repository) CountUsersByRegisterDeviceID(ctx context.Context, appCode s
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpsertPendingThirdPartyRegistration(ctx context.Context, pending authdomain.PendingThirdPartyRegistration) (authdomain.PendingThirdPartyRegistration, error) {
|
||||
// 同一个 provider subject 重复登录只刷新待注册来源快照,不新建 users,也不制造多条 pending 垃圾数据。
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO pending_third_party_registrations (
|
||||
app_code, pending_id, provider, provider_subject, provider_union_id,
|
||||
username, gender, country, invite_code, register_ip, register_user_agent, country_by_ip,
|
||||
device_id, device, os_version, avatar, birth_date, app_version, build_number, source,
|
||||
install_channel, campaign, platform, language, timezone, status, completed_user_id,
|
||||
expires_at_ms, completed_at_ms, created_at_ms, updated_at_ms
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
provider_union_id = IF(status = 'pending', VALUES(provider_union_id), provider_union_id),
|
||||
username = IF(status = 'pending', VALUES(username), username),
|
||||
gender = IF(status = 'pending', VALUES(gender), gender),
|
||||
country = IF(status = 'pending', VALUES(country), country),
|
||||
invite_code = IF(status = 'pending', VALUES(invite_code), invite_code),
|
||||
register_ip = IF(status = 'pending', VALUES(register_ip), register_ip),
|
||||
register_user_agent = IF(status = 'pending', VALUES(register_user_agent), register_user_agent),
|
||||
country_by_ip = IF(status = 'pending', VALUES(country_by_ip), country_by_ip),
|
||||
device_id = IF(status = 'pending', VALUES(device_id), device_id),
|
||||
device = IF(status = 'pending', VALUES(device), device),
|
||||
os_version = IF(status = 'pending', VALUES(os_version), os_version),
|
||||
avatar = IF(status = 'pending', VALUES(avatar), avatar),
|
||||
birth_date = IF(status = 'pending', VALUES(birth_date), birth_date),
|
||||
app_version = IF(status = 'pending', VALUES(app_version), app_version),
|
||||
build_number = IF(status = 'pending', VALUES(build_number), build_number),
|
||||
source = IF(status = 'pending', VALUES(source), source),
|
||||
install_channel = IF(status = 'pending', VALUES(install_channel), install_channel),
|
||||
campaign = IF(status = 'pending', VALUES(campaign), campaign),
|
||||
platform = IF(status = 'pending', VALUES(platform), platform),
|
||||
language = IF(status = 'pending', VALUES(language), language),
|
||||
timezone = IF(status = 'pending', VALUES(timezone), timezone),
|
||||
expires_at_ms = IF(status = 'pending', VALUES(expires_at_ms), expires_at_ms),
|
||||
updated_at_ms = IF(status = 'pending', VALUES(updated_at_ms), updated_at_ms)
|
||||
`, appcode.Normalize(pending.AppCode), pending.PendingID, pending.Provider, pending.ProviderSubject, shared.NullableString(pending.ProviderUnionID),
|
||||
shared.NullableString(pending.Registration.Username), shared.NullableString(pending.Registration.Gender), shared.NullableString(pending.Registration.Country), shared.NullableString(pending.Registration.InviteCode), shared.NullableString(pending.Registration.IP), shared.NullableString(pending.Registration.UserAgent), shared.NullableString(pending.Registration.CountryByIP),
|
||||
shared.NullableString(pending.Registration.DeviceID), shared.NullableString(pending.Registration.Device), shared.NullableString(pending.Registration.OSVersion), shared.NullableString(pending.Registration.Avatar), shared.NullableString(pending.Registration.BirthDate), shared.NullableString(pending.Registration.AppVersion), shared.NullableString(pending.Registration.BuildNumber), shared.NullableString(pending.Registration.Source),
|
||||
shared.NullableString(pending.Registration.InstallChannel), shared.NullableString(pending.Registration.Campaign), shared.NullableString(pending.Registration.Platform), shared.NullableString(pending.Registration.Language), shared.NullableString(pending.Registration.Timezone), authdomain.PendingThirdPartyRegistrationStatusPending,
|
||||
pending.ExpiresAtMs, pending.CreatedAtMs, pending.UpdatedAtMs)
|
||||
func (r *Repository) ListDisplayUserIDsByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) ([]string, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT current_display_user_id
|
||||
FROM users
|
||||
WHERE app_code = ? AND register_device_id = ?
|
||||
ORDER BY user_id ASC
|
||||
`, appcode.Normalize(appCode), deviceID)
|
||||
if err != nil {
|
||||
return authdomain.PendingThirdPartyRegistration{}, mapAuthDuplicateError(err)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.findPendingThirdPartyRegistrationByProvider(ctx, pending.AppCode, pending.Provider, pending.ProviderSubject)
|
||||
}
|
||||
|
||||
func (r *Repository) FindPendingThirdPartyRegistration(ctx context.Context, appCode string, pendingID string) (authdomain.PendingThirdPartyRegistration, error) {
|
||||
return r.scanPendingThirdPartyRegistration(ctx, r.db.QueryRowContext(ctx, pendingThirdPartyRegistrationSelectSQL()+`
|
||||
WHERE app_code = ? AND pending_id = ?
|
||||
`, appcode.Normalize(appCode), pendingID))
|
||||
}
|
||||
|
||||
func (r *Repository) findPendingThirdPartyRegistrationByProvider(ctx context.Context, appCode string, provider string, providerSubject string) (authdomain.PendingThirdPartyRegistration, error) {
|
||||
return r.scanPendingThirdPartyRegistration(ctx, r.db.QueryRowContext(ctx, pendingThirdPartyRegistrationSelectSQL()+`
|
||||
WHERE app_code = ? AND provider = ? AND provider_subject = ?
|
||||
`, appcode.Normalize(appCode), provider, providerSubject))
|
||||
displayUserIDs := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var displayUserID string
|
||||
if err := rows.Scan(&displayUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
displayUserIDs = append(displayUserIDs, displayUserID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return displayUserIDs, nil
|
||||
}
|
||||
|
||||
// CreateThirdPartyUser 原子创建用户、默认短号、三方身份和首个 session。
|
||||
@ -176,10 +114,6 @@ func (r *Repository) CreateThirdPartyUser(ctx context.Context, user userdomain.U
|
||||
if _, err := invitestorage.BindRelation(ctx, tx, inviteBind); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := userstorage.InsertUserRegisteredOutbox(ctx, tx, user); err != nil {
|
||||
// 直接带完整资料注册时,UserRegistered 必须与用户/三方身份同事务提交,统计国家不能事后补猜。
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO third_party_identities (app_code, user_id, provider, provider_subject, provider_union_id, created_at_ms, updated_at_ms)
|
||||
@ -196,135 +130,4 @@ func (r *Repository) CreateThirdPartyUser(ctx context.Context, user userdomain.U
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *Repository) CompletePendingThirdPartyUser(ctx context.Context, pendingID string, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, inviteBind invitedomain.BindCommand, maxAccountsPerDevice int32) (userdomain.User, error) {
|
||||
// pending 消费、真实用户创建、默认短号、三方绑定、session、邀请码和注册事件必须同事务;
|
||||
// 任何一步失败都不能留下半个真实账号。
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
pending, err := r.scanPendingThirdPartyRegistration(ctx, tx.QueryRowContext(ctx, pendingThirdPartyRegistrationSelectSQL()+`
|
||||
WHERE app_code = ? AND pending_id = ?
|
||||
FOR UPDATE
|
||||
`, appcode.Normalize(user.AppCode), pendingID))
|
||||
if err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
if pending.Status != authdomain.PendingThirdPartyRegistrationStatusPending {
|
||||
return userdomain.User{}, xerr.New(xerr.Conflict, "pending registration already completed")
|
||||
}
|
||||
if pending.ExpiresAtMs > 0 && pending.ExpiresAtMs <= user.CreatedAtMs {
|
||||
return userdomain.User{}, xerr.New(xerr.SessionExpired, "pending registration expired")
|
||||
}
|
||||
if pending.Provider != thirdParty.Provider || pending.ProviderSubject != thirdParty.ProviderSubject {
|
||||
return userdomain.User{}, xerr.New(xerr.Conflict, "pending registration identity mismatch")
|
||||
}
|
||||
if err := ensureRegisterDeviceQuotaTx(ctx, tx, user.AppCode, user.RegisterDeviceID, maxAccountsPerDevice); err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
if err := userstorage.InsertUserIdentity(ctx, tx, user, identity); err != nil {
|
||||
return userdomain.User{}, mapAuthDuplicateError(err)
|
||||
}
|
||||
if _, err := invitestorage.EnsurePrimaryCodeForUser(ctx, tx, user.AppCode, user.UserID, user.CreatedAtMs); err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
if binding, err := invitestorage.BindRelation(ctx, tx, inviteBind); err != nil {
|
||||
return userdomain.User{}, err
|
||||
} else {
|
||||
user.InviteBinding = binding
|
||||
if binding.InviteCode != "" {
|
||||
user.InviteCode = binding.InviteCode
|
||||
}
|
||||
}
|
||||
if err := userstorage.InsertUserRegisteredOutbox(ctx, tx, user); err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO third_party_identities (app_code, user_id, provider, provider_subject, provider_union_id, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, appcode.Normalize(thirdParty.AppCode), thirdParty.UserID, thirdParty.Provider, thirdParty.ProviderSubject, shared.NullableString(thirdParty.ProviderUnionID), thirdParty.CreatedAtMs, thirdParty.UpdatedAtMs); err != nil {
|
||||
return userdomain.User{}, mapAuthDuplicateError(err)
|
||||
}
|
||||
if err := insertSession(ctx, tx, session); err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE pending_third_party_registrations
|
||||
SET status = ?, completed_user_id = ?, completed_at_ms = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND pending_id = ?
|
||||
`, authdomain.PendingThirdPartyRegistrationStatusCompleted, user.UserID, user.CreatedAtMs, user.CreatedAtMs, appcode.Normalize(user.AppCode), pendingID); err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func pendingThirdPartyRegistrationSelectSQL() string {
|
||||
return `
|
||||
SELECT app_code, pending_id, provider, provider_subject, COALESCE(provider_union_id, ''),
|
||||
COALESCE(username, ''), COALESCE(gender, ''), COALESCE(country, ''), COALESCE(invite_code, ''),
|
||||
COALESCE(register_ip, ''), COALESCE(register_user_agent, ''), COALESCE(country_by_ip, ''),
|
||||
COALESCE(device_id, ''), COALESCE(device, ''), COALESCE(os_version, ''), COALESCE(avatar, ''),
|
||||
COALESCE(birth_date, ''), COALESCE(app_version, ''), COALESCE(build_number, ''), COALESCE(source, ''),
|
||||
COALESCE(install_channel, ''), COALESCE(campaign, ''), COALESCE(platform, ''), COALESCE(language, ''),
|
||||
COALESCE(timezone, ''), status, completed_user_id, expires_at_ms, completed_at_ms, created_at_ms, updated_at_ms
|
||||
FROM pending_third_party_registrations
|
||||
`
|
||||
}
|
||||
|
||||
type pendingThirdPartyScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func (r *Repository) scanPendingThirdPartyRegistration(_ context.Context, row pendingThirdPartyScanner) (authdomain.PendingThirdPartyRegistration, error) {
|
||||
var pending authdomain.PendingThirdPartyRegistration
|
||||
err := row.Scan(
|
||||
&pending.AppCode,
|
||||
&pending.PendingID,
|
||||
&pending.Provider,
|
||||
&pending.ProviderSubject,
|
||||
&pending.ProviderUnionID,
|
||||
&pending.Registration.Username,
|
||||
&pending.Registration.Gender,
|
||||
&pending.Registration.Country,
|
||||
&pending.Registration.InviteCode,
|
||||
&pending.Registration.IP,
|
||||
&pending.Registration.UserAgent,
|
||||
&pending.Registration.CountryByIP,
|
||||
&pending.Registration.DeviceID,
|
||||
&pending.Registration.Device,
|
||||
&pending.Registration.OSVersion,
|
||||
&pending.Registration.Avatar,
|
||||
&pending.Registration.BirthDate,
|
||||
&pending.Registration.AppVersion,
|
||||
&pending.Registration.BuildNumber,
|
||||
&pending.Registration.Source,
|
||||
&pending.Registration.InstallChannel,
|
||||
&pending.Registration.Campaign,
|
||||
&pending.Registration.Platform,
|
||||
&pending.Registration.Language,
|
||||
&pending.Registration.Timezone,
|
||||
&pending.Status,
|
||||
&pending.CompletedUserID,
|
||||
&pending.ExpiresAtMs,
|
||||
&pending.CompletedAtMs,
|
||||
&pending.CreatedAtMs,
|
||||
&pending.UpdatedAtMs,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return authdomain.PendingThirdPartyRegistration{}, xerr.New(xerr.NotFound, "pending registration not found")
|
||||
}
|
||||
if err != nil {
|
||||
return authdomain.PendingThirdPartyRegistration{}, err
|
||||
}
|
||||
pending.AppCode = appcode.Normalize(pending.AppCode)
|
||||
pending.Registration.AppCode = pending.AppCode
|
||||
return pending, nil
|
||||
}
|
||||
|
||||
// RecordLoginAudit 写登录审计;调用方不会让审计失败影响主链路。
|
||||
|
||||
@ -354,16 +354,6 @@ func (r *Repository) FindThirdPartyIdentity(ctx context.Context, provider string
|
||||
return r.Repository.AuthRepository().FindThirdPartyIdentity(ctx, provider, providerSubject)
|
||||
}
|
||||
|
||||
// UpsertPendingThirdPartyRegistration 让测试 wrapper 继续满足 pending 三方注册接口。
|
||||
func (r *Repository) UpsertPendingThirdPartyRegistration(ctx context.Context, pending authdomain.PendingThirdPartyRegistration) (authdomain.PendingThirdPartyRegistration, error) {
|
||||
return r.Repository.AuthRepository().UpsertPendingThirdPartyRegistration(ctx, pending)
|
||||
}
|
||||
|
||||
// FindPendingThirdPartyRegistration 让测试 wrapper 继续满足 pending 三方注册接口。
|
||||
func (r *Repository) FindPendingThirdPartyRegistration(ctx context.Context, appCode string, pendingID string) (authdomain.PendingThirdPartyRegistration, error) {
|
||||
return r.Repository.AuthRepository().FindPendingThirdPartyRegistration(ctx, appCode, pendingID)
|
||||
}
|
||||
|
||||
// CountUsersByRegisterDeviceID 让测试 wrapper 继续满足认证接口。
|
||||
func (r *Repository) CountUsersByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error) {
|
||||
return r.Repository.AuthRepository().CountUsersByRegisterDeviceID(ctx, appCode, deviceID)
|
||||
@ -379,11 +369,6 @@ func (r *Repository) CreateThirdPartyUser(ctx context.Context, user userdomain.U
|
||||
return r.Repository.AuthRepository().CreateThirdPartyUser(ctx, user, identity, thirdParty, session, invite, maxAccountsPerDevice)
|
||||
}
|
||||
|
||||
// CompletePendingThirdPartyUser 让测试 wrapper 继续满足 pending 三方完成注册接口。
|
||||
func (r *Repository) CompletePendingThirdPartyUser(ctx context.Context, pendingID string, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, invite invitedomain.BindCommand, maxAccountsPerDevice int32) (userdomain.User, error) {
|
||||
return r.Repository.AuthRepository().CompletePendingThirdPartyUser(ctx, pendingID, user, identity, thirdParty, session, invite, maxAccountsPerDevice)
|
||||
}
|
||||
|
||||
// GetRegisterRiskConfig 让测试 wrapper 继续满足认证接口。
|
||||
func (r *Repository) GetRegisterRiskConfig(ctx context.Context, appCode string) (authdomain.RegisterRiskConfig, error) {
|
||||
return r.Repository.AuthRepository().GetRegisterRiskConfig(ctx, appCode)
|
||||
|
||||
@ -139,30 +139,6 @@ func (s *Server) LoginThirdParty(ctx context.Context, req *userv1.LoginThirdPart
|
||||
return &userv1.AuthResponse{Token: toProtoToken(token), IsNewUser: isNewUser, ProfileCompleted: token.ProfileCompleted, OnboardingStatus: token.OnboardingStatus}, nil
|
||||
}
|
||||
|
||||
// CompletePendingThirdPartyRegistration 消费 pending token 对应的三方注册态,并一次性创建真实用户。
|
||||
func (s *Server) CompletePendingThirdPartyRegistration(ctx context.Context, req *userv1.CompletePendingThirdPartyRegistrationRequest) (*userv1.CompleteOnboardingResponse, error) {
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
if s.authSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "auth service is not configured"))
|
||||
}
|
||||
if !s.authSvc.TokenConfigValid() {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "token service is not configured"))
|
||||
}
|
||||
user, token, err := s.authSvc.CompletePendingThirdPartyRegistration(ctx, req.GetPendingRegistrationId(), req.GetUsername(), req.GetAvatar(), req.GetGender(), req.GetCountry(), req.GetInviteCode(), authMeta(req.GetMeta()))
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
|
||||
return &userv1.CompleteOnboardingResponse{
|
||||
User: toProtoUser(user),
|
||||
ProfileCompleted: user.ProfileCompleted,
|
||||
ProfileCompletedAtMs: user.ProfileCompletedAtMs,
|
||||
OnboardingStatus: string(user.OnboardingStatus),
|
||||
Token: toProtoToken(token),
|
||||
Invite: toProtoInviteBinding(user.InviteBinding),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetPassword 为已登录用户首次写入密码身份。
|
||||
func (s *Server) SetPassword(ctx context.Context, req *userv1.SetPasswordRequest) (*userv1.SetPasswordResponse, error) {
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user