diff --git a/gen/iam/v1/auth.pb.go b/gen/iam/v1/auth.pb.go index db9af3c..99347a3 100644 --- a/gen/iam/v1/auth.pb.go +++ b/gen/iam/v1/auth.pb.go @@ -167,9 +167,12 @@ type LoginData struct { // Authenticated user info. User *AuthUser `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` // Whether 2FA is required (return true if 2FA enabled but totp_code not provided). - Requires_2Fa bool `protobuf:"varint,6,opt,name=requires_2fa,json=requires2fa,proto3" json:"requires_2fa,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Requires_2Fa bool `protobuf:"varint,6,opt,name=requires_2fa,json=requires2fa,proto3" json:"requires_2fa,omitempty"` + // Whether email verification is required before full access (email_verified_at IS NULL). + // Frontend should redirect to /verify-email and show banner on dashboard. + RequiresEmailVerification bool `protobuf:"varint,7,opt,name=requires_email_verification,json=requiresEmailVerification,proto3" json:"requires_email_verification,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LoginData) Reset() { @@ -244,6 +247,13 @@ func (x *LoginData) GetRequires_2Fa() bool { return false } +func (x *LoginData) GetRequiresEmailVerification() bool { + if x != nil { + return x.RequiresEmailVerification + } + return false +} + // AuthUser contains basic user info for authentication context. type AuthUser struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -263,8 +273,10 @@ type AuthUser struct { Permissions []string `protobuf:"bytes,7,rep,name=permissions,proto3" json:"permissions,omitempty"` // Whether 2FA is enabled. TwoFactorEnabled bool `protobuf:"varint,8,opt,name=two_factor_enabled,json=twoFactorEnabled,proto3" json:"two_factor_enabled,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Whether the user has verified their email address (true if email_verified_at IS NOT NULL). + EmailVerified bool `protobuf:"varint,9,opt,name=email_verified,json=emailVerified,proto3" json:"email_verified,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AuthUser) Reset() { @@ -353,6 +365,13 @@ func (x *AuthUser) GetTwoFactorEnabled() bool { return false } +func (x *AuthUser) GetEmailVerified() bool { + if x != nil { + return x.EmailVerified + } + return false +} + // LogoutRequest is the request for logout. type LogoutRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1509,6 +1528,301 @@ func (x *GetCurrentUserResponse) GetData() *AuthUser { return nil } +// SendEmailVerificationRequest is empty (uses JWT context to resolve user + email). +type SendEmailVerificationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendEmailVerificationRequest) Reset() { + *x = SendEmailVerificationRequest{} + mi := &file_iam_v1_auth_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendEmailVerificationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendEmailVerificationRequest) ProtoMessage() {} + +func (x *SendEmailVerificationRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_auth_proto_msgTypes[26] + 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 SendEmailVerificationRequest.ProtoReflect.Descriptor instead. +func (*SendEmailVerificationRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_auth_proto_rawDescGZIP(), []int{26} +} + +// SendEmailVerificationResponse confirms the verification code was sent. +type SendEmailVerificationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response metadata. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Human-readable message (e.g., "Verification code sent to j***@example.com"). + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // Code expiry in seconds (e.g., 900 = 15 minutes). + ExpiresIn int32 `protobuf:"varint,3,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendEmailVerificationResponse) Reset() { + *x = SendEmailVerificationResponse{} + mi := &file_iam_v1_auth_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendEmailVerificationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendEmailVerificationResponse) ProtoMessage() {} + +func (x *SendEmailVerificationResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_auth_proto_msgTypes[27] + 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 SendEmailVerificationResponse.ProtoReflect.Descriptor instead. +func (*SendEmailVerificationResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_auth_proto_rawDescGZIP(), []int{27} +} + +func (x *SendEmailVerificationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *SendEmailVerificationResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SendEmailVerificationResponse) GetExpiresIn() int32 { + if x != nil { + return x.ExpiresIn + } + return 0 +} + +// VerifyEmailRequest validates the 6-digit verification code. +type VerifyEmailRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // 6-digit verification code sent to the user's email. + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VerifyEmailRequest) Reset() { + *x = VerifyEmailRequest{} + mi := &file_iam_v1_auth_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VerifyEmailRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyEmailRequest) ProtoMessage() {} + +func (x *VerifyEmailRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_auth_proto_msgTypes[28] + 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 VerifyEmailRequest.ProtoReflect.Descriptor instead. +func (*VerifyEmailRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_auth_proto_rawDescGZIP(), []int{28} +} + +func (x *VerifyEmailRequest) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +// VerifyEmailResponse confirms the email is verified. +type VerifyEmailResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response metadata. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VerifyEmailResponse) Reset() { + *x = VerifyEmailResponse{} + mi := &file_iam_v1_auth_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VerifyEmailResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyEmailResponse) ProtoMessage() {} + +func (x *VerifyEmailResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_auth_proto_msgTypes[29] + 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 VerifyEmailResponse.ProtoReflect.Descriptor instead. +func (*VerifyEmailResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_auth_proto_rawDescGZIP(), []int{29} +} + +func (x *VerifyEmailResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// ResendEmailVerificationRequest re-sends the verification code. +// Rate-limited to 1 per 60 seconds per user. +type ResendEmailVerificationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResendEmailVerificationRequest) Reset() { + *x = ResendEmailVerificationRequest{} + mi := &file_iam_v1_auth_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResendEmailVerificationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResendEmailVerificationRequest) ProtoMessage() {} + +func (x *ResendEmailVerificationRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_auth_proto_msgTypes[30] + 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 ResendEmailVerificationRequest.ProtoReflect.Descriptor instead. +func (*ResendEmailVerificationRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_auth_proto_rawDescGZIP(), []int{30} +} + +// ResendEmailVerificationResponse confirms re-send. +type ResendEmailVerificationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Standard response metadata. + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + // Human-readable message. + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // Code expiry in seconds. + ExpiresIn int32 `protobuf:"varint,3,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResendEmailVerificationResponse) Reset() { + *x = ResendEmailVerificationResponse{} + mi := &file_iam_v1_auth_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResendEmailVerificationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResendEmailVerificationResponse) ProtoMessage() {} + +func (x *ResendEmailVerificationResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_auth_proto_msgTypes[31] + 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 ResendEmailVerificationResponse.ProtoReflect.Descriptor instead. +func (*ResendEmailVerificationResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_auth_proto_rawDescGZIP(), []int{31} +} + +func (x *ResendEmailVerificationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ResendEmailVerificationResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ResendEmailVerificationResponse) GetExpiresIn() int32 { + if x != nil { + return x.ExpiresIn + } + return 0 +} + var File_iam_v1_auth_proto protoreflect.FileDescriptor const file_iam_v1_auth_proto_rawDesc = "" + @@ -1523,7 +1837,7 @@ const file_iam_v1_auth_proto_rawDesc = "" + "deviceInfo\"c\n" + "\rLoginResponse\x12+\n" + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12%\n" + - "\x04data\x18\x02 \x01(\v2\x11.iam.v1.LoginDataR\x04data\"\xda\x01\n" + + "\x04data\x18\x02 \x01(\v2\x11.iam.v1.LoginDataR\x04data\"\x9a\x02\n" + "\tLoginData\x12!\n" + "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12#\n" + "\rrefresh_token\x18\x02 \x01(\tR\frefreshToken\x12\x1d\n" + @@ -1532,7 +1846,8 @@ const file_iam_v1_auth_proto_rawDesc = "" + "\n" + "token_type\x18\x04 \x01(\tR\ttokenType\x12$\n" + "\x04user\x18\x05 \x01(\v2\x10.iam.v1.AuthUserR\x04user\x12!\n" + - "\frequires_2fa\x18\x06 \x01(\bR\vrequires2fa\"\x88\x02\n" + + "\frequires_2fa\x18\x06 \x01(\bR\vrequires2fa\x12>\n" + + "\x1brequires_email_verification\x18\a \x01(\bR\x19requiresEmailVerification\"\xaf\x02\n" + "\bAuthUser\x12\x17\n" + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1a\n" + "\busername\x18\x02 \x01(\tR\busername\x12\x14\n" + @@ -1541,7 +1856,8 @@ const file_iam_v1_auth_proto_rawDesc = "" + "\x13profile_picture_url\x18\x05 \x01(\tR\x11profilePictureUrl\x12\x14\n" + "\x05roles\x18\x06 \x03(\tR\x05roles\x12 \n" + "\vpermissions\x18\a \x03(\tR\vpermissions\x12,\n" + - "\x12two_factor_enabled\x18\b \x01(\bR\x10twoFactorEnabled\"K\n" + + "\x12two_factor_enabled\x18\b \x01(\bR\x10twoFactorEnabled\x12%\n" + + "\x0eemail_verified\x18\t \x01(\bR\remailVerified\"K\n" + "\rLogoutRequest\x12(\n" + "\rrefresh_token\x18\x01 \x01(\tH\x00R\frefreshToken\x88\x01\x01B\x10\n" + "\x0e_refresh_token\"=\n" + @@ -1610,7 +1926,24 @@ const file_iam_v1_auth_proto_rawDesc = "" + "\x15GetCurrentUserRequest\"k\n" + "\x16GetCurrentUserResponse\x12+\n" + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12$\n" + - "\x04data\x18\x02 \x01(\v2\x10.iam.v1.AuthUserR\x04data2\xd2\t\n" + + "\x04data\x18\x02 \x01(\v2\x10.iam.v1.AuthUserR\x04data\"\x1e\n" + + "\x1cSendEmailVerificationRequest\"\x85\x01\n" + + "\x1dSendEmailVerificationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x1d\n" + + "\n" + + "expires_in\x18\x03 \x01(\x05R\texpiresIn\">\n" + + "\x12VerifyEmailRequest\x12(\n" + + "\x04code\x18\x01 \x01(\tB\x14\xbaH\x11r\x0f2\n" + + "^[0-9]{6}$\x98\x01\x06R\x04code\"B\n" + + "\x13VerifyEmailResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\" \n" + + "\x1eResendEmailVerificationRequest\"\x87\x01\n" + + "\x1fResendEmailVerificationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x1d\n" + + "\n" + + "expires_in\x18\x03 \x01(\x05R\texpiresIn2\x84\r\n" + "\vAuthService\x12W\n" + "\x05Login\x12\x14.iam.v1.LoginRequest\x1a\x15.iam.v1.LoginResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/v1/iam/auth/login\x12[\n" + "\x06Logout\x12\x15.iam.v1.LogoutRequest\x1a\x16.iam.v1.LogoutResponse\"\"\x82\xd3\xe4\x93\x02\x1c:\x01*\"\x17/api/v1/iam/auth/logout\x12n\n" + @@ -1623,7 +1956,10 @@ const file_iam_v1_auth_proto_rawDesc = "" + "\tVerify2FA\x12\x18.iam.v1.Verify2FARequest\x1a\x19.iam.v1.Verify2FAResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/v1/iam/auth/2fa/verify\x12l\n" + "\n" + "Disable2FA\x12\x19.iam.v1.Disable2FARequest\x1a\x1a.iam.v1.Disable2FAResponse\"'\x82\xd3\xe4\x93\x02!:\x01*\"\x1c/api/v1/iam/auth/2fa/disable\x12l\n" + - "\x0eGetCurrentUser\x12\x1d.iam.v1.GetCurrentUserRequest\x1a\x1e.iam.v1.GetCurrentUserResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/v1/iam/auth/meB\x87\x01\n" + + "\x0eGetCurrentUser\x12\x1d.iam.v1.GetCurrentUserRequest\x1a\x1e.iam.v1.GetCurrentUserResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/v1/iam/auth/me\x12\x99\x01\n" + + "\x15SendEmailVerification\x12$.iam.v1.SendEmailVerificationRequest\x1a%.iam.v1.SendEmailVerificationResponse\"3\x82\xd3\xe4\x93\x02-:\x01*\"(/api/v1/iam/auth/send-email-verification\x12p\n" + + "\vVerifyEmail\x12\x1a.iam.v1.VerifyEmailRequest\x1a\x1b.iam.v1.VerifyEmailResponse\"(\x82\xd3\xe4\x93\x02\":\x01*\"\x1d/api/v1/iam/auth/verify-email\x12\xa1\x01\n" + + "\x17ResendEmailVerification\x12&.iam.v1.ResendEmailVerificationRequest\x1a'.iam.v1.ResendEmailVerificationResponse\"5\x82\xd3\xe4\x93\x02/:\x01*\"*/api/v1/iam/auth/resend-email-verificationB\x87\x01\n" + "\n" + "com.iam.v1B\tAuthProtoP\x01Z5github.com/mutugading/goapps-backend/gen/iam/v1;iamv1\xa2\x02\x03IXX\xaa\x02\x06Iam.V1\xca\x02\x06Iam\\V1\xe2\x02\x12Iam\\V1\\GPBMetadata\xea\x02\aIam::V1b\x06proto3" @@ -1639,80 +1975,95 @@ func file_iam_v1_auth_proto_rawDescGZIP() []byte { return file_iam_v1_auth_proto_rawDescData } -var file_iam_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_iam_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_iam_v1_auth_proto_goTypes = []any{ - (*LoginRequest)(nil), // 0: iam.v1.LoginRequest - (*LoginResponse)(nil), // 1: iam.v1.LoginResponse - (*LoginData)(nil), // 2: iam.v1.LoginData - (*AuthUser)(nil), // 3: iam.v1.AuthUser - (*LogoutRequest)(nil), // 4: iam.v1.LogoutRequest - (*LogoutResponse)(nil), // 5: iam.v1.LogoutResponse - (*RefreshTokenRequest)(nil), // 6: iam.v1.RefreshTokenRequest - (*RefreshTokenResponse)(nil), // 7: iam.v1.RefreshTokenResponse - (*TokenPair)(nil), // 8: iam.v1.TokenPair - (*ForgotPasswordRequest)(nil), // 9: iam.v1.ForgotPasswordRequest - (*ForgotPasswordResponse)(nil), // 10: iam.v1.ForgotPasswordResponse - (*VerifyResetOTPRequest)(nil), // 11: iam.v1.VerifyResetOTPRequest - (*VerifyResetOTPResponse)(nil), // 12: iam.v1.VerifyResetOTPResponse - (*ResetPasswordRequest)(nil), // 13: iam.v1.ResetPasswordRequest - (*ResetPasswordResponse)(nil), // 14: iam.v1.ResetPasswordResponse - (*UpdatePasswordRequest)(nil), // 15: iam.v1.UpdatePasswordRequest - (*UpdatePasswordResponse)(nil), // 16: iam.v1.UpdatePasswordResponse - (*Enable2FARequest)(nil), // 17: iam.v1.Enable2FARequest - (*Enable2FAResponse)(nil), // 18: iam.v1.Enable2FAResponse - (*TwoFactorSetup)(nil), // 19: iam.v1.TwoFactorSetup - (*Verify2FARequest)(nil), // 20: iam.v1.Verify2FARequest - (*Verify2FAResponse)(nil), // 21: iam.v1.Verify2FAResponse - (*Disable2FARequest)(nil), // 22: iam.v1.Disable2FARequest - (*Disable2FAResponse)(nil), // 23: iam.v1.Disable2FAResponse - (*GetCurrentUserRequest)(nil), // 24: iam.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 25: iam.v1.GetCurrentUserResponse - (*v1.BaseResponse)(nil), // 26: common.v1.BaseResponse + (*LoginRequest)(nil), // 0: iam.v1.LoginRequest + (*LoginResponse)(nil), // 1: iam.v1.LoginResponse + (*LoginData)(nil), // 2: iam.v1.LoginData + (*AuthUser)(nil), // 3: iam.v1.AuthUser + (*LogoutRequest)(nil), // 4: iam.v1.LogoutRequest + (*LogoutResponse)(nil), // 5: iam.v1.LogoutResponse + (*RefreshTokenRequest)(nil), // 6: iam.v1.RefreshTokenRequest + (*RefreshTokenResponse)(nil), // 7: iam.v1.RefreshTokenResponse + (*TokenPair)(nil), // 8: iam.v1.TokenPair + (*ForgotPasswordRequest)(nil), // 9: iam.v1.ForgotPasswordRequest + (*ForgotPasswordResponse)(nil), // 10: iam.v1.ForgotPasswordResponse + (*VerifyResetOTPRequest)(nil), // 11: iam.v1.VerifyResetOTPRequest + (*VerifyResetOTPResponse)(nil), // 12: iam.v1.VerifyResetOTPResponse + (*ResetPasswordRequest)(nil), // 13: iam.v1.ResetPasswordRequest + (*ResetPasswordResponse)(nil), // 14: iam.v1.ResetPasswordResponse + (*UpdatePasswordRequest)(nil), // 15: iam.v1.UpdatePasswordRequest + (*UpdatePasswordResponse)(nil), // 16: iam.v1.UpdatePasswordResponse + (*Enable2FARequest)(nil), // 17: iam.v1.Enable2FARequest + (*Enable2FAResponse)(nil), // 18: iam.v1.Enable2FAResponse + (*TwoFactorSetup)(nil), // 19: iam.v1.TwoFactorSetup + (*Verify2FARequest)(nil), // 20: iam.v1.Verify2FARequest + (*Verify2FAResponse)(nil), // 21: iam.v1.Verify2FAResponse + (*Disable2FARequest)(nil), // 22: iam.v1.Disable2FARequest + (*Disable2FAResponse)(nil), // 23: iam.v1.Disable2FAResponse + (*GetCurrentUserRequest)(nil), // 24: iam.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 25: iam.v1.GetCurrentUserResponse + (*SendEmailVerificationRequest)(nil), // 26: iam.v1.SendEmailVerificationRequest + (*SendEmailVerificationResponse)(nil), // 27: iam.v1.SendEmailVerificationResponse + (*VerifyEmailRequest)(nil), // 28: iam.v1.VerifyEmailRequest + (*VerifyEmailResponse)(nil), // 29: iam.v1.VerifyEmailResponse + (*ResendEmailVerificationRequest)(nil), // 30: iam.v1.ResendEmailVerificationRequest + (*ResendEmailVerificationResponse)(nil), // 31: iam.v1.ResendEmailVerificationResponse + (*v1.BaseResponse)(nil), // 32: common.v1.BaseResponse } var file_iam_v1_auth_proto_depIdxs = []int32{ - 26, // 0: iam.v1.LoginResponse.base:type_name -> common.v1.BaseResponse + 32, // 0: iam.v1.LoginResponse.base:type_name -> common.v1.BaseResponse 2, // 1: iam.v1.LoginResponse.data:type_name -> iam.v1.LoginData 3, // 2: iam.v1.LoginData.user:type_name -> iam.v1.AuthUser - 26, // 3: iam.v1.LogoutResponse.base:type_name -> common.v1.BaseResponse - 26, // 4: iam.v1.RefreshTokenResponse.base:type_name -> common.v1.BaseResponse + 32, // 3: iam.v1.LogoutResponse.base:type_name -> common.v1.BaseResponse + 32, // 4: iam.v1.RefreshTokenResponse.base:type_name -> common.v1.BaseResponse 8, // 5: iam.v1.RefreshTokenResponse.data:type_name -> iam.v1.TokenPair - 26, // 6: iam.v1.ForgotPasswordResponse.base:type_name -> common.v1.BaseResponse - 26, // 7: iam.v1.VerifyResetOTPResponse.base:type_name -> common.v1.BaseResponse - 26, // 8: iam.v1.ResetPasswordResponse.base:type_name -> common.v1.BaseResponse - 26, // 9: iam.v1.UpdatePasswordResponse.base:type_name -> common.v1.BaseResponse - 26, // 10: iam.v1.Enable2FAResponse.base:type_name -> common.v1.BaseResponse + 32, // 6: iam.v1.ForgotPasswordResponse.base:type_name -> common.v1.BaseResponse + 32, // 7: iam.v1.VerifyResetOTPResponse.base:type_name -> common.v1.BaseResponse + 32, // 8: iam.v1.ResetPasswordResponse.base:type_name -> common.v1.BaseResponse + 32, // 9: iam.v1.UpdatePasswordResponse.base:type_name -> common.v1.BaseResponse + 32, // 10: iam.v1.Enable2FAResponse.base:type_name -> common.v1.BaseResponse 19, // 11: iam.v1.Enable2FAResponse.data:type_name -> iam.v1.TwoFactorSetup - 26, // 12: iam.v1.Verify2FAResponse.base:type_name -> common.v1.BaseResponse - 26, // 13: iam.v1.Disable2FAResponse.base:type_name -> common.v1.BaseResponse - 26, // 14: iam.v1.GetCurrentUserResponse.base:type_name -> common.v1.BaseResponse + 32, // 12: iam.v1.Verify2FAResponse.base:type_name -> common.v1.BaseResponse + 32, // 13: iam.v1.Disable2FAResponse.base:type_name -> common.v1.BaseResponse + 32, // 14: iam.v1.GetCurrentUserResponse.base:type_name -> common.v1.BaseResponse 3, // 15: iam.v1.GetCurrentUserResponse.data:type_name -> iam.v1.AuthUser - 0, // 16: iam.v1.AuthService.Login:input_type -> iam.v1.LoginRequest - 4, // 17: iam.v1.AuthService.Logout:input_type -> iam.v1.LogoutRequest - 6, // 18: iam.v1.AuthService.RefreshToken:input_type -> iam.v1.RefreshTokenRequest - 9, // 19: iam.v1.AuthService.ForgotPassword:input_type -> iam.v1.ForgotPasswordRequest - 11, // 20: iam.v1.AuthService.VerifyResetOTP:input_type -> iam.v1.VerifyResetOTPRequest - 13, // 21: iam.v1.AuthService.ResetPassword:input_type -> iam.v1.ResetPasswordRequest - 15, // 22: iam.v1.AuthService.UpdatePassword:input_type -> iam.v1.UpdatePasswordRequest - 17, // 23: iam.v1.AuthService.Enable2FA:input_type -> iam.v1.Enable2FARequest - 20, // 24: iam.v1.AuthService.Verify2FA:input_type -> iam.v1.Verify2FARequest - 22, // 25: iam.v1.AuthService.Disable2FA:input_type -> iam.v1.Disable2FARequest - 24, // 26: iam.v1.AuthService.GetCurrentUser:input_type -> iam.v1.GetCurrentUserRequest - 1, // 27: iam.v1.AuthService.Login:output_type -> iam.v1.LoginResponse - 5, // 28: iam.v1.AuthService.Logout:output_type -> iam.v1.LogoutResponse - 7, // 29: iam.v1.AuthService.RefreshToken:output_type -> iam.v1.RefreshTokenResponse - 10, // 30: iam.v1.AuthService.ForgotPassword:output_type -> iam.v1.ForgotPasswordResponse - 12, // 31: iam.v1.AuthService.VerifyResetOTP:output_type -> iam.v1.VerifyResetOTPResponse - 14, // 32: iam.v1.AuthService.ResetPassword:output_type -> iam.v1.ResetPasswordResponse - 16, // 33: iam.v1.AuthService.UpdatePassword:output_type -> iam.v1.UpdatePasswordResponse - 18, // 34: iam.v1.AuthService.Enable2FA:output_type -> iam.v1.Enable2FAResponse - 21, // 35: iam.v1.AuthService.Verify2FA:output_type -> iam.v1.Verify2FAResponse - 23, // 36: iam.v1.AuthService.Disable2FA:output_type -> iam.v1.Disable2FAResponse - 25, // 37: iam.v1.AuthService.GetCurrentUser:output_type -> iam.v1.GetCurrentUserResponse - 27, // [27:38] is the sub-list for method output_type - 16, // [16:27] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 32, // 16: iam.v1.SendEmailVerificationResponse.base:type_name -> common.v1.BaseResponse + 32, // 17: iam.v1.VerifyEmailResponse.base:type_name -> common.v1.BaseResponse + 32, // 18: iam.v1.ResendEmailVerificationResponse.base:type_name -> common.v1.BaseResponse + 0, // 19: iam.v1.AuthService.Login:input_type -> iam.v1.LoginRequest + 4, // 20: iam.v1.AuthService.Logout:input_type -> iam.v1.LogoutRequest + 6, // 21: iam.v1.AuthService.RefreshToken:input_type -> iam.v1.RefreshTokenRequest + 9, // 22: iam.v1.AuthService.ForgotPassword:input_type -> iam.v1.ForgotPasswordRequest + 11, // 23: iam.v1.AuthService.VerifyResetOTP:input_type -> iam.v1.VerifyResetOTPRequest + 13, // 24: iam.v1.AuthService.ResetPassword:input_type -> iam.v1.ResetPasswordRequest + 15, // 25: iam.v1.AuthService.UpdatePassword:input_type -> iam.v1.UpdatePasswordRequest + 17, // 26: iam.v1.AuthService.Enable2FA:input_type -> iam.v1.Enable2FARequest + 20, // 27: iam.v1.AuthService.Verify2FA:input_type -> iam.v1.Verify2FARequest + 22, // 28: iam.v1.AuthService.Disable2FA:input_type -> iam.v1.Disable2FARequest + 24, // 29: iam.v1.AuthService.GetCurrentUser:input_type -> iam.v1.GetCurrentUserRequest + 26, // 30: iam.v1.AuthService.SendEmailVerification:input_type -> iam.v1.SendEmailVerificationRequest + 28, // 31: iam.v1.AuthService.VerifyEmail:input_type -> iam.v1.VerifyEmailRequest + 30, // 32: iam.v1.AuthService.ResendEmailVerification:input_type -> iam.v1.ResendEmailVerificationRequest + 1, // 33: iam.v1.AuthService.Login:output_type -> iam.v1.LoginResponse + 5, // 34: iam.v1.AuthService.Logout:output_type -> iam.v1.LogoutResponse + 7, // 35: iam.v1.AuthService.RefreshToken:output_type -> iam.v1.RefreshTokenResponse + 10, // 36: iam.v1.AuthService.ForgotPassword:output_type -> iam.v1.ForgotPasswordResponse + 12, // 37: iam.v1.AuthService.VerifyResetOTP:output_type -> iam.v1.VerifyResetOTPResponse + 14, // 38: iam.v1.AuthService.ResetPassword:output_type -> iam.v1.ResetPasswordResponse + 16, // 39: iam.v1.AuthService.UpdatePassword:output_type -> iam.v1.UpdatePasswordResponse + 18, // 40: iam.v1.AuthService.Enable2FA:output_type -> iam.v1.Enable2FAResponse + 21, // 41: iam.v1.AuthService.Verify2FA:output_type -> iam.v1.Verify2FAResponse + 23, // 42: iam.v1.AuthService.Disable2FA:output_type -> iam.v1.Disable2FAResponse + 25, // 43: iam.v1.AuthService.GetCurrentUser:output_type -> iam.v1.GetCurrentUserResponse + 27, // 44: iam.v1.AuthService.SendEmailVerification:output_type -> iam.v1.SendEmailVerificationResponse + 29, // 45: iam.v1.AuthService.VerifyEmail:output_type -> iam.v1.VerifyEmailResponse + 31, // 46: iam.v1.AuthService.ResendEmailVerification:output_type -> iam.v1.ResendEmailVerificationResponse + 33, // [33:47] is the sub-list for method output_type + 19, // [19:33] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_iam_v1_auth_proto_init() } @@ -1727,7 +2078,7 @@ func file_iam_v1_auth_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_iam_v1_auth_proto_rawDesc), len(file_iam_v1_auth_proto_rawDesc)), NumEnums: 0, - NumMessages: 26, + NumMessages: 32, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/iam/v1/auth.pb.gw.go b/gen/iam/v1/auth.pb.gw.go index c4809ee..8cbb230 100644 --- a/gen/iam/v1/auth.pb.gw.go +++ b/gen/iam/v1/auth.pb.gw.go @@ -326,6 +326,87 @@ func local_request_AuthService_GetCurrentUser_0(ctx context.Context, marshaler r return msg, metadata, err } +func request_AuthService_SendEmailVerification_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SendEmailVerificationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.SendEmailVerification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_SendEmailVerification_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SendEmailVerificationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.SendEmailVerification(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_VerifyEmail_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq VerifyEmailRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.VerifyEmail(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_VerifyEmail_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq VerifyEmailRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.VerifyEmail(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_ResendEmailVerification_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ResendEmailVerificationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ResendEmailVerification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_ResendEmailVerification_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ResendEmailVerificationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ResendEmailVerification(ctx, &protoReq) + return msg, metadata, err +} + // RegisterAuthServiceHandlerServer registers the http handlers for service AuthService to "mux". // UnaryRPC :call AuthServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -552,6 +633,66 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux } forward_AuthService_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_AuthService_SendEmailVerification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.AuthService/SendEmailVerification", runtime.WithHTTPPathPattern("/api/v1/iam/auth/send-email-verification")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_SendEmailVerification_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_SendEmailVerification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_VerifyEmail_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.AuthService/VerifyEmail", runtime.WithHTTPPathPattern("/api/v1/iam/auth/verify-email")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_VerifyEmail_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_VerifyEmail_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_ResendEmailVerification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.AuthService/ResendEmailVerification", runtime.WithHTTPPathPattern("/api/v1/iam/auth/resend-email-verification")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_ResendEmailVerification_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ResendEmailVerification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -779,33 +920,90 @@ func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux } forward_AuthService_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_AuthService_SendEmailVerification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.AuthService/SendEmailVerification", runtime.WithHTTPPathPattern("/api/v1/iam/auth/send-email-verification")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_SendEmailVerification_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_SendEmailVerification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_VerifyEmail_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.AuthService/VerifyEmail", runtime.WithHTTPPathPattern("/api/v1/iam/auth/verify-email")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_VerifyEmail_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_VerifyEmail_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_ResendEmailVerification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.AuthService/ResendEmailVerification", runtime.WithHTTPPathPattern("/api/v1/iam/auth/resend-email-verification")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_ResendEmailVerification_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ResendEmailVerification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } var ( - pattern_AuthService_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "login"}, "")) - pattern_AuthService_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "logout"}, "")) - pattern_AuthService_RefreshToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "refresh"}, "")) - pattern_AuthService_ForgotPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "forgot-password"}, "")) - pattern_AuthService_VerifyResetOTP_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "verify-otp"}, "")) - pattern_AuthService_ResetPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "reset-password"}, "")) - pattern_AuthService_UpdatePassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "update-password"}, "")) - pattern_AuthService_Enable2FA_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "auth", "2fa", "enable"}, "")) - pattern_AuthService_Verify2FA_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "auth", "2fa", "verify"}, "")) - pattern_AuthService_Disable2FA_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "auth", "2fa", "disable"}, "")) - pattern_AuthService_GetCurrentUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "me"}, "")) + pattern_AuthService_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "login"}, "")) + pattern_AuthService_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "logout"}, "")) + pattern_AuthService_RefreshToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "refresh"}, "")) + pattern_AuthService_ForgotPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "forgot-password"}, "")) + pattern_AuthService_VerifyResetOTP_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "verify-otp"}, "")) + pattern_AuthService_ResetPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "reset-password"}, "")) + pattern_AuthService_UpdatePassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "update-password"}, "")) + pattern_AuthService_Enable2FA_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "auth", "2fa", "enable"}, "")) + pattern_AuthService_Verify2FA_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "auth", "2fa", "verify"}, "")) + pattern_AuthService_Disable2FA_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "auth", "2fa", "disable"}, "")) + pattern_AuthService_GetCurrentUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "me"}, "")) + pattern_AuthService_SendEmailVerification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "send-email-verification"}, "")) + pattern_AuthService_VerifyEmail_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "verify-email"}, "")) + pattern_AuthService_ResendEmailVerification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "auth", "resend-email-verification"}, "")) ) var ( - forward_AuthService_Login_0 = runtime.ForwardResponseMessage - forward_AuthService_Logout_0 = runtime.ForwardResponseMessage - forward_AuthService_RefreshToken_0 = runtime.ForwardResponseMessage - forward_AuthService_ForgotPassword_0 = runtime.ForwardResponseMessage - forward_AuthService_VerifyResetOTP_0 = runtime.ForwardResponseMessage - forward_AuthService_ResetPassword_0 = runtime.ForwardResponseMessage - forward_AuthService_UpdatePassword_0 = runtime.ForwardResponseMessage - forward_AuthService_Enable2FA_0 = runtime.ForwardResponseMessage - forward_AuthService_Verify2FA_0 = runtime.ForwardResponseMessage - forward_AuthService_Disable2FA_0 = runtime.ForwardResponseMessage - forward_AuthService_GetCurrentUser_0 = runtime.ForwardResponseMessage + forward_AuthService_Login_0 = runtime.ForwardResponseMessage + forward_AuthService_Logout_0 = runtime.ForwardResponseMessage + forward_AuthService_RefreshToken_0 = runtime.ForwardResponseMessage + forward_AuthService_ForgotPassword_0 = runtime.ForwardResponseMessage + forward_AuthService_VerifyResetOTP_0 = runtime.ForwardResponseMessage + forward_AuthService_ResetPassword_0 = runtime.ForwardResponseMessage + forward_AuthService_UpdatePassword_0 = runtime.ForwardResponseMessage + forward_AuthService_Enable2FA_0 = runtime.ForwardResponseMessage + forward_AuthService_Verify2FA_0 = runtime.ForwardResponseMessage + forward_AuthService_Disable2FA_0 = runtime.ForwardResponseMessage + forward_AuthService_GetCurrentUser_0 = runtime.ForwardResponseMessage + forward_AuthService_SendEmailVerification_0 = runtime.ForwardResponseMessage + forward_AuthService_VerifyEmail_0 = runtime.ForwardResponseMessage + forward_AuthService_ResendEmailVerification_0 = runtime.ForwardResponseMessage ) diff --git a/gen/iam/v1/auth_grpc.pb.go b/gen/iam/v1/auth_grpc.pb.go index 695a5b7..a9c9e12 100644 --- a/gen/iam/v1/auth_grpc.pb.go +++ b/gen/iam/v1/auth_grpc.pb.go @@ -19,17 +19,20 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AuthService_Login_FullMethodName = "/iam.v1.AuthService/Login" - AuthService_Logout_FullMethodName = "/iam.v1.AuthService/Logout" - AuthService_RefreshToken_FullMethodName = "/iam.v1.AuthService/RefreshToken" - AuthService_ForgotPassword_FullMethodName = "/iam.v1.AuthService/ForgotPassword" - AuthService_VerifyResetOTP_FullMethodName = "/iam.v1.AuthService/VerifyResetOTP" - AuthService_ResetPassword_FullMethodName = "/iam.v1.AuthService/ResetPassword" - AuthService_UpdatePassword_FullMethodName = "/iam.v1.AuthService/UpdatePassword" - AuthService_Enable2FA_FullMethodName = "/iam.v1.AuthService/Enable2FA" - AuthService_Verify2FA_FullMethodName = "/iam.v1.AuthService/Verify2FA" - AuthService_Disable2FA_FullMethodName = "/iam.v1.AuthService/Disable2FA" - AuthService_GetCurrentUser_FullMethodName = "/iam.v1.AuthService/GetCurrentUser" + AuthService_Login_FullMethodName = "/iam.v1.AuthService/Login" + AuthService_Logout_FullMethodName = "/iam.v1.AuthService/Logout" + AuthService_RefreshToken_FullMethodName = "/iam.v1.AuthService/RefreshToken" + AuthService_ForgotPassword_FullMethodName = "/iam.v1.AuthService/ForgotPassword" + AuthService_VerifyResetOTP_FullMethodName = "/iam.v1.AuthService/VerifyResetOTP" + AuthService_ResetPassword_FullMethodName = "/iam.v1.AuthService/ResetPassword" + AuthService_UpdatePassword_FullMethodName = "/iam.v1.AuthService/UpdatePassword" + AuthService_Enable2FA_FullMethodName = "/iam.v1.AuthService/Enable2FA" + AuthService_Verify2FA_FullMethodName = "/iam.v1.AuthService/Verify2FA" + AuthService_Disable2FA_FullMethodName = "/iam.v1.AuthService/Disable2FA" + AuthService_GetCurrentUser_FullMethodName = "/iam.v1.AuthService/GetCurrentUser" + AuthService_SendEmailVerification_FullMethodName = "/iam.v1.AuthService/SendEmailVerification" + AuthService_VerifyEmail_FullMethodName = "/iam.v1.AuthService/VerifyEmail" + AuthService_ResendEmailVerification_FullMethodName = "/iam.v1.AuthService/ResendEmailVerification" ) // AuthServiceClient is the client API for AuthService service. @@ -63,6 +66,13 @@ type AuthServiceClient interface { Disable2FA(ctx context.Context, in *Disable2FARequest, opts ...grpc.CallOption) (*Disable2FAResponse, error) // GetCurrentUser returns the authenticated user's info. GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) + // SendEmailVerification sends a 6-digit verification code to the authenticated user's email. + // Generates a new token, invalidating any previous unconsumed tokens. + SendEmailVerification(ctx context.Context, in *SendEmailVerificationRequest, opts ...grpc.CallOption) (*SendEmailVerificationResponse, error) + // VerifyEmail consumes a verification code and marks the authenticated user's email as verified. + VerifyEmail(ctx context.Context, in *VerifyEmailRequest, opts ...grpc.CallOption) (*VerifyEmailResponse, error) + // ResendEmailVerification re-sends the verification code. Rate-limited to 1 per minute per user. + ResendEmailVerification(ctx context.Context, in *ResendEmailVerificationRequest, opts ...grpc.CallOption) (*ResendEmailVerificationResponse, error) } type authServiceClient struct { @@ -183,6 +193,36 @@ func (c *authServiceClient) GetCurrentUser(ctx context.Context, in *GetCurrentUs return out, nil } +func (c *authServiceClient) SendEmailVerification(ctx context.Context, in *SendEmailVerificationRequest, opts ...grpc.CallOption) (*SendEmailVerificationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SendEmailVerificationResponse) + err := c.cc.Invoke(ctx, AuthService_SendEmailVerification_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) VerifyEmail(ctx context.Context, in *VerifyEmailRequest, opts ...grpc.CallOption) (*VerifyEmailResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VerifyEmailResponse) + err := c.cc.Invoke(ctx, AuthService_VerifyEmail_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) ResendEmailVerification(ctx context.Context, in *ResendEmailVerificationRequest, opts ...grpc.CallOption) (*ResendEmailVerificationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResendEmailVerificationResponse) + err := c.cc.Invoke(ctx, AuthService_ResendEmailVerification_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. @@ -214,6 +254,13 @@ type AuthServiceServer interface { Disable2FA(context.Context, *Disable2FARequest) (*Disable2FAResponse, error) // GetCurrentUser returns the authenticated user's info. GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) + // SendEmailVerification sends a 6-digit verification code to the authenticated user's email. + // Generates a new token, invalidating any previous unconsumed tokens. + SendEmailVerification(context.Context, *SendEmailVerificationRequest) (*SendEmailVerificationResponse, error) + // VerifyEmail consumes a verification code and marks the authenticated user's email as verified. + VerifyEmail(context.Context, *VerifyEmailRequest) (*VerifyEmailResponse, error) + // ResendEmailVerification re-sends the verification code. Rate-limited to 1 per minute per user. + ResendEmailVerification(context.Context, *ResendEmailVerificationRequest) (*ResendEmailVerificationResponse, error) mustEmbedUnimplementedAuthServiceServer() } @@ -257,6 +304,15 @@ func (UnimplementedAuthServiceServer) Disable2FA(context.Context, *Disable2FAReq func (UnimplementedAuthServiceServer) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetCurrentUser not implemented") } +func (UnimplementedAuthServiceServer) SendEmailVerification(context.Context, *SendEmailVerificationRequest) (*SendEmailVerificationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SendEmailVerification not implemented") +} +func (UnimplementedAuthServiceServer) VerifyEmail(context.Context, *VerifyEmailRequest) (*VerifyEmailResponse, error) { + return nil, status.Error(codes.Unimplemented, "method VerifyEmail not implemented") +} +func (UnimplementedAuthServiceServer) ResendEmailVerification(context.Context, *ResendEmailVerificationRequest) (*ResendEmailVerificationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ResendEmailVerification not implemented") +} func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} @@ -476,6 +532,60 @@ func _AuthService_GetCurrentUser_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _AuthService_SendEmailVerification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendEmailVerificationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).SendEmailVerification(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_SendEmailVerification_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).SendEmailVerification(ctx, req.(*SendEmailVerificationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_VerifyEmail_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VerifyEmailRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).VerifyEmail(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_VerifyEmail_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).VerifyEmail(ctx, req.(*VerifyEmailRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_ResendEmailVerification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResendEmailVerificationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).ResendEmailVerification(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_ResendEmailVerification_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).ResendEmailVerification(ctx, req.(*ResendEmailVerificationRequest)) + } + 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) @@ -527,6 +637,18 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetCurrentUser", Handler: _AuthService_GetCurrentUser_Handler, }, + { + MethodName: "SendEmailVerification", + Handler: _AuthService_SendEmailVerification_Handler, + }, + { + MethodName: "VerifyEmail", + Handler: _AuthService_VerifyEmail_Handler, + }, + { + MethodName: "ResendEmailVerification", + Handler: _AuthService_ResendEmailVerification_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "iam/v1/auth.proto", diff --git a/gen/openapi/iam/v1/auth.swagger.json b/gen/openapi/iam/v1/auth.swagger.json index d56f381..1e4c34b 100644 --- a/gen/openapi/iam/v1/auth.swagger.json +++ b/gen/openapi/iam/v1/auth.swagger.json @@ -277,6 +277,40 @@ ] } }, + "/api/v1/iam/auth/resend-email-verification": { + "post": { + "summary": "ResendEmailVerification re-sends the verification code. Rate-limited to 1 per minute per user.", + "operationId": "AuthService_ResendEmailVerification", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ResendEmailVerificationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "ResendEmailVerificationRequest re-sends the verification code.\nRate-limited to 1 per 60 seconds per user.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1ResendEmailVerificationRequest" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, "/api/v1/iam/auth/reset-password": { "post": { "summary": "ResetPassword sets a new password using a valid reset token.", @@ -311,6 +345,40 @@ ] } }, + "/api/v1/iam/auth/send-email-verification": { + "post": { + "summary": "SendEmailVerification sends a 6-digit verification code to the authenticated user's email.\nGenerates a new token, invalidating any previous unconsumed tokens.", + "operationId": "AuthService_SendEmailVerification", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1SendEmailVerificationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "SendEmailVerificationRequest is empty (uses JWT context to resolve user + email).", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1SendEmailVerificationRequest" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, "/api/v1/iam/auth/update-password": { "post": { "summary": "UpdatePassword changes password for authenticated user.", @@ -345,6 +413,40 @@ ] } }, + "/api/v1/iam/auth/verify-email": { + "post": { + "summary": "VerifyEmail consumes a verification code and marks the authenticated user's email as verified.", + "operationId": "AuthService_VerifyEmail", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1VerifyEmailResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "VerifyEmailRequest validates the 6-digit verification code.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1VerifyEmailRequest" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, "/api/v1/iam/auth/verify-otp": { "post": { "summary": "VerifyResetOTP validates the OTP code and returns a reset token.", @@ -449,6 +551,10 @@ "twoFactorEnabled": { "type": "boolean", "description": "Whether 2FA is enabled." + }, + "emailVerified": { + "type": "boolean", + "description": "Whether the user has verified their email address (true if email_verified_at IS NOT NULL)." } }, "description": "AuthUser contains basic user info for authentication context." @@ -597,6 +703,10 @@ "requires2fa": { "type": "boolean", "description": "Whether 2FA is required (return true if 2FA enabled but totp_code not provided)." + }, + "requiresEmailVerification": { + "type": "boolean", + "description": "Whether email verification is required before full access (email_verified_at IS NULL).\nFrontend should redirect to /verify-email and show banner on dashboard." } }, "description": "LoginData contains the login result." @@ -681,6 +791,29 @@ }, "description": "RefreshTokenResponse is the response with new tokens." }, + "v1ResendEmailVerificationRequest": { + "type": "object", + "description": "ResendEmailVerificationRequest re-sends the verification code.\nRate-limited to 1 per 60 seconds per user." + }, + "v1ResendEmailVerificationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "message": { + "type": "string", + "description": "Human-readable message." + }, + "expiresIn": { + "type": "integer", + "format": "int32", + "description": "Code expiry in seconds." + } + }, + "description": "ResendEmailVerificationResponse confirms re-send." + }, "v1ResetPasswordRequest": { "type": "object", "properties": { @@ -709,6 +842,29 @@ }, "description": "ResetPasswordResponse confirms password reset." }, + "v1SendEmailVerificationRequest": { + "type": "object", + "description": "SendEmailVerificationRequest is empty (uses JWT context to resolve user + email)." + }, + "v1SendEmailVerificationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + }, + "message": { + "type": "string", + "description": "Human-readable message (e.g., \"Verification code sent to j***@example.com\")." + }, + "expiresIn": { + "type": "integer", + "format": "int32", + "description": "Code expiry in seconds (e.g., 900 = 15 minutes)." + } + }, + "description": "SendEmailVerificationResponse confirms the verification code was sent." + }, "v1TokenPair": { "type": "object", "properties": { @@ -815,6 +971,26 @@ }, "description": "Verify2FAResponse confirms 2FA is enabled." }, + "v1VerifyEmailRequest": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "6-digit verification code sent to the user's email." + } + }, + "description": "VerifyEmailRequest validates the 6-digit verification code." + }, + "v1VerifyEmailResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse", + "description": "Standard response metadata." + } + }, + "description": "VerifyEmailResponse confirms the email is verified." + }, "v1VerifyResetOTPRequest": { "type": "object", "properties": { diff --git a/services/iam/internal/application/auth/service.go b/services/iam/internal/application/auth/service.go index 1c6eb7f..b203638 100644 --- a/services/iam/internal/application/auth/service.go +++ b/services/iam/internal/application/auth/service.go @@ -32,6 +32,8 @@ type EmailService interface { SendOTP(ctx context.Context, email, otp string, expiryMinutes int) error // Send2FANotification sends a notification about 2FA status change. Send2FANotification(ctx context.Context, email, action string) error + // SendEmailVerification sends an email verification OTP to the user's email. + SendEmailVerification(ctx context.Context, email, otp string, expiryMinutes int) error } // Service implements domainAuth.Service interface. @@ -122,9 +124,11 @@ func (s *Service) Login(ctx context.Context, input domainAuth.LoginInput) (*doma Email: u.Email(), FullName: fullName, TwoFactorEnabled: u.TwoFactorEnabled(), + EmailVerified: u.IsEmailVerified(), Roles: roleNames, Permissions: permNames, }, + RequiresEmailVerification: !u.IsEmailVerified(), }, nil } @@ -348,13 +352,18 @@ func (s *Service) ForgotPassword(ctx context.Context, email string) (expiresIn i u, err := s.userRepo.GetByEmail(ctx, email) if err != nil { - // Return success even if user not found (prevent email enumeration) + // Return success even if user not found (prevent email enumeration). if errors.Is(err, shared.ErrNotFound) { return int(s.securityCfg.OTPExpiry.Seconds()), nil } return 0, fmt.Errorf("failed to get user: %w", err) } + // Reject if email not verified (prevent reset on unverified accounts). + if !u.IsEmailVerified() { + return 0, shared.ErrEmailNotVerified + } + // Generate OTP otp := generateOTP(6) if err := s.otpCache.StoreOTP(ctx, u.ID(), otp); err != nil { @@ -600,6 +609,132 @@ func (s *Service) Disable2FA(ctx context.Context, userID uuid.UUID, pwd, verific return nil } +// ============================================================================= +// Email Verification +// ============================================================================= + +const emailVerifyExpiry = 15 * time.Minute + +// SendEmailVerification sends a verification code to the authenticated user's email. +func (s *Service) SendEmailVerification(ctx context.Context, userID uuid.UUID) (*domainAuth.EmailVerificationResult, error) { + if s.otpCache == nil { + return nil, errors.New("email verification service unavailable") + } + + u, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + if u.IsEmailVerified() { + return nil, shared.ErrEmailAlreadyVerified + } + + return s.sendVerificationCode(ctx, u) +} + +// VerifyEmail consumes a verification code and marks the user's email as verified. +func (s *Service) VerifyEmail(ctx context.Context, userID uuid.UUID, code string) error { + if s.otpCache == nil { + return errors.New("email verification service unavailable") + } + + u, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + + if u.IsEmailVerified() { + return shared.ErrEmailAlreadyVerified + } + + valid, verifyErr := s.otpCache.VerifyEmailOTP(ctx, userID, code) + if verifyErr != nil || !valid { + return shared.ErrInvalidVerifyCode + } + + u.VerifyEmail() + if err := s.userRepo.Update(ctx, u); err != nil { + return fmt.Errorf("failed to update user email verification: %w", err) + } + + s.logAudit(ctx, userID, "EMAIL_VERIFIED", "User verified email address", "", "") + return nil +} + +// ResendEmailVerification re-sends the verification code (rate-limited to 1/min). +func (s *Service) ResendEmailVerification(ctx context.Context, userID uuid.UUID) (*domainAuth.EmailVerificationResult, error) { + if s.otpCache == nil { + return nil, errors.New("email verification service unavailable") + } + + // Check cooldown. + onCooldown, err := s.otpCache.IsEmailVerifyCooldown(ctx, userID) + if err != nil { + log.Warn().Err(err).Msg("failed to check email verify cooldown") + } + if onCooldown { + return nil, shared.ErrVerificationCooldown + } + + u, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + if u.IsEmailVerified() { + return nil, shared.ErrEmailAlreadyVerified + } + + return s.sendVerificationCode(ctx, u) +} + +// sendVerificationCode generates, stores, and sends a 6-digit verification code. +func (s *Service) sendVerificationCode(ctx context.Context, u *user.User) (*domainAuth.EmailVerificationResult, error) { + code := generateOTP(6) + if err := s.otpCache.StoreEmailVerificationOTP(ctx, u.ID(), code, emailVerifyExpiry); err != nil { + return nil, fmt.Errorf("failed to store verification code: %w", err) + } + + // Set cooldown to prevent resend spam. + if err := s.otpCache.SetEmailVerifyCooldown(ctx, u.ID()); err != nil { + log.Warn().Err(err).Msg("failed to set email verify cooldown") + } + + // Mask email for display (e.g., "j***@example.com"). + maskedEmail := maskEmail(u.Email()) + + if s.emailService != nil { + expiryMin := int(emailVerifyExpiry.Minutes()) + if err := s.emailService.SendEmailVerification(ctx, u.Email(), code, expiryMin); err != nil { + log.Warn().Err(err).Str("email", u.Email()).Msg("failed to send verification email") + // Don't fail — code is stored, user can retry. + } + } else { + log.Warn().Str("code", code).Str("email", u.Email()).Msg("Email service not configured, verification code logged for development") + } + + s.logAudit(ctx, u.ID(), "EMAIL_VERIFICATION_SENT", "Email verification code sent", "", "") + + return &domainAuth.EmailVerificationResult{ + Message: fmt.Sprintf("Verification code sent to %s", maskedEmail), + ExpiresIn: int(emailVerifyExpiry.Seconds()), + }, nil +} + +// maskEmail masks an email address for display (e.g., "john@example.com" → "j***@example.com"). +func maskEmail(email string) string { + parts := strings.SplitN(email, "@", 2) + if len(parts) != 2 { + return email + } + local := parts[0] + if len(local) <= 1 { + return local + "***@" + parts[1] + } + return string(local[0]) + "***@" + parts[1] +} + // Helper functions // verifyPassword verifies a password against a stored hash. diff --git a/services/iam/internal/application/user/handlers_test.go b/services/iam/internal/application/user/handlers_test.go index f48ba94..af12c08 100644 --- a/services/iam/internal/application/user/handlers_test.go +++ b/services/iam/internal/application/user/handlers_test.go @@ -206,6 +206,7 @@ func newDummyUser(id uuid.UUID) *user.User { nil, // lastLoginAt "", // lastLoginIP nil, // passwordChangedAt + nil, // emailVerifiedAt shared.NewAuditInfo("admin"), ) } diff --git a/services/iam/internal/delivery/grpc/auth_handler.go b/services/iam/internal/delivery/grpc/auth_handler.go index f0b59ec..382a5d6 100644 --- a/services/iam/internal/delivery/grpc/auth_handler.go +++ b/services/iam/internal/delivery/grpc/auth_handler.go @@ -84,9 +84,11 @@ func (h *AuthHandler) Login(ctx context.Context, req *iamv1.LoginRequest) (*iamv Email: result.User.Email, FullName: result.User.FullName, TwoFactorEnabled: result.User.TwoFactorEnabled, + EmailVerified: result.User.EmailVerified, Roles: result.User.Roles, Permissions: result.User.Permissions, }, + RequiresEmailVerification: result.RequiresEmailVerification, }, }, nil } @@ -293,19 +295,86 @@ func (h *AuthHandler) GetCurrentUser(ctx context.Context, _ *iamv1.GetCurrentUse permissionNames[i] = p.Code() } + // Get full name from detail if available. + var fullName string + detail, detailErr := h.userRepo.GetDetailByUserID(ctx, userID) + if detailErr == nil && detail != nil { + fullName = detail.FullName() + } + return &iamv1.GetCurrentUserResponse{ Base: SuccessResponse("User retrieved successfully"), Data: &iamv1.AuthUser{ UserId: u.ID().String(), Username: u.Username(), Email: u.Email(), + FullName: fullName, TwoFactorEnabled: u.TwoFactorEnabled(), + EmailVerified: u.IsEmailVerified(), Roles: roleNames, Permissions: permissionNames, }, }, nil } +// SendEmailVerification sends a verification code to the authenticated user's email. +func (h *AuthHandler) SendEmailVerification(ctx context.Context, _ *iamv1.SendEmailVerificationRequest) (*iamv1.SendEmailVerificationResponse, error) { + userID, err := getUserIDFromContext(ctx) + if err != nil { + return &iamv1.SendEmailVerificationResponse{Base: UnauthorizedResponse("not authenticated")}, nil //nolint:nilerr // error returned in response body + } + + result, err := h.authService.SendEmailVerification(ctx, userID) + if err != nil { + return &iamv1.SendEmailVerificationResponse{Base: domainErrorToBaseResponse(err)}, nil //nolint:nilerr // error returned in response body + } + + return &iamv1.SendEmailVerificationResponse{ + Base: SuccessResponse("Verification code sent"), + Message: result.Message, + ExpiresIn: safeconv.IntToInt32(result.ExpiresIn), + }, nil +} + +// VerifyEmail consumes a verification code and marks the user's email as verified. +func (h *AuthHandler) VerifyEmail(ctx context.Context, req *iamv1.VerifyEmailRequest) (*iamv1.VerifyEmailResponse, error) { + if baseResp := h.validationHelper.ValidateRequest(req); baseResp != nil { + return &iamv1.VerifyEmailResponse{Base: baseResp}, nil + } + + userID, err := getUserIDFromContext(ctx) + if err != nil { + return &iamv1.VerifyEmailResponse{Base: UnauthorizedResponse("not authenticated")}, nil //nolint:nilerr // error returned in response body + } + + if err := h.authService.VerifyEmail(ctx, userID, req.GetCode()); err != nil { + return &iamv1.VerifyEmailResponse{Base: domainErrorToBaseResponse(err)}, nil //nolint:nilerr // error returned in response body + } + + return &iamv1.VerifyEmailResponse{ + Base: SuccessResponse("Email verified successfully"), + }, nil +} + +// ResendEmailVerification re-sends the verification code. +func (h *AuthHandler) ResendEmailVerification(ctx context.Context, _ *iamv1.ResendEmailVerificationRequest) (*iamv1.ResendEmailVerificationResponse, error) { + userID, err := getUserIDFromContext(ctx) + if err != nil { + return &iamv1.ResendEmailVerificationResponse{Base: UnauthorizedResponse("not authenticated")}, nil //nolint:nilerr // error returned in response body + } + + result, err := h.authService.ResendEmailVerification(ctx, userID) + if err != nil { + return &iamv1.ResendEmailVerificationResponse{Base: domainErrorToBaseResponse(err)}, nil //nolint:nilerr // error returned in response body + } + + return &iamv1.ResendEmailVerificationResponse{ + Base: SuccessResponse("Verification code re-sent"), + Message: result.Message, + ExpiresIn: safeconv.IntToInt32(result.ExpiresIn), + }, nil +} + // getUserIDFromContext extracts the user ID from the context. // The user ID is set by AuthInterceptor using the UserIDKey typed context key. func getUserIDFromContext(ctx context.Context) (uuid.UUID, error) { diff --git a/services/iam/internal/delivery/grpc/permission_interceptor.go b/services/iam/internal/delivery/grpc/permission_interceptor.go index facb0cb..1611b11 100644 --- a/services/iam/internal/delivery/grpc/permission_interceptor.go +++ b/services/iam/internal/delivery/grpc/permission_interceptor.go @@ -21,11 +21,14 @@ type PermissionRequirement struct { // Methods not listed here and not in publicMethods will be denied by default. var methodPermissions = map[string]PermissionRequirement{ // Auth Service — authenticated only (no specific permission) - "/iam.v1.AuthService/GetCurrentUser": {Permission: ""}, - "/iam.v1.AuthService/UpdatePassword": {Permission: ""}, - "/iam.v1.AuthService/Enable2FA": {Permission: ""}, - "/iam.v1.AuthService/Verify2FA": {Permission: ""}, - "/iam.v1.AuthService/Disable2FA": {Permission: ""}, + "/iam.v1.AuthService/GetCurrentUser": {Permission: ""}, + "/iam.v1.AuthService/UpdatePassword": {Permission: ""}, + "/iam.v1.AuthService/Enable2FA": {Permission: ""}, + "/iam.v1.AuthService/Verify2FA": {Permission: ""}, + "/iam.v1.AuthService/Disable2FA": {Permission: ""}, + "/iam.v1.AuthService/SendEmailVerification": {Permission: ""}, + "/iam.v1.AuthService/VerifyEmail": {Permission: ""}, + "/iam.v1.AuthService/ResendEmailVerification": {Permission: ""}, // User Service "/iam.v1.UserService/CreateUser": {Permission: "iam.user.account.create"}, diff --git a/services/iam/internal/delivery/grpc/validation_helper.go b/services/iam/internal/delivery/grpc/validation_helper.go index 5a48a3e..f27bb00 100644 --- a/services/iam/internal/delivery/grpc/validation_helper.go +++ b/services/iam/internal/delivery/grpc/validation_helper.go @@ -146,7 +146,8 @@ func domainErrorToBaseResponse(err error) *commonv1.BaseResponse { case errors.Is(err, shared.ErrNotFound): return NotFoundResponse(err.Error()) case errors.Is(err, shared.ErrAlreadyExists), - errors.Is(err, shared.ErrAlreadyDeleted): + errors.Is(err, shared.ErrAlreadyDeleted), + errors.Is(err, shared.ErrEmailAlreadyVerified): return ConflictResponse(err.Error()) case errors.Is(err, shared.ErrUnauthorized), errors.Is(err, shared.ErrInvalidCredentials), @@ -159,6 +160,12 @@ func domainErrorToBaseResponse(err error) *commonv1.BaseResponse { case errors.Is(err, shared.ErrTOTPRequired), errors.Is(err, shared.ErrTwoFARequired): return ErrorResponse("428", "2FA required") + case errors.Is(err, shared.ErrEmailNotVerified): + return ErrorResponse("412", err.Error()) + case errors.Is(err, shared.ErrVerificationCooldown): + return ErrorResponse("429", err.Error()) + case errors.Is(err, shared.ErrInvalidVerifyCode): + return ErrorResponse("400", err.Error()) case errors.Is(err, shared.ErrNotActive), errors.Is(err, shared.ErrTOTPInvalid), errors.Is(err, shared.ErrInvalid2FACode), @@ -166,19 +173,25 @@ func domainErrorToBaseResponse(err error) *commonv1.BaseResponse { errors.Is(err, shared.ErrInvalidOTP): return ErrorResponse("422", err.Error()) default: - errMsg := err.Error() - switch { - case strings.Contains(errMsg, "invalid"): - return ErrorResponse("400", errMsg) - case strings.Contains(errMsg, "not found"): - return NotFoundResponse(errMsg) - case strings.Contains(errMsg, "already exists"): - return ConflictResponse(errMsg) - case strings.Contains(errMsg, "not editable"): - return ErrorResponse("422", errMsg) - default: - log.Error().Err(err).Msg("unhandled domain error mapped to 500") - return InternalErrorResponse("internal server error") - } + return mapUnknownError(err) + } +} + +// mapUnknownError handles errors not matched by sentinel checks, +// using error message heuristics as a last resort. +func mapUnknownError(err error) *commonv1.BaseResponse { + errMsg := err.Error() + switch { + case strings.Contains(errMsg, "invalid"): + return ErrorResponse("400", errMsg) + case strings.Contains(errMsg, "not found"): + return NotFoundResponse(errMsg) + case strings.Contains(errMsg, "already exists"): + return ConflictResponse(errMsg) + case strings.Contains(errMsg, "not editable"): + return ErrorResponse("422", errMsg) + default: + log.Error().Err(err).Msg("unhandled domain error mapped to 500") + return InternalErrorResponse("internal server error") } } diff --git a/services/iam/internal/domain/auth/service.go b/services/iam/internal/domain/auth/service.go index ce4af28..5779bd1 100644 --- a/services/iam/internal/domain/auth/service.go +++ b/services/iam/internal/domain/auth/service.go @@ -38,6 +38,15 @@ type Service interface { // Disable2FA disables 2FA for a user. Disable2FA(ctx context.Context, userID uuid.UUID, password, totpCode string) error + + // SendEmailVerification sends a verification code to the authenticated user's email. + SendEmailVerification(ctx context.Context, userID uuid.UUID) (*EmailVerificationResult, error) + + // VerifyEmail consumes a verification code and marks the user's email as verified. + VerifyEmail(ctx context.Context, userID uuid.UUID, code string) error + + // ResendEmailVerification re-sends the verification code (rate-limited). + ResendEmailVerification(ctx context.Context, userID uuid.UUID) (*EmailVerificationResult, error) } // LoginInput contains the login request parameters. @@ -52,10 +61,11 @@ type LoginInput struct { // LoginResult contains the login response data. type LoginResult struct { - AccessToken string - RefreshToken string - ExpiresIn int64 - User *UserInfo + AccessToken string + RefreshToken string + ExpiresIn int64 + User *UserInfo + RequiresEmailVerification bool } // UserInfo contains basic user info for authentication context. @@ -65,10 +75,17 @@ type UserInfo struct { Email string FullName string TwoFactorEnabled bool + EmailVerified bool Roles []string Permissions []string } +// EmailVerificationResult contains the result of sending a verification email. +type EmailVerificationResult struct { + Message string + ExpiresIn int +} + // RefreshResult contains the token refresh response data. type RefreshResult struct { AccessToken string diff --git a/services/iam/internal/domain/shared/errors.go b/services/iam/internal/domain/shared/errors.go index 11e6a80..8d85823 100644 --- a/services/iam/internal/domain/shared/errors.go +++ b/services/iam/internal/domain/shared/errors.go @@ -44,6 +44,12 @@ var ( // OTP errors ErrInvalidOTP = errors.New("invalid OTP code") + // Email verification errors + ErrEmailNotVerified = errors.New("email address has not been verified") + ErrEmailAlreadyVerified = errors.New("email address is already verified") + ErrVerificationCooldown = errors.New("please wait before requesting a new verification code") + ErrInvalidVerifyCode = errors.New("invalid or expired verification code") + // Legacy aliases for compatibility ErrTOTPRequired = ErrTwoFARequired ErrTOTPInvalid = ErrInvalid2FACode diff --git a/services/iam/internal/domain/user/entity.go b/services/iam/internal/domain/user/entity.go index dd1e451..24b83df 100644 --- a/services/iam/internal/domain/user/entity.go +++ b/services/iam/internal/domain/user/entity.go @@ -49,6 +49,7 @@ type User struct { lastLoginAt *time.Time lastLoginIP string passwordChangedAt *time.Time + emailVerifiedAt *time.Time audit shared.AuditInfo } @@ -89,6 +90,7 @@ func ReconstructUser( lastLoginAt *time.Time, lastLoginIP string, passwordChangedAt *time.Time, + emailVerifiedAt *time.Time, audit shared.AuditInfo, ) *User { return &User{ @@ -105,6 +107,7 @@ func ReconstructUser( lastLoginAt: lastLoginAt, lastLoginIP: lastLoginIP, passwordChangedAt: passwordChangedAt, + emailVerifiedAt: emailVerifiedAt, audit: audit, } } @@ -148,6 +151,23 @@ func (u *User) LastLoginIP() string { return u.lastLoginIP } // PasswordChangedAt returns when the password was last changed. func (u *User) PasswordChangedAt() *time.Time { return u.passwordChangedAt } +// EmailVerifiedAt returns when the email was verified. +func (u *User) EmailVerifiedAt() *time.Time { return u.emailVerifiedAt } + +// IsEmailVerified returns whether the user's email has been verified. +func (u *User) IsEmailVerified() bool { return u.emailVerifiedAt != nil } + +// VerifyEmail marks the user's email as verified. +func (u *User) VerifyEmail() { + now := time.Now() + u.emailVerifiedAt = &now +} + +// ClearEmailVerification clears the email verification (e.g., on email change). +func (u *User) ClearEmailVerification() { + u.emailVerifiedAt = nil +} + // Audit returns the audit information. func (u *User) Audit() shared.AuditInfo { return u.audit } diff --git a/services/iam/internal/domain/user/entity_test.go b/services/iam/internal/domain/user/entity_test.go index e52a84a..3d7f17e 100644 --- a/services/iam/internal/domain/user/entity_test.go +++ b/services/iam/internal/domain/user/entity_test.go @@ -36,7 +36,7 @@ func deletedUser(t *testing.T) *user.User { false, false, 0, nil, false, "", nil, "", - nil, + nil, nil, shared.AuditInfo{ CreatedAt: time.Now().Add(-24 * time.Hour), CreatedBy: "admin", @@ -55,7 +55,7 @@ func lockedUser(t *testing.T, lockedUntil *time.Time) *user.User { true, true, 5, lockedUntil, false, "", nil, "", - nil, + nil, nil, shared.AuditInfo{ CreatedAt: time.Now().Add(-24 * time.Hour), CreatedBy: "admin", @@ -213,7 +213,7 @@ func TestUser_CanLogin(t *testing.T) { false, false, 0, nil, false, "", nil, "", - nil, + nil, nil, shared.AuditInfo{ CreatedAt: time.Now(), CreatedBy: "admin", @@ -549,7 +549,7 @@ func TestReconstructUser(t *testing.T) { true, true, 3, &lockedUntil, true, "TOTP_SECRET_123", &lastLogin, "10.0.0.50", - &passwordChanged, + &passwordChanged, nil, audit, ) diff --git a/services/iam/internal/infrastructure/email/service.go b/services/iam/internal/infrastructure/email/service.go index 84b927b..d3d4f74 100644 --- a/services/iam/internal/infrastructure/email/service.go +++ b/services/iam/internal/infrastructure/email/service.go @@ -50,6 +50,25 @@ func (s *Service) SendOTP(ctx context.Context, email, otp string, expiryMinutes return s.send(ctx, email, subject, body) } +// SendEmailVerification sends an email verification OTP to the user's email. +func (s *Service) SendEmailVerification(ctx context.Context, email, otp string, expiryMinutes int) error { + subject := "GoApps - Email Verification" + body := fmt.Sprintf(` + +
+Your email verification code is:
+This code expires in %d minutes.
+If you did not request this, please ignore this email.
+ +`, otp, expiryMinutes) + + return s.send(ctx, email, subject, body) +} + // Send2FANotification sends a notification about 2FA status change. func (s *Service) Send2FANotification(ctx context.Context, email, action string) error { subject := "GoApps - Two-Factor Authentication Update" diff --git a/services/iam/internal/infrastructure/postgres/user_repository.go b/services/iam/internal/infrastructure/postgres/user_repository.go index bfad772..af7b87e 100644 --- a/services/iam/internal/infrastructure/postgres/user_repository.go +++ b/services/iam/internal/infrastructure/postgres/user_repository.go @@ -35,14 +35,14 @@ func (r *UserRepository) Create(ctx context.Context, u *user.User, detail *user. INSERT INTO mst_user ( user_id, username, email, password_hash, is_active, is_locked, failed_login_attempts, locked_until, two_factor_enabled, two_factor_secret, - last_login_at, last_login_ip, password_changed_at, + last_login_at, last_login_ip, password_changed_at, email_verified_at, created_at, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) ` _, err := tx.ExecContext(ctx, query, u.ID(), u.Username(), u.Email(), u.PasswordHash(), u.IsActive(), u.IsLocked(), u.FailedLoginAttempts(), u.LockedUntil(), u.TwoFactorEnabled(), u.TwoFactorSecret(), - u.LastLoginAt(), u.LastLoginIP(), u.PasswordChangedAt(), + u.LastLoginAt(), u.LastLoginIP(), u.PasswordChangedAt(), u.EmailVerifiedAt(), u.Audit().CreatedAt, u.Audit().CreatedBy, ) if err != nil { @@ -83,7 +83,7 @@ func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*user.User, query := ` SELECT user_id, username, email, password_hash, is_active, is_locked, failed_login_attempts, locked_until, two_factor_enabled, two_factor_secret, - last_login_at, last_login_ip, password_changed_at, + last_login_at, last_login_ip, password_changed_at, email_verified_at, created_at, created_by, updated_at, updated_by, deleted_at, deleted_by FROM mst_user WHERE user_id = $1 AND deleted_at IS NULL @@ -93,7 +93,7 @@ func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*user.User, err := r.db.QueryRowContext(ctx, query, id).Scan( &u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.IsActive, &u.IsLocked, &u.FailedLoginAttempts, &u.LockedUntil, &u.TwoFactorEnabled, &u.TwoFactorSecret, - &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, + &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, &u.EmailVerifiedAt, &u.CreatedAt, &u.CreatedBy, &u.UpdatedAt, &u.UpdatedBy, &u.DeletedAt, &u.DeletedBy, ) if err != nil { @@ -111,7 +111,7 @@ func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*u query := ` SELECT user_id, username, email, password_hash, is_active, is_locked, failed_login_attempts, locked_until, two_factor_enabled, two_factor_secret, - last_login_at, last_login_ip, password_changed_at, + last_login_at, last_login_ip, password_changed_at, email_verified_at, created_at, created_by, updated_at, updated_by, deleted_at, deleted_by FROM mst_user WHERE username = $1 AND deleted_at IS NULL @@ -121,7 +121,7 @@ func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*u err := r.db.QueryRowContext(ctx, query, username).Scan( &u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.IsActive, &u.IsLocked, &u.FailedLoginAttempts, &u.LockedUntil, &u.TwoFactorEnabled, &u.TwoFactorSecret, - &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, + &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, &u.EmailVerifiedAt, &u.CreatedAt, &u.CreatedBy, &u.UpdatedAt, &u.UpdatedBy, &u.DeletedAt, &u.DeletedBy, ) if err != nil { @@ -139,7 +139,7 @@ func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*user.Us query := ` SELECT user_id, username, email, password_hash, is_active, is_locked, failed_login_attempts, locked_until, two_factor_enabled, two_factor_secret, - last_login_at, last_login_ip, password_changed_at, + last_login_at, last_login_ip, password_changed_at, email_verified_at, created_at, created_by, updated_at, updated_by, deleted_at, deleted_by FROM mst_user WHERE email = $1 AND deleted_at IS NULL @@ -149,7 +149,7 @@ func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*user.Us err := r.db.QueryRowContext(ctx, query, email).Scan( &u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.IsActive, &u.IsLocked, &u.FailedLoginAttempts, &u.LockedUntil, &u.TwoFactorEnabled, &u.TwoFactorSecret, - &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, + &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, &u.EmailVerifiedAt, &u.CreatedAt, &u.CreatedBy, &u.UpdatedAt, &u.UpdatedBy, &u.DeletedAt, &u.DeletedBy, ) if err != nil { @@ -169,7 +169,8 @@ func (r *UserRepository) Update(ctx context.Context, u *user.User) error { email = $2, password_hash = $3, is_active = $4, is_locked = $5, failed_login_attempts = $6, locked_until = $7, two_factor_enabled = $8, two_factor_secret = $9, last_login_at = $10, last_login_ip = $11, - password_changed_at = $12, updated_at = $13, updated_by = $14 + password_changed_at = $12, email_verified_at = $13, + updated_at = $14, updated_by = $15 WHERE user_id = $1 AND deleted_at IS NULL ` @@ -177,7 +178,8 @@ func (r *UserRepository) Update(ctx context.Context, u *user.User) error { u.ID(), u.Email(), u.PasswordHash(), u.IsActive(), u.IsLocked(), u.FailedLoginAttempts(), u.LockedUntil(), u.TwoFactorEnabled(), u.TwoFactorSecret(), u.LastLoginAt(), u.LastLoginIP(), - u.PasswordChangedAt(), u.Audit().UpdatedAt, u.Audit().UpdatedBy, + u.PasswordChangedAt(), u.EmailVerifiedAt(), + u.Audit().UpdatedAt, u.Audit().UpdatedBy, ) if err != nil { return fmt.Errorf("failed to update user: %w", err) @@ -336,7 +338,7 @@ func (r *UserRepository) List(ctx context.Context, params user.ListParams) ([]*u query := fmt.Sprintf(` SELECT user_id, username, email, password_hash, is_active, is_locked, failed_login_attempts, locked_until, two_factor_enabled, two_factor_secret, - last_login_at, last_login_ip, password_changed_at, + last_login_at, last_login_ip, password_changed_at, email_verified_at, created_at, created_by, updated_at, updated_by, deleted_at, deleted_by FROM mst_user WHERE %s @@ -362,7 +364,7 @@ func (r *UserRepository) List(ctx context.Context, params user.ListParams) ([]*u if err := rows.Scan( &u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.IsActive, &u.IsLocked, &u.FailedLoginAttempts, &u.LockedUntil, &u.TwoFactorEnabled, &u.TwoFactorSecret, - &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, + &u.LastLoginAt, &u.LastLoginIP, &u.PasswordChangedAt, &u.EmailVerifiedAt, &u.CreatedAt, &u.CreatedBy, &u.UpdatedAt, &u.UpdatedBy, &u.DeletedAt, &u.DeletedBy, ); err != nil { return nil, 0, fmt.Errorf("failed to scan user: %w", err) @@ -559,14 +561,14 @@ func (r *UserRepository) BatchCreate(ctx context.Context, users []*user.User, de INSERT INTO mst_user ( user_id, username, email, password_hash, is_active, is_locked, failed_login_attempts, locked_until, two_factor_enabled, two_factor_secret, - last_login_at, last_login_ip, password_changed_at, + last_login_at, last_login_ip, password_changed_at, email_verified_at, created_at, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) ` _, err := tx.ExecContext(ctx, query, u.ID(), u.Username(), u.Email(), u.PasswordHash(), u.IsActive(), u.IsLocked(), u.FailedLoginAttempts(), u.LockedUntil(), u.TwoFactorEnabled(), u.TwoFactorSecret(), - u.LastLoginAt(), u.LastLoginIP(), u.PasswordChangedAt(), + u.LastLoginAt(), u.LastLoginIP(), u.PasswordChangedAt(), u.EmailVerifiedAt(), u.Audit().CreatedAt, u.Audit().CreatedBy, ) if err != nil { @@ -668,6 +670,7 @@ type userRow struct { LastLoginAt *time.Time LastLoginIP sql.NullString PasswordChangedAt *time.Time + EmailVerifiedAt *time.Time CreatedAt time.Time CreatedBy string UpdatedAt *time.Time @@ -698,7 +701,7 @@ func (r *userRow) toDomain() *user.User { r.ID, r.Username, r.Email, r.PasswordHash, r.IsActive, r.IsLocked, r.FailedLoginAttempts, r.LockedUntil, r.TwoFactorEnabled, secret, r.LastLoginAt, ip, r.PasswordChangedAt, - audit, + r.EmailVerifiedAt, audit, ) } diff --git a/services/iam/internal/infrastructure/redis/cache.go b/services/iam/internal/infrastructure/redis/cache.go index b9f430f..2b2b87c 100644 --- a/services/iam/internal/infrastructure/redis/cache.go +++ b/services/iam/internal/infrastructure/redis/cache.go @@ -130,9 +130,11 @@ func NewOTPCache(client *Client, ttl time.Duration) *OTPCache { } const ( - otpPrefix = "iam:otp:" - resetTokenPrefix = "iam:reset:" - loginAttemptPrefix = "iam:login_attempt:" + otpPrefix = "iam:otp:" + resetTokenPrefix = "iam:reset:" + loginAttemptPrefix = "iam:login_attempt:" + emailVerifyPrefix = "iam:email_verify:" + emailVerifyCoolPrefix = "iam:email_verify_cooldown:" ) // StoreOTP stores an OTP code for a user. @@ -235,6 +237,53 @@ func (c *OTPCache) Delete2FASetup(ctx context.Context, userID uuid.UUID) error { return err } +// StoreEmailVerificationOTP stores a 6-digit email verification code for a user. +func (c *OTPCache) StoreEmailVerificationOTP(ctx context.Context, userID uuid.UUID, code string, ttl time.Duration) error { + key := emailVerifyPrefix + userID.String() + return c.client.Set(ctx, key, code, ttl).Err() +} + +// VerifyEmailOTP verifies and deletes an email verification code. +func (c *OTPCache) VerifyEmailOTP(ctx context.Context, userID uuid.UUID, code string) (bool, error) { + key := emailVerifyPrefix + userID.String() + stored, err := c.client.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return false, nil + } + return false, err + } + + if stored != code { + return false, nil + } + + // Delete after successful verification. + if delErr := c.client.Del(ctx, key).Err(); delErr != nil { + return true, fmt.Errorf("verified but failed to delete key: %w", delErr) + } + return true, nil +} + +// SetEmailVerifyCooldown sets a 60-second cooldown to prevent resend spam. +func (c *OTPCache) SetEmailVerifyCooldown(ctx context.Context, userID uuid.UUID) error { + key := emailVerifyCoolPrefix + userID.String() + return c.client.Set(ctx, key, "1", 60*time.Second).Err() +} + +// IsEmailVerifyCooldown checks if the user is still in the resend cooldown period. +func (c *OTPCache) IsEmailVerifyCooldown(ctx context.Context, userID uuid.UUID) (bool, error) { + key := emailVerifyCoolPrefix + userID.String() + _, err := c.client.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return false, nil + } + return false, err + } + return true, nil +} + // RateLimitCache provides rate limiting functionality. type RateLimitCache struct { client *Client diff --git a/services/iam/migrations/postgres/000017_add_email_verification.down.sql b/services/iam/migrations/postgres/000017_add_email_verification.down.sql new file mode 100644 index 0000000..753d1f0 --- /dev/null +++ b/services/iam/migrations/postgres/000017_add_email_verification.down.sql @@ -0,0 +1,4 @@ +-- IAM Service Database Migrations +-- 000017: Rollback email verification support + +ALTER TABLE mst_user DROP COLUMN IF EXISTS email_verified_at; diff --git a/services/iam/migrations/postgres/000017_add_email_verification.up.sql b/services/iam/migrations/postgres/000017_add_email_verification.up.sql new file mode 100644 index 0000000..e59b493 --- /dev/null +++ b/services/iam/migrations/postgres/000017_add_email_verification.up.sql @@ -0,0 +1,9 @@ +-- IAM Service Database Migrations +-- 000017: Add email verification support +-- +-- Changes: Add email_verified_at column to mst_user +-- Note: Verification OTP codes are stored in Redis (same pattern as password reset OTP) + +ALTER TABLE mst_user ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMP WITH TIME ZONE; + +COMMENT ON COLUMN mst_user.email_verified_at IS 'When the user verified their email; NULL means unverified';