diff --git a/Catalogos_Postales_Registro.sql b/Catalogos_Postales_Registro.sql new file mode 100644 index 0000000..8d60f12 --- /dev/null +++ b/Catalogos_Postales_Registro.sql @@ -0,0 +1,418 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO +SET XACT_ABORT ON; +GO + +/* + Catálogos postales para el registro público. + + Este script NO elimina, vacía ni recrea las tablas pobladas. + Solamente agrega columnas/índices faltantes y crea o actualiza + el procedimiento almacenado de consulta. +*/ + +IF OBJECT_ID(N'dbo.Gral_SAT_CP', N'U') IS NULL + THROW 51020, N'Falta la tabla poblada dbo.Gral_SAT_CP.', 1; +IF OBJECT_ID(N'dbo.Gral_SAT_Municipio', N'U') IS NULL + THROW 51021, N'Falta la tabla poblada dbo.Gral_SAT_Municipio.', 1; +IF OBJECT_ID(N'dbo.Gral_SAT_Localidad', N'U') IS NULL + THROW 51022, N'Falta la tabla poblada dbo.Gral_SAT_Localidad.', 1; +IF OBJECT_ID(N'dbo.Gral_SAT_Colonia', N'U') IS NULL + THROW 51023, N'Falta la tabla poblada dbo.Gral_SAT_Colonia.', 1; +IF OBJECT_ID(N'dbo.Gral_SAT_Estado', N'U') IS NULL + THROW 51024, N'Falta la tabla poblada dbo.Gral_SAT_Estado.', 1; +IF OBJECT_ID(N'dbo.Gral_SAT_Pais', N'U') IS NULL + THROW 51025, N'Falta la tabla poblada dbo.Gral_SAT_Pais.', 1; +GO + +/* Borrado lógico incremental para los catálogos utilizados. */ +DECLARE @Tablas TABLE ([Tabla] sysname NOT NULL PRIMARY KEY); +INSERT INTO @Tablas ([Tabla]) +VALUES + (N'Gral_SAT_CP'), + (N'Gral_SAT_Municipio'), + (N'Gral_SAT_Localidad'), + (N'Gral_SAT_Colonia'), + (N'Gral_SAT_Estado'), + (N'Gral_SAT_Pais'); + +DECLARE @Tabla sysname; +DECLARE @Sql nvarchar(max); +DECLARE Catalogos CURSOR LOCAL FAST_FORWARD FOR SELECT [Tabla] FROM @Tablas ORDER BY [Tabla]; + +OPEN Catalogos; +FETCH NEXT FROM Catalogos INTO @Tabla; + +WHILE @@FETCH_STATUS = 0 +BEGIN + IF COL_LENGTH(N'dbo.' + @Tabla, N'Borrado') IS NULL + BEGIN + SET @Sql = N'ALTER TABLE [dbo].' + QUOTENAME(@Tabla) + + N' ADD [Borrado] bit NOT NULL CONSTRAINT ' + + QUOTENAME(N'DF_' + @Tabla + N'_Borrado') + + N' DEFAULT ((0)) WITH VALUES;'; + EXEC sys.sp_executesql @Sql; + END; + + IF COL_LENGTH(N'dbo.' + @Tabla, N'FechaBorrado') IS NULL + BEGIN + SET @Sql = N'ALTER TABLE [dbo].' + QUOTENAME(@Tabla) + + N' ADD [FechaBorrado] datetime2(0) NULL;'; + EXEC sys.sp_executesql @Sql; + END; + + IF NOT EXISTS + ( + SELECT 1 + FROM sys.check_constraints + WHERE [parent_object_id] = OBJECT_ID(N'dbo.' + @Tabla) + AND [name] = N'CK_' + @Tabla + N'_BorradoLogico' + ) + BEGIN + SET @Sql = N'ALTER TABLE [dbo].' + QUOTENAME(@Tabla) + + N' ADD CONSTRAINT ' + QUOTENAME(N'CK_' + @Tabla + N'_BorradoLogico') + + N' CHECK (([Borrado] = (0) AND [FechaBorrado] IS NULL)' + + N' OR ([Borrado] = (1) AND [FechaBorrado] IS NOT NULL));'; + EXEC sys.sp_executesql @Sql; + END; + + FETCH NEXT FROM Catalogos INTO @Tabla; +END; + +CLOSE Catalogos; +DEALLOCATE Catalogos; +GO + +IF NOT EXISTS +( + SELECT 1 FROM sys.indexes + WHERE [object_id] = OBJECT_ID(N'dbo.Gral_SAT_CP') + AND [name] = N'IX_Gral_SAT_CP_BusquedaRegistro' +) + CREATE NONCLUSTERED INDEX [IX_Gral_SAT_CP_BusquedaRegistro] + ON [dbo].[Gral_SAT_CP] ([CP]) + INCLUDE ([CveCP], [ClaveSATEstado], [ClaveSATMunicipio], [ClaveSATLocalidad]) + WHERE [Borrado] = (0); +GO + +IF NOT EXISTS +( + SELECT 1 FROM sys.indexes + WHERE [object_id] = OBJECT_ID(N'dbo.Gral_SAT_Colonia') + AND [name] = N'IX_Gral_SAT_Colonia_CP_Vigente' +) + CREATE NONCLUSTERED INDEX [IX_Gral_SAT_Colonia_CP_Vigente] + ON [dbo].[Gral_SAT_Colonia] ([CP]) + INCLUDE ([CveColonia], [ClaveSATColonia], [NombreAsentamiento]) + WHERE [Borrado] = (0); +GO + +IF NOT EXISTS +( + SELECT 1 FROM sys.indexes + WHERE [object_id] = OBJECT_ID(N'dbo.Gral_SAT_Municipio') + AND [name] = N'IX_Gral_SAT_Municipio_Claves_Vigente' +) + CREATE NONCLUSTERED INDEX [IX_Gral_SAT_Municipio_Claves_Vigente] + ON [dbo].[Gral_SAT_Municipio] ([ClaveSATEstado], [ClaveSATMunicipio]) + INCLUDE ([CveMunicipio], [Descripcion]) + WHERE [Borrado] = (0); +GO + +IF NOT EXISTS +( + SELECT 1 FROM sys.indexes + WHERE [object_id] = OBJECT_ID(N'dbo.Gral_SAT_Localidad') + AND [name] = N'IX_Gral_SAT_Localidad_Claves_Vigente' +) + CREATE NONCLUSTERED INDEX [IX_Gral_SAT_Localidad_Claves_Vigente] + ON [dbo].[Gral_SAT_Localidad] ([ClaveSATEstado], [ClaveSATLocalidad]) + INCLUDE ([CveLocalidad], [NombreLocalidad]) + WHERE [Borrado] = (0); +GO + +/* Claves normalizadas utilizadas por el registro. */ +IF OBJECT_ID(N'dbo.Clientes', N'U') IS NULL + THROW 51026, N'Falta la tabla dbo.Clientes.', 1; + +IF COL_LENGTH(N'dbo.Clientes', N'CveLocalidad') IS NULL + ALTER TABLE [dbo].[Clientes] ADD [CveLocalidad] int NULL; +GO + +IF OBJECT_ID(N'dbo.FK_Clientes_Gral_SAT_Pais', N'F') IS NULL + AND NOT EXISTS (SELECT 1 FROM [dbo].[Clientes] C LEFT JOIN [dbo].[Gral_SAT_Pais] P ON P.[CvePais]=C.[CvePais] WHERE C.[CvePais] IS NOT NULL AND P.[CvePais] IS NULL) + ALTER TABLE [dbo].[Clientes] WITH CHECK ADD CONSTRAINT [FK_Clientes_Gral_SAT_Pais] FOREIGN KEY ([CvePais]) REFERENCES [dbo].[Gral_SAT_Pais] ([CvePais]); +GO + +IF OBJECT_ID(N'dbo.FK_Clientes_Gral_SAT_Estado_Normalizado', N'F') IS NULL + AND NOT EXISTS (SELECT 1 FROM [dbo].[Clientes] C LEFT JOIN [dbo].[Gral_SAT_Estado] E ON E.[CveEstado]=C.[CveEstado] WHERE C.[CveEstado] IS NOT NULL AND E.[CveEstado] IS NULL) + ALTER TABLE [dbo].[Clientes] WITH CHECK ADD CONSTRAINT [FK_Clientes_Gral_SAT_Estado_Normalizado] FOREIGN KEY ([CveEstado]) REFERENCES [dbo].[Gral_SAT_Estado] ([CveEstado]); +GO + +IF OBJECT_ID(N'dbo.FK_Clientes_Gral_SAT_Municipio', N'F') IS NULL + AND NOT EXISTS (SELECT 1 FROM [dbo].[Clientes] C LEFT JOIN [dbo].[Gral_SAT_Municipio] M ON M.[CveMunicipio]=C.[CveMunicipio] WHERE C.[CveMunicipio] IS NOT NULL AND M.[CveMunicipio] IS NULL) + ALTER TABLE [dbo].[Clientes] WITH CHECK ADD CONSTRAINT [FK_Clientes_Gral_SAT_Municipio] FOREIGN KEY ([CveMunicipio]) REFERENCES [dbo].[Gral_SAT_Municipio] ([CveMunicipio]); +GO + +IF OBJECT_ID(N'dbo.FK_Clientes_Gral_SAT_Localidad', N'F') IS NULL + ALTER TABLE [dbo].[Clientes] WITH CHECK ADD CONSTRAINT [FK_Clientes_Gral_SAT_Localidad] FOREIGN KEY ([CveLocalidad]) REFERENCES [dbo].[Gral_SAT_Localidad] ([CveLocalidad]); +GO + +IF OBJECT_ID(N'dbo.FK_Clientes_Gral_SAT_Colonia', N'F') IS NULL + AND NOT EXISTS (SELECT 1 FROM [dbo].[Clientes] C LEFT JOIN [dbo].[Gral_SAT_Colonia] CO ON CO.[CveColonia]=C.[CveColonia] WHERE C.[CveColonia] IS NOT NULL AND CO.[CveColonia] IS NULL) + ALTER TABLE [dbo].[Clientes] WITH CHECK ADD CONSTRAINT [FK_Clientes_Gral_SAT_Colonia] FOREIGN KEY ([CveColonia]) REFERENCES [dbo].[Gral_SAT_Colonia] ([CveColonia]); +GO + +CREATE OR ALTER PROCEDURE [dbo].[PA_Gral_CodigoPostalGet] + @CP varchar(5), + @CveUsuarioEjecuta int, + @ErrorMostrar varchar(max) OUTPUT, + @ErrorTipo int OUTPUT, + @Return bit OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SELECT @ErrorMostrar = NULL, @ErrorTipo = 0, @Return = 0; + SET @CP = LTRIM(RTRIM(@CP)); + + BEGIN TRY + IF LEN(@CP) <> 5 OR @CP LIKE '%[^0-9]%' + BEGIN + SET @ErrorMostrar = 'El código postal debe contener exactamente 5 dígitos.'; + SET @ErrorTipo = 2; + RETURN; + END; + + DECLARE + @CvePais int, + @Pais varchar(100), + @CveEstado int, + @Estado varchar(50), + @CveMunicipio int, + @Municipio varchar(100), + @CveLocalidad int, + @Localidad varchar(100); + + SELECT TOP (1) + @CvePais = P.[CvePais], + @Pais = P.[Descripcion], + @CveEstado = E.[CveEstado], + @Estado = E.[NombreEstado], + @CveMunicipio = M.[CveMunicipio], + @Municipio = M.[Descripcion], + @CveLocalidad = L.[CveLocalidad], + @Localidad = L.[NombreLocalidad] + FROM [dbo].[Gral_SAT_CP] AS CP + INNER JOIN [dbo].[Gral_SAT_Estado] AS E + ON E.[ClaveSATEstado] = CP.[ClaveSATEstado] + AND E.[Borrado] = (0) + INNER JOIN [dbo].[Gral_SAT_Pais] AS P + ON P.[ClaveSATPais] = E.[ClaveSATPais] + AND P.[Borrado] = (0) + INNER JOIN [dbo].[Gral_SAT_Municipio] AS M + ON M.[ClaveSATEstado] = CP.[ClaveSATEstado] + AND M.[ClaveSATMunicipio] = CP.[ClaveSATMunicipio] + AND M.[Borrado] = (0) + LEFT JOIN [dbo].[Gral_SAT_Localidad] AS L + ON L.[ClaveSATEstado] = CP.[ClaveSATEstado] + AND L.[ClaveSATLocalidad] = CP.[ClaveSATLocalidad] + AND L.[Borrado] = (0) + WHERE CP.[CP] = @CP + AND CP.[Borrado] = (0); + + IF @CvePais IS NULL OR @CveEstado IS NULL OR @CveMunicipio IS NULL + BEGIN + SET @ErrorMostrar = CONCAT( + 'El código postal ', @CP, + ' no existe o sus relaciones de país, estado y municipio están incompletas.'); + SET @ErrorTipo = 2; + RETURN; + END; + + SELECT + @CvePais AS [CvePais], + @Pais AS [Pais], + @CveEstado AS [CveEstado], + @Estado AS [Estado], + @CveMunicipio AS [CveMunicipio], + @Municipio AS [Municipio], + @CveLocalidad AS [CveLocalidad], + @Localidad AS [Localidad], + COALESCE(NULLIF(@Localidad, ''), @Municipio) AS [Ciudad]; + + SELECT + C.[CveColonia], + C.[NombreAsentamiento] + FROM [dbo].[Gral_SAT_Colonia] AS C + WHERE C.[CP] = @CP + AND C.[Borrado] = (0) + ORDER BY C.[NombreAsentamiento]; + + SET @Return = 1; + END TRY + BEGIN CATCH + SET @ErrorMostrar = 'Ocurrió un error al consultar el código postal; favor de contactar a Sistemas.'; + SET @ErrorTipo = 1; + SET @Return = 0; + DECLARE @E nvarchar(2048) = CONCAT( + 'Número: ', ERROR_NUMBER(), + '; Procedimiento: ', ERROR_PROCEDURE(), + '; Línea: ', ERROR_LINE(), + '; Detalle: ', ERROR_MESSAGE()); + RAISERROR(N'%s', 10, 1, @E); + END CATCH; +END; +GO + +CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_RegistroClienteAdd] + @NombreUsuario varchar(100), + @HashContrasena varchar(500), + @CorreoElectronico varchar(255), + @RFC varchar(15), + @RazonSocial varchar(200), + @NombreComercial varchar(200) = NULL, + @Direccion varchar(90), + @NoExt varchar(20) = NULL, + @NoInt varchar(50) = NULL, + @Colonia varchar(100), + @Ciudad varchar(100), + @CveSATEstado int, + @Pais varchar(100) = 'México', + @CP varchar(5), + @Telefono varchar(25) = NULL, + @CvePais int, + @CveMunicipio int, + @CveLocalidad int = NULL, + @CveColonia int, + @HashToken char(64), + @HorasExpiracion int = 24, + @CveUsuarioEjecuta int, + @ErrorMostrar varchar(max) OUTPUT, + @ErrorTipo int OUTPUT, + @Return bit OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + SELECT @ErrorMostrar = NULL, @ErrorTipo = 0, @Return = 0; + + BEGIN TRY + DECLARE @CveRolEmpresa int, @CveUsuario int, @CveCliente int; + DECLARE @NombreEstado varchar(50), @PaisCatalogo varchar(100); + DECLARE @CiudadCatalogo varchar(100), @ColoniaCatalogo varchar(100); + + SET @NombreUsuario = LTRIM(RTRIM(@NombreUsuario)); + SET @CorreoElectronico = LOWER(LTRIM(RTRIM(@CorreoElectronico))); + SET @RFC = UPPER(REPLACE(LTRIM(RTRIM(@RFC)), ' ', '')); + SET @CP = LTRIM(RTRIM(@CP)); + + SELECT @CveRolEmpresa = [CveRol] + FROM [dbo].[Sis_Roles] + WHERE [NombreRol] = 'Empresa' AND [Activo] = (1) AND [Borrado] = (0); + + SELECT TOP (1) + @NombreEstado = LEFT(E.[NombreEstado], 35), + @PaisCatalogo = LEFT(P.[Descripcion], 50), + @CiudadCatalogo = LEFT(COALESCE(NULLIF(L.[NombreLocalidad], ''), M.[Descripcion]), 50), + @ColoniaCatalogo = LEFT(C.[NombreAsentamiento], 50) + FROM [dbo].[Gral_SAT_CP] AS CP + INNER JOIN [dbo].[Gral_SAT_Estado] AS E + ON E.[ClaveSATEstado] = CP.[ClaveSATEstado] + AND E.[CveEstado] = @CveSATEstado AND E.[Borrado] = (0) + INNER JOIN [dbo].[Gral_SAT_Pais] AS P + ON P.[ClaveSATPais] = E.[ClaveSATPais] + AND P.[CvePais] = @CvePais AND P.[Borrado] = (0) + INNER JOIN [dbo].[Gral_SAT_Municipio] AS M + ON M.[ClaveSATEstado] = CP.[ClaveSATEstado] + AND M.[ClaveSATMunicipio] = CP.[ClaveSATMunicipio] + AND M.[CveMunicipio] = @CveMunicipio AND M.[Borrado] = (0) + LEFT JOIN [dbo].[Gral_SAT_Localidad] AS L + ON L.[ClaveSATEstado] = CP.[ClaveSATEstado] + AND L.[ClaveSATLocalidad] = CP.[ClaveSATLocalidad] AND L.[Borrado] = (0) + INNER JOIN [dbo].[Gral_SAT_Colonia] AS C + ON C.[CP] = CP.[CP] AND C.[CveColonia] = @CveColonia AND C.[Borrado] = (0) + WHERE CP.[CP] = @CP AND CP.[Borrado] = (0) + AND (@CveLocalidad IS NULL OR L.[CveLocalidad] = @CveLocalidad); + + IF @CveRolEmpresa IS NULL + BEGIN + SELECT @ErrorMostrar = 'No fue posible determinar el rol Empresa.', @ErrorTipo = 1; + RETURN; + END; + + IF @NombreEstado IS NULL OR @PaisCatalogo IS NULL OR @CiudadCatalogo IS NULL OR @ColoniaCatalogo IS NULL + BEGIN + SELECT @ErrorMostrar = 'Los datos del código postal no coinciden con los catálogos vigentes. Actualiza el código postal.', @ErrorTipo = 2; + RETURN; + END; + + SELECT @Pais = @PaisCatalogo, @Ciudad = @CiudadCatalogo, @Colonia = @ColoniaCatalogo; + + IF EXISTS (SELECT 1 FROM [dbo].[Sis_Usuarios] WHERE [NombreUsuario] = @NombreUsuario) + OR EXISTS (SELECT 1 FROM [dbo].[Sis_Usuarios] WHERE LOWER([CorreoElectronico]) = @CorreoElectronico) + OR EXISTS (SELECT 1 FROM [dbo].[Clientes] WHERE UPPER(REPLACE(LTRIM(RTRIM([RFC])), ' ', '')) = @RFC) + BEGIN + SELECT @ErrorMostrar = 'Los datos ya fueron registrados por otra solicitud. Actualiza la página e inténtalo nuevamente.', @ErrorTipo = 2; + RETURN; + END; + + BEGIN TRANSACTION; + + INSERT INTO [dbo].[Sis_Usuarios] + ([NombreUsuario], [HashContrasena], [CorreoElectronico], [CveRol], [Activo], [DebeCambiarContrasena], [CorreoVerificado]) + VALUES (@NombreUsuario, @HashContrasena, @CorreoElectronico, @CveRolEmpresa, (0), (0), (0)); + + SET @CveUsuario = CONVERT(int, SCOPE_IDENTITY()); + + INSERT INTO [dbo].[Clientes] + ([Nombre], [NombreComercial], [Direccion], [NoExt], [NoInt], [Colonia], + [Ciudad], [Estado], [CveSATEstado], [Pais], [RFC], [CP], [Telefono], + [Mail], [Estatus], [Fecha], [CveUsuario], [FechaIng], + [CvePais], [CveEstado], [CveMunicipio], [CveLocalidad], [CveColonia]) + VALUES + (@RazonSocial, NULLIF(@NombreComercial, ''), @Direccion, NULLIF(@NoExt, ''), + NULLIF(@NoInt, ''), @Colonia, @Ciudad, @NombreEstado, @CveSATEstado, + @Pais, @RFC, @CP, NULLIF(@Telefono, ''), @CorreoElectronico, (0), + CONVERT(date, GETDATE()), @CveUsuario, GETDATE(), + @CvePais, @CveSATEstado, @CveMunicipio, @CveLocalidad, @CveColonia); + + SET @CveCliente = CONVERT(int, SCOPE_IDENTITY()); + + INSERT INTO [dbo].[Sis_UsuariosClientes] + ([CveUsuario], [CveCliente], [Orden], [EsPrincipal], [Activo], [CveUsuarioRegistro]) + VALUES (@CveUsuario, @CveCliente, (1), (1), (1), @CveUsuario); + + INSERT INTO [dbo].[Sis_UsuariosVerificacionesCorreo] + ([CveUsuario], [CveClienteRegistro], [HashToken], [FechaExpiracion]) + VALUES (@CveUsuario, @CveCliente, @HashToken, DATEADD(HOUR, @HorasExpiracion, SYSDATETIME())); + + COMMIT TRANSACTION; + + SELECT @CveUsuario AS [CveUsuario], @CveCliente AS [CveCliente]; + SET @Return = 1; + END TRY + BEGIN CATCH + IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; + SELECT @ErrorMostrar = 'Ocurrió un error al crear el registro; favor de contactar a Sistemas.', @ErrorTipo = 1, @Return = 0; + DECLARE @E nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', ERROR_PROCEDURE(), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE()); + RAISERROR(N'%s', 10, 1, @E); + END CATCH; +END; +GO + +/* + Se registran para limpieza, pero la limpieza física queda deshabilitada. + Habilitarla posteriormente requiere autorización expresa. +*/ +IF OBJECT_ID(N'dbo.Sis_ConfigurarLimpieza', N'P') IS NOT NULL +BEGIN + EXEC [dbo].[Sis_ConfigurarLimpieza] @Esquema=N'dbo', @Tabla=N'Gral_SAT_CP', @DiasRetencion=365, @TamanoLote=500, @Habilitada=0; + EXEC [dbo].[Sis_ConfigurarLimpieza] @Esquema=N'dbo', @Tabla=N'Gral_SAT_Municipio', @DiasRetencion=365, @TamanoLote=500, @Habilitada=0; + EXEC [dbo].[Sis_ConfigurarLimpieza] @Esquema=N'dbo', @Tabla=N'Gral_SAT_Localidad', @DiasRetencion=365, @TamanoLote=500, @Habilitada=0; + EXEC [dbo].[Sis_ConfigurarLimpieza] @Esquema=N'dbo', @Tabla=N'Gral_SAT_Colonia', @DiasRetencion=365, @TamanoLote=500, @Habilitada=0; + EXEC [dbo].[Sis_ConfigurarLimpieza] @Esquema=N'dbo', @Tabla=N'Gral_SAT_Estado', @DiasRetencion=365, @TamanoLote=500, @Habilitada=0; + EXEC [dbo].[Sis_ConfigurarLimpieza] @Esquema=N'dbo', @Tabla=N'Gral_SAT_Pais', @DiasRetencion=365, @TamanoLote=500, @Habilitada=0; +END; +GO diff --git a/SIG.Web/AppCode/Configuration/AppServices.cs b/SIG.Web/AppCode/Configuration/AppServices.cs index 52083bb..685239f 100644 --- a/SIG.Web/AppCode/Configuration/AppServices.cs +++ b/SIG.Web/AppCode/Configuration/AppServices.cs @@ -1,3 +1,5 @@ +using System; +using System.Configuration; using SIG.Web.AppCode.Contracts.Database; using SIG.Web.AppCode.Contracts.Search; using SIG.Web.AppCode.Contracts.Security; @@ -17,6 +19,7 @@ namespace SIG.Web.AppCode.Configuration public static IDbConnectionFactory DbConnectionFactory { get; private set; } public static IDbExecutor DbExecutor { get; private set; } public static IPasswordHasher PasswordHasher { get; private set; } + public static ICaptchaService CaptchaService { get; private set; } public static IUserRepository UserRepository { get; private set; } public static IUserAdministrationRepository UserAdministrationRepository { get; private set; } public static IPublicRegistrationService PublicRegistrationService { get; private set; } @@ -34,6 +37,10 @@ namespace SIG.Web.AppCode.Configuration DbExecutor = new DbExecutor(DbConnectionFactory); PasswordHasher = new PasswordHasher(); + CaptchaService = new RecaptchaV2Service( + ReadBooleanSetting("RecaptchaEnabled", "SIG_RECAPTCHA_ENABLED", false), + ReadSetting("RecaptchaSiteKey", "SIG_RECAPTCHA_SITE_KEY"), + ReadSetting("RecaptchaSecretKey", "SIG_RECAPTCHA_SECRET_KEY")); UserRepository = new UserRepository(DbExecutor); UserAdministrationRepository = new UserAdministrationRepository(DbExecutor); @@ -55,5 +62,21 @@ namespace SIG.Web.AppCode.Configuration UserAdministrationRepository, PasswordHasher, new SmtpEmailSender()); - } } + } + + private static string ReadSetting(string appSettingName, string environmentVariableName) + { + string environmentValue = Environment.GetEnvironmentVariable(environmentVariableName); + return string.IsNullOrWhiteSpace(environmentValue) + ? ConfigurationManager.AppSettings[appSettingName] + : environmentValue; + } + private static bool ReadBooleanSetting(string appSettingName, string environmentVariableName, bool defaultValue) + { + bool value; + string configuredValue = ReadSetting(appSettingName, environmentVariableName); + return bool.TryParse(configuredValue, out value) ? value : defaultValue; + } + + } } diff --git a/SIG.Web/AppCode/Contracts/Security/ICaptchaService.cs b/SIG.Web/AppCode/Contracts/Security/ICaptchaService.cs new file mode 100644 index 0000000..4bb58fc --- /dev/null +++ b/SIG.Web/AppCode/Contracts/Security/ICaptchaService.cs @@ -0,0 +1,12 @@ +using SIG.Web.AppCode.Models.Common; + +namespace SIG.Web.AppCode.Contracts.Security +{ + public interface ICaptchaService + { + bool IsEnabled { get; } + bool IsConfigured { get; } + string SiteKey { get; } + DbResult Verify(string responseToken, string remoteIp, string expectedHostName); + } +} diff --git a/SIG.Web/AppCode/Contracts/Security/IPublicRegistrationService.cs b/SIG.Web/AppCode/Contracts/Security/IPublicRegistrationService.cs index 4d19d3b..6aede80 100644 --- a/SIG.Web/AppCode/Contracts/Security/IPublicRegistrationService.cs +++ b/SIG.Web/AppCode/Contracts/Security/IPublicRegistrationService.cs @@ -8,6 +8,7 @@ namespace SIG.Web.AppCode.Contracts.Security { DbResult Validate(PublicRegistrationDraft draft, string domainName); DbResult> ListStates(string domainName); + DbResult GetPostalCode(string postalCode, string domainName); DbResult Register(PublicRegistrationDraft draft, string verificationBaseUrl, string domainName); DbResult Verify(string token, string domainName); } diff --git a/SIG.Web/AppCode/Contracts/Security/IUserAdministrationRepository.cs b/SIG.Web/AppCode/Contracts/Security/IUserAdministrationRepository.cs index 358bc49..53b9c96 100644 --- a/SIG.Web/AppCode/Contracts/Security/IUserAdministrationRepository.cs +++ b/SIG.Web/AppCode/Contracts/Security/IUserAdministrationRepository.cs @@ -10,6 +10,7 @@ namespace SIG.Web.AppCode.Contracts.Security DbResult GetUser(int userId, int executingUser, string domainName); DbResult> ListRoles(int executingUser, string domainName); DbResult> ListStates(string domainName); + DbResult GetPostalCode(string postalCode, string domainName); DbResult SaveUser(UserAdministrationModel user, string passwordHash, int executingUser, string domainName); DbResult DeleteUser(int userId, int executingUser, string domainName); DbResult> ListUserClients(int userId, int executingUser, string domainName); diff --git a/SIG.Web/AppCode/Models/Security/UserAdministrationModels.cs b/SIG.Web/AppCode/Models/Security/UserAdministrationModels.cs index 1343dc1..3bcbbea 100644 --- a/SIG.Web/AppCode/Models/Security/UserAdministrationModels.cs +++ b/SIG.Web/AppCode/Models/Security/UserAdministrationModels.cs @@ -20,6 +20,31 @@ namespace SIG.Web.AppCode.Models.Security public string Pais { get; set; } public string CP { get; set; } public string Telefono { get; set; } + public int CvePais { get; set; } + public int CveMunicipio { get; set; } + public int CveLocalidad { get; set; } + public int CveColonia { get; set; } + } + + public sealed class PostalCodeLookupModel + { + public PostalCodeLookupModel() { Colonias = new System.Collections.Generic.List(); } + public int CvePais { get; set; } + public string Pais { get; set; } + public int CveEstado { get; set; } + public string Estado { get; set; } + public int CveMunicipio { get; set; } + public string Municipio { get; set; } + public int CveLocalidad { get; set; } + public string Localidad { get; set; } + public string Ciudad { get; set; } + public System.Collections.Generic.List Colonias { get; set; } + } + + public sealed class NeighborhoodOptionModel + { + public int CveColonia { get; set; } + public string NombreAsentamiento { get; set; } } public sealed class UserAdministrationModel diff --git a/SIG.Web/AppCode/Repositories/Security/UserAdministrationRepository.cs b/SIG.Web/AppCode/Repositories/Security/UserAdministrationRepository.cs index ae8d0b3..f32e11f 100644 --- a/SIG.Web/AppCode/Repositories/Security/UserAdministrationRepository.cs +++ b/SIG.Web/AppCode/Repositories/Security/UserAdministrationRepository.cs @@ -26,6 +26,29 @@ namespace SIG.Web.AppCode.Repositories.Security public DbResult> ListStates(string domainName) { return QueryList("dbo.PA_Gral_EstadoList", 0, domainName, null); } + public DbResult GetPostalCode(string postalCode, string domainName) + { + return _db.Execute(domainName, connection => + { + using (IDbCommand command = connection.CreateCommand()) + { + StoredProcedureCommand.Configure(command, "dbo.PA_Gral_CodigoPostalGet", 0); + StoredProcedureCommand.AddInput(command, "@CP", postalCode, DbType.String); + + PostalCodeLookupModel value; + using (IDataReader reader = command.ExecuteReader()) + { + value = reader.ToObject(); + if (value != null && reader.NextResult()) + value.Colonias = reader.ToList(); + } + + StoredProcedureCommand.EnsureSuccess(command); + return value; + } + }); + } + public DbResult SaveUser(UserAdministrationModel user, string passwordHash, int executingUser, string domainName) { return _db.Execute(domainName, connection => @@ -94,6 +117,10 @@ namespace SIG.Web.AppCode.Repositories.Security StoredProcedureCommand.AddInput(c, "@Pais", draft.Pais, DbType.String); StoredProcedureCommand.AddInput(c, "@CP", draft.CP, DbType.String); StoredProcedureCommand.AddInput(c, "@Telefono", draft.Telefono, DbType.String); + StoredProcedureCommand.AddInput(c, "@CvePais", draft.CvePais, DbType.Int32); + StoredProcedureCommand.AddInput(c, "@CveMunicipio", draft.CveMunicipio, DbType.Int32); + StoredProcedureCommand.AddInput(c, "@CveLocalidad", draft.CveLocalidad <= 0 ? (object)null : draft.CveLocalidad, DbType.Int32); + StoredProcedureCommand.AddInput(c, "@CveColonia", draft.CveColonia, DbType.Int32); StoredProcedureCommand.AddInput(c, "@HashToken", tokenHash, DbType.StringFixedLength); StoredProcedureCommand.AddInput(c, "@HorasExpiracion", 24, DbType.Int32); }); diff --git a/SIG.Web/AppCode/Services/Security/PublicRegistrationService.cs b/SIG.Web/AppCode/Services/Security/PublicRegistrationService.cs index 7e53271..33984cf 100644 --- a/SIG.Web/AppCode/Services/Security/PublicRegistrationService.cs +++ b/SIG.Web/AppCode/Services/Security/PublicRegistrationService.cs @@ -22,6 +22,7 @@ namespace SIG.Web.AppCode.Services.Security } public DbResult> ListStates(string domainName) { return _repository.ListStates(domainName); } + public DbResult GetPostalCode(string postalCode, string domainName) { return _repository.GetPostalCode(postalCode, domainName); } public DbResult Register(PublicRegistrationDraft draft, string verificationBaseUrl, string domainName) { @@ -48,7 +49,7 @@ namespace SIG.Web.AppCode.Services.Security if (d == null) return "Captura los datos del registro."; if (string.IsNullOrWhiteSpace(d.NombreUsuario) || string.IsNullOrWhiteSpace(d.CorreoElectronico) || string.IsNullOrWhiteSpace(d.Contrasena)) return "Completa los datos de usuario."; if (d.Contrasena.Length < 8) return "La contraseña debe tener al menos 8 caracteres."; - if (string.IsNullOrWhiteSpace(d.RFC) || string.IsNullOrWhiteSpace(d.RazonSocial) || string.IsNullOrWhiteSpace(d.Direccion) || string.IsNullOrWhiteSpace(d.Colonia) || string.IsNullOrWhiteSpace(d.Ciudad) || d.CveSATEstado <= 0) return "Completa los datos obligatorios de la empresa."; + if (string.IsNullOrWhiteSpace(d.RFC) || string.IsNullOrWhiteSpace(d.RazonSocial) || string.IsNullOrWhiteSpace(d.Direccion) || string.IsNullOrWhiteSpace(d.Colonia) || string.IsNullOrWhiteSpace(d.Ciudad) || d.CveSATEstado <= 0 || d.CvePais <= 0 || d.CveMunicipio <= 0 || d.CveColonia <= 0) return "Completa y valida los datos obligatorios de la empresa y su código postal."; return null; } diff --git a/SIG.Web/AppCode/Services/Security/RecaptchaV2Service.cs b/SIG.Web/AppCode/Services/Security/RecaptchaV2Service.cs new file mode 100644 index 0000000..327bed9 --- /dev/null +++ b/SIG.Web/AppCode/Services/Security/RecaptchaV2Service.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using Newtonsoft.Json; +using SIG.Web.AppCode.Contracts.Security; +using SIG.Web.AppCode.Models.Common; + +namespace SIG.Web.AppCode.Services.Security +{ + public sealed class RecaptchaV2Service : ICaptchaService + { + private const string VerifyUrl = "https://www.google.com/recaptcha/api/siteverify"; + private readonly bool _enabled; + private readonly string _secretKey; + + public RecaptchaV2Service(bool enabled, string siteKey, string secretKey) + { + _enabled = enabled; + SiteKey = (siteKey ?? string.Empty).Trim(); + _secretKey = (secretKey ?? string.Empty).Trim(); + } + + public bool IsEnabled { get { return _enabled; } } + public bool IsConfigured { get { return SiteKey.Length > 0 && _secretKey.Length > 0; } } + public string SiteKey { get; private set; } + + public DbResult Verify(string responseToken, string remoteIp, string expectedHostName) + { + if (!IsEnabled) + return DbResult.Ok(true); + + if (!IsConfigured) + return DbResult.Fail("La validación No soy un robot todavía no está configurada. Contacta a Sistemas.", "Faltan RecaptchaSiteKey o RecaptchaSecretKey.", null); + + if (string.IsNullOrWhiteSpace(responseToken)) + return DbResult.Fail("Confirma que no eres un robot antes de crear la cuenta.", "Token reCAPTCHA vacío.", null); + + try + { + string form = "secret=" + Uri.EscapeDataString(_secretKey) + + "&response=" + Uri.EscapeDataString(responseToken); + if (!string.IsNullOrWhiteSpace(remoteIp)) + form += "&remoteip=" + Uri.EscapeDataString(remoteIp); + + byte[] body = Encoding.UTF8.GetBytes(form); + var request = (HttpWebRequest)WebRequest.Create(VerifyUrl); + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; + request.ContentLength = body.Length; + request.Timeout = 10000; + request.ReadWriteTimeout = 10000; + + using (Stream stream = request.GetRequestStream()) stream.Write(body, 0, body.Length); + + string json; + using (var response = (HttpWebResponse)request.GetResponse()) + using (var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) + json = reader.ReadToEnd(); + + VerificationResponse verification = JsonConvert.DeserializeObject(json); + if (verification == null || !verification.Success) + return DbResult.Fail("No fue posible validar que no eres un robot. Inténtalo nuevamente.", "reCAPTCHA rechazado: " + JoinCodes(verification), null); + + if (!string.IsNullOrWhiteSpace(expectedHostName) + && !string.Equals(expectedHostName, verification.HostName, StringComparison.OrdinalIgnoreCase)) + return DbResult.Fail("La validación No soy un robot no corresponde a este sitio. Inténtalo nuevamente.", "Hostname reCAPTCHA inesperado.", null); + + return DbResult.Ok(true); + } + catch (Exception ex) + { + return DbResult.Fail("El servicio No soy un robot no está disponible en este momento. Inténtalo nuevamente.", ex.Message, ex); + } + } + + private static string JoinCodes(VerificationResponse response) + { + return response == null || response.ErrorCodes == null ? "respuesta inválida" : string.Join(",", response.ErrorCodes); + } + + private sealed class VerificationResponse + { + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("hostname")] + public string HostName { get; set; } + + [JsonProperty("error-codes")] + public List ErrorCodes { get; set; } + } + } +} diff --git a/SIG.Web/Login.aspx b/SIG.Web/Login.aspx index 8341ac1..0deb57e 100644 --- a/SIG.Web/Login.aspx +++ b/SIG.Web/Login.aspx @@ -1,4 +1,4 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="SIG.Web.Login" %> +<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="SIG.Web.Login" CodePage="65001" %> @@ -155,16 +155,24 @@ } .login-card-icon { + min-height: 92px; margin-bottom: 14px; + display: flex; + align-items: center; + justify-content: center; + gap: 30px; } .login-card-icon img { - width: 148px; max-width: 100%; height: auto; - display: inline-block; + display: block; + object-fit: contain; } + .login-card-icon .water-logo { width: 165px; } + .login-card-icon .developer-logo { width: 78px; } + .login-card-title { margin: 0; font-size: 34px; @@ -214,11 +222,18 @@ .input-icon > i { position: absolute; left: 17px; - top: 50%; - transform: translateY(-50%); + top: 0; + bottom: 0; + width: 22px; + display: flex; + align-items: center; + justify-content: center; + margin: 0; color: #7a8ca9; font-size: 20px; + line-height: 1; z-index: 2; + pointer-events: none; } .input-icon .form-control { @@ -258,36 +273,41 @@ } .login-options { - margin-top: 18px; + margin-top: 20px; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 14px 22px; + color: #173b70; + font-size: 14px; + } + + .login-links { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; - color: #173b70; - font-size: 15px; + justify-content: flex-end; + gap: 22px; } .login-options a { color: #0c63c7; text-decoration: none; font-weight: 600; + line-height: 1.35; + text-align: right; } - .login-options a:hover { - text-decoration: underline; - } + .login-options a:hover { text-decoration: underline; } - .remember-label { + .remember-check { display: inline-flex; align-items: center; - gap: 8px; - margin: 0; + white-space: nowrap; font-weight: 500; } - .remember-label input { - margin: 0; - } + .remember-check input { margin: 0 7px 0 0; vertical-align: middle; } + .remember-check label { margin: 0; font-weight: 500; cursor: pointer; } .login-footer-note { position: fixed; @@ -361,10 +381,9 @@ font-size: 28px; } - .login-options { - flex-direction: column; - align-items: flex-start; - } + .login-options { grid-template-columns: 1fr; align-items: stretch; } + .login-links { justify-content: space-between; gap: 14px; } + .login-options a { text-align: left; } .login-footer-note { position: static; @@ -401,8 +420,8 @@