Initial commit

This commit is contained in:
Jorge Luis caudillo esparza
2026-07-14 11:15:20 -07:00
commit 6685ef63fb
7326 changed files with 1020873 additions and 0 deletions

37
.gitattributes vendored Normal file
View File

@@ -0,0 +1,37 @@
# Texto del proyecto
* text=auto
*.cs text eol=crlf
*.aspx text eol=crlf
*.ascx text eol=crlf
*.master text eol=crlf
*.config text eol=crlf
*.csproj text eol=crlf
*.sln text eol=crlf
*.sql text eol=crlf
*.ps1 text eol=crlf
*.cmd text eol=crlf
*.bat text eol=crlf
*.md text eol=lf
*.js text eol=lf
*.css text eol=lf
*.json text eol=lf
*.xml text eol=crlf
# Binarios: nunca normalizar saltos de línea
*.pdf binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.bmp binary
*.webp binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.otf binary
*.zip binary
*.dll binary
*.pdb binary
*.exe binary

38
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,38 @@
# AGENTS.md
## Project
This is an ASP.NET WebForms project in C# targeting .NET Framework 4.8.
## Core rules
- Do not add VB.NET code.
- Do not convert WebForms pages to ASP.NET Core.
- Keep pages as .aspx, .ascx, .master, and C# code-behind.
- Keep the copied `Styles`, `Scripts`, and `Metronic` folders unchanged unless the user explicitly asks for a cleanup.
- Do not connect WebForms pages directly to database classes.
- Pages must use contracts and services from `AppCode`.
- Use `SIG` as the prefix for new controls/classes.
- Do not use `SITIC` in new examples, namespaces, or file names.
- `Site.master` must keep ContentPlaceHolder IDs short: `H`, `C`, `J`.
- Do not restore the hidden searcher in the header.
- Keep the header actions button.
- User data must come from `IUserContextProvider`.
- Menu data must come from `IMenuProvider`.
- Login must use `ILoginService`.
- Search controls must use `IBuscadorCatalogoDataProvider`.
- `ClsPaginaBase` must only handle session and permissions.
- Database access must go through `IDomainConnectionResolver` and `IDbConnectionFactory`.
- Notification channels must go through the notification center contracts.
## Folder rules
- Configuration files go under `AppCode/Configuration`.
- Session/user/menu contracts go under `AppCode/Session`.
- Permission-related code goes under `AppCode/Security`.
- Business process folders go under `AppCode/Modules` later.
- Do not create process folders such as Sales/Purchases/Inventory until the user asks.
## Coding style
- Prefer small contracts and small services.
- Avoid large static helper classes.
- Avoid putting SQL in pages or controls.
- Prefer explicit null checks and clear error messages.

74
.gitignore vendored Normal file
View File

@@ -0,0 +1,74 @@
# Visual Studio
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
*.VC.db
*.VC.VC.opendb
# Compilación .NET Framework / ASP.NET
[Bb]in/
[Oo]bj/
[Dd]ebug/
[Rr]elease/
x64/
x86/
TestResults/
artifacts/
# NuGet (se restauran desde el proyecto)
packages/
*.nupkg
*.snupkg
# Publicación
publish/
PublishProfiles/
*.Publish.xml
*.pubxml.user
# Archivos locales de IIS / WebForms
*.csproj.user
*.sln.cache
IISExpress/
# Logs y datos temporales de la aplicación
**/Log/*
**/log/*
!**/Log/.gitkeep
!**/log/.gitkeep
SIG.Web/App_Data/*.mdf
SIG.Web/App_Data/*.ldf
SIG.Web/App_Data/*.log
**/App_Data/log.txt
SIG.Web/App_Data/Temp/
SIG.Web/App_Data/Uploads/
# Herramientas y trabajo local de Codex
.codex-work/
.agents/
.codex_patch_probe.txt
# Sistema operativo
Thumbs.db
Desktop.ini
.DS_Store
# Temporales y respaldos
*.tmp
*.temp
*.bak
*.orig
*.rej
*~
# Cobertura y pruebas
coverage/
*.coverage
*.coveragexml
# Secretos locales opcionales
*.secrets.json
Web.local.config
appsettings.local.json

768
AGENTS.md Normal file
View File

@@ -0,0 +1,768 @@
# AGENTS.md
## Project identity
This project is **Plataforma de Desarrolladores Agua Hillo**.
It is an **ASP.NET WebForms** application using **C#** and **.NET Framework 4.8 or 4.8.1**.
Do **not** convert this project to:
- ASP.NET Core
- Razor Pages
- MVC Core
- Blazor
- Minimal APIs
- Angular-only app
- Web API-only app
ASP.NET WebForms must stay on .NET Framework. If a command, package, or template requires ASP.NET Core, do not use it for this project.
---
## Main goal
Create a clean WebForms starter project from zero, keeping the same Metronic visual style and only the necessary bundle methods used by the master page and login page.
The project must use a contract/service structure so pages, master page, login, menu, user session, and future modules do not connect directly to database classes.
Target architecture:
```text
ASPX / Master Page / User Controls
↓
Contracts
↓
Services
↓
Repositories / SQL helpers
↓
Database
```
---
## Language rules
- All new code must be **C#**.
- Do not add VB.NET files.
- Do not add `.vb` files to the active project.
- Old VB files may exist only as migration reference files using `.vb.txt`.
- Do not use VB comment syntax in C# files.
- Do not use JavaScript/CSS/Powershell comments as C# comments.
---
## Command rules
When giving commands for this project:
- Prefer **PowerShell**, **Developer PowerShell**, or **Visual Studio Package Manager Console** commands.
- Commands must be written in **one line**.
- Do not use PowerShell line continuation with backticks.
- Do not suggest `dotnet new webforms` because WebForms is not supported by the .NET CLI as an ASP.NET Core project.
- If a WebForms project must be created from command line, use Visual Studio/MSBuild-compatible project files or create the project through Visual Studio using **ASP.NET Web Application (.NET Framework)**.
- Target framework must be `.NET Framework 4.8` or `.NET Framework 4.8.1`.
Recommended project creation path:
```powershell
mkdir "C:\Proyectos\Agua de hermosillo\Plataforma de Desarrolladores"
```
```powershell
cd "C:\Proyectos\Agua de hermosillo\Plataforma de Desarrolladores"
```
Then create/open the WebForms project in Visual Studio 2022 as:
```text
ASP.NET Web Application (.NET Framework) → Web Forms → C# → .NET Framework 4.8/4.8.1
```
---
## Project folder rules
Use this base structure:
```text
SIG.Web/
AppCode/
Infrastructure/
Sql/
Repositories/
Security/
Utilities/
Contracts/
Models/
Services/
Styles/
Scripts/
Metronic/
Site.master
Site.master.cs
Login.aspx
Login.aspx.cs
Inicio.aspx
Inicio.aspx.cs
BundleConfig.cs
Global.asax
Global.asax.cs
Web.config
```
Rules:
- Do not place database access directly inside `.aspx.cs`, `.ascx.cs`, or `Site.master.cs`.
- Database-related helpers go under `App_Code/Sql` or a clearly named SQL/data folder.
- Business access goes through `Contracts` and `Services`.
- If the project file needs explicit compilation entries, include C# files recursively, for example `App_Code\**\*.cs`, but avoid duplicate compile entries that include the same file twice.
---
## Asset folder rules
The following folders are legacy visual dependencies and must be copied as complete folders:
```text
Styles/
Scripts/
Metronic/
```
Do not replace Metronic with Bootstrap-only layout.
Do not upgrade Metronic unless the user explicitly asks.
Do not remove old scripts just because they look unused; only remove references from bundles when they are not used by the master/login flow.
---
## Bundle rules
The project must implement only the bundle methods needed by the current `Site.master` and login page.
Do not bring every old bundle method from the legacy project.
Do not add unused bundle groups just because they existed before.
Do not add duplicate bundle keys.
Do not add empty bundle methods.
Allowed current bundle keys are the ones actually rendered by master/login, especially:
```text
~/bundles/modernizr
~/bundles/scripts
~/bundles/highcharts
~/Metronic/assets/global/scripts/JSV1
~/Metronic/assets/global/css/cssV1
```
WebForms/AjaxControlToolkit support bundles are allowed only when referenced by `ScriptManager` or required by WebForms controls:
```text
~/bundles/jquery
~/bundles/WebFormsJs
~/bundles/MsAjaxJs
~/Content/AjaxControlToolkit/Styles/Bundle
```
Rules:
- If `Site.master` or `Login.aspx` does not render a bundle, do not add it.
- If a bundle exists only for old pages not migrated yet, leave it out until the page needs it.
- Prefer preserving original asset paths from `Styles`, `Scripts`, and `Metronic`.
- Use `CssRewriteUrlTransform` for CSS bundles that reference fonts, images, or relative URLs.
- Do not register CSS/JS from server controls when the style/script is global; use `BundleConfig` and the master page.
---
## WebForms page rules
All normal `.aspx` pages must use `Site.master`, except the login page.
Normal pages:
- Must include `MasterPageFile="~/Site.master"`.
- Must not include `<html>`, `<head>`, `<body>`, or `<form>`.
- Must use `<asp:Content>` controls.
- Should inherit from `ClsPaginaBase` or the current C# page base class.
- Must not connect directly to SQL classes.
- Must call contracts/services for data, authentication, permissions, catalogs, menus, and user context.
Public registration exception:
- `Pages/Security/RegistroCliente.aspx`, `RegistroPendiente.aspx` and `VerificarCorreo.aspx` may be standalone public pages with their own `form runat="server"`.
- `RegistroCliente.aspx` keeps its two-step draft in encrypted ViewState and persists only after all client/company validations pass.
- All persistence continues through contracts/services/repositories and stored procedures.
Login page:
- Is the only page allowed to have its own full HTML structure.
- May inherit from `System.Web.UI.Page` or its own login base class.
- Must not inherit from `ClsPaginaBase` if that base class requires an authenticated session.
- Must use `IAuthContract` or the current auth contract.
- Must create a dummy session until real authentication is implemented.
- Must not hardcode real database login logic in `Login.aspx.cs`.
Important naming rule:
- Do not create both `Login.aspx` and `LogIn.aspx`.
- If the starter already uses `LogIn.aspx`, keep that name and keep `Web.config` consistent.
- If creating from scratch, prefer `Login.aspx`, but update `Web.config`, redirects, and links consistently.
---
## Master page rules
`Site.master` owns the main HTML structure.
Rules:
- `Site.master` owns the only main `<form runat="server">`.
- `Site.master` must keep the Metronic layout structure.
- Keep the sidebar/header/page-content structure similar to the original Metronic project.
- Do not restore the hidden search box from the old header.
- Keep the actions button if it exists in the selected master layout.
- Do not hardcode real user/menu database logic inside `Site.master.cs`.
- User data must come from a contract/service.
- Menu data must come from a contract/service.
- Breadcrumb/title/layout data must come from a contract/service when dynamic.
- Do not use `<title id="Titulo" runat="server">`.
- Prefer `<title><%: Page.Title %></title>`.
- If a generated starter currently uses a title literal, replace it with `Page.Title` unless there is a specific reason to keep the literal.
Current preferred contracts:
```text
IAuthContract
IUserSessionContract
IMenuContract
IMasterPageContract
```
If old names such as `IUserContextProvider` or `IMenuProvider` exist, migrate them to the current contract naming or create adapters. Do not keep two competing naming systems.
---
## ContentPlaceHolder rules
`Site.master` must use short placeholder IDs.
Preferred final placeholder IDs:
```text
H = page-specific head/CSS content
C = main page content
J = page-specific JavaScript
```
Example in `Site.master`:
```aspx
<asp:ContentPlaceHolder ID="H" runat="server" />
<asp:ContentPlaceHolder ID="C" runat="server" />
<asp:ContentPlaceHolder ID="J" runat="server" />
```
Example in a normal page:
```aspx
<asp:Content ID="ContentHead" ContentPlaceHolderID="H" runat="server">
</asp:Content>
<asp:Content ID="ContentMain" ContentPlaceHolderID="C" runat="server">
</asp:Content>
<asp:Content ID="ContentScripts" ContentPlaceHolderID="J" runat="server">
</asp:Content>
```
If the generated starter currently has `HeadContent`, `MainContent`, or `ScriptsContent`, rename them to `H`, `C`, and `J` before adding more pages.
Do not mix placeholder naming systems in the same project.
---
## Login and session rules
Authentication must go through a contract.
Required flow:
```text
Login.aspx.cs
↓
IAuthContract
↓
DummyAuthContract now, real AuthService later
↓
IUserSessionContract
↓
Session user
```
Initial login may be dummy.
Dummy session user should be safe and clearly fake, for example:
```text
UserId: 15
CompanyId: 1
RoleId: 1
RoleName: Administrador
CompanyName: Empresa Demo
DisplayName: Usuario Demo
```
Rules:
- Dummy login may accept any non-empty username and password during starter setup.
- Mark dummy authentication clearly in code comments.
- Do not mix real SQL authentication with dummy authentication in the same class.
- When real login is implemented, create a new service such as `SqlAuthContract` or `AuthService`.
- Do not store passwords in session.
- Do not log raw passwords.
- Use Forms Authentication if staying with classic WebForms auth.
---
## Contract rules
Contracts belong in `/Contracts`.
Current minimum contracts:
```text
Contracts/IAuthContract.cs
Contracts/IUserSessionContract.cs
Contracts/IMenuContract.cs
Contracts/IMasterPageContract.cs
```
Suggested future contracts:
```text
Contracts/IPermissionContract.cs
Contracts/INotificationContract.cs
Contracts/ICatalogSearchContract.cs
Contracts/IAuditContract.cs
Contracts/ICurrentCompanyContract.cs
```
Rules:
- Pages call contracts, not SQL classes.
- Master page calls contracts, not SQL classes.
- Login calls contracts, not SQL classes.
- Contracts must return models, DTOs, or result objects, not raw `DataTable` unless migrating legacy code.
- If legacy `DataTable` is required, isolate it behind a contract and document why.
---
## Service rules
Services belong in `/Services`.
Current starter services may include:
```text
Services/ContractRegistry.cs
Services/DummyAuthContract.cs
Services/UserSessionContract.cs
Services/DummyMenuContract.cs
Services/MasterPageContract.cs
```
Rules:
- `ContractRegistry` may be used as a simple manual dependency resolver for WebForms.
- Do not introduce a complex dependency injection framework unless required.
- Services may depend on SQL helpers/repositories.
- `.aspx.cs` files should be thin and delegate work to services.
- Keep dummy services separate from SQL-backed services.
---
## Model rules
Models belong in `/Models`.
Current starter models may include:
```text
Models/AppUser.cs
Models/LoginResult.cs
Models/MenuItemModel.cs
Models/MasterLayoutModel.cs
Models/BreadcrumbItem.cs
```
Rules:
- Use models to move data between contracts and WebForms pages.
- Do not pass anonymous objects across layers.
- Do not expose SQL connection classes or `SqlDataReader` from services to pages.
- Keep models simple and serializable when possible.
---
## Database and SQL structure rules
All database connection structure must be centralized under `App_Code/Sql` or an equivalent SQL/data folder.
Current starter SQL structure:
```text
App_Code/Sql/ConnectionNames.cs
App_Code/Sql/IConnectionStringProvider.cs
App_Code/Sql/WebConfigConnectionStringProvider.cs
App_Code/Sql/SqlConnectionFactory.cs
App_Code/Sql/SqlQueryExecutor.cs
```
Rules:
- Define connection names in one place.
- Read connection strings from `Web.config`.
- Do not hardcode connection strings in pages, master, controls, or services.
- Do not create `SqlConnection` directly in `.aspx.cs`.
- Do not create `SqlCommand` directly in `.aspx.cs`.
- Use repositories or SQL executor classes behind contracts.
- All application database reads and writes must use stored procedures; inline SQL in C# is prohibited.
- Mandatory: read and follow `AGENTS_STORED_PROCEDURES.md` for naming, output parameters, TRY/CATCH, logging, transactions, and logical deletion.
- Mandatory: read and follow `AGENTS_DATA_READER_MAPPING.md` for object, list, `DataTable`, and `JArray` result mapping.
- Mandatory: read and follow `AGENTS_DATABASE_ERROR_LOGGING.md` for safe parameter logging and reproducible stored-procedure calls.
- Mandatory: read and follow `AGENTS_PAGE_STRUCTURE.md` for the `Pages/Modulo/` location of all new WebForms pages.
- Mandatory: read and follow `AGENTS_WEBFORMS_PARTIAL_POSTBACKS.md` for the single master form, ScriptManager ownership, UpdatePanel usage, and partial-postback rules.
- Mandatory: read and follow `AGENTS_JAVASCRIPT_INITIALIZATION.md` for per-element initialization flags, global event guards, and idempotent control setup after partial postbacks.
- Keep stored procedure names centralized.
- Repositories must use `CommandType.StoredProcedure` and parameterized inputs/outputs.
- Never concatenate raw user input into SQL.
Recommended Web.config connection string pattern:
```xml
<connectionStrings>
<add name="DefaultConnection" connectionString="..." providerName="System.Data.SqlClient" />
<add name="SecurityConnection" connectionString="..." providerName="System.Data.SqlClient" />
<add name="ReportingConnection" connectionString="..." providerName="System.Data.SqlClient" />
</connectionStrings>
```
---
## Base page rules
Normal pages should inherit from a C# base page.
Preferred base class:
```text
App_Code/Infrastructure/ClsPaginaBase.cs
```
If the starter currently has `WebFormsPageBase.cs`, either:
- rename it to `ClsPaginaBase.cs`, or
- keep `WebFormsPageBase` and update this AGENTS.md/project rules consistently.
Do not keep both base classes doing the same job.
Base page responsibilities:
- Validate authenticated session.
- Expose current user from `IUserSessionContract`.
- Provide helper methods for messages, redirects, and permissions.
- Avoid database access.
- Avoid page-specific business logic.
---
## Global.asax rules
`Global.asax` / `Global.asax.cs` should:
- Register bundles through `BundleConfig.RegisterBundles(BundleTable.Bundles)`.
- Configure basic application startup.
- Avoid business logic.
- Avoid direct database startup queries unless absolutely required.
If OWIN is not used, prevent startup errors by adding this to `Web.config`:
```xml
<add key="owin:AutomaticAppStartup" value="false" />
```
Do not add OWIN packages unless the project actually needs OWIN.
---
## Web.config rules
`Web.config` must stay compatible with ASP.NET WebForms on .NET Framework 4.8/4.8.1.
Rules:
- Use Forms Authentication unless a different auth system is explicitly requested.
- Allow anonymous access only to the login page and required static assets.
- Do not add ASP.NET Core middleware settings.
- Do not add unsupported `.NET Core` config sections.
- Do not add CodeDom provider entries unless the matching NuGet package is installed.
- If this error appears: `No se pudo ubicar el tipo de proveedor CodeDom Microsoft.CodeDom.Providers.DotNetCompilerPlatform...`, either install the exact package version or remove/fix the `system.codedom` section.
- Keep MIME mappings for fonts such as `.woff` and `.woff2` when Metronic requires them.
---
## OWIN rule
If the application shows this error:
```text
No assembly found containing an OwinStartupAttribute
```
And the project does not use OWIN, add this in `appSettings`:
```xml
<add key="owin:AutomaticAppStartup" value="false" />
```
Do not create an empty OWIN startup class just to silence the error unless OWIN is actually required.
---
## Site.master rendering rules
The master page should render bundles and scripts in this general order:
Inside `<head>`:
```aspx
<%: Scripts.Render("~/bundles/modernizr") %>
<%: Styles.Render("~/Content/AjaxControlToolkit/Styles/Bundle") %>
<%: Styles.Render("~/Metronic/assets/global/css/cssV1") %>
<asp:ContentPlaceHolder ID="H" runat="server" />
```
Inside `<form>` near the top:
```aspx
<asp:ScriptManager ID="TSM" runat="server" />
<%: Scripts.Render("~/bundles/scripts") %>
```
Before closing `</form>`:
```aspx
<%: Scripts.Render("~/Metronic/assets/global/scripts/JSV1") %>
<%: Scripts.Render("~/bundles/highcharts") %>
<asp:ContentPlaceHolder ID="J" runat="server" />
```
Rules:
- Do not render the same script twice.
- Do not render JQuery twice.
- Keep `ScriptManager` if AjaxControlToolkit, UpdatePanel, or WebForms AJAX is used.
- Keep `TSM_AsyncPostBackError` only if implemented in code-behind.
- Remove or implement any event handler referenced by markup.
---
## Header/menu rules
The master header must preserve the selected Metronic layout.
Rules:
- Keep user dropdown.
- Keep logout option.
- Keep actions button when present.
- Do not restore the hidden/old search box from legacy header.
- Sidebar menu must be built from `IMenuContract`.
- Do not build the menu from SQL inside `Site.master.cs`.
- Menu items should use `MenuItemModel` or an equivalent model.
- Encode text output when generating HTML menu strings.
---
## Page title and breadcrumb rules
Page title and breadcrumb should be handled consistently.
Rules:
- Use `Page.Title` for the browser title.
- Use a master/layout model for visible page title and breadcrumb.
- Do not hardcode global breadcrumb logic in every page.
- Normal pages may set page title in `Page_Load` or via properties.
Preferred model:
```text
MasterLayoutModel
PageTitle
Breadcrumbs
CurrentUser
MenuItems
```
---
## Security rules
- Do not store passwords in code, comments, Web.config examples, or session.
- Do not expose real connection strings in committed files.
- Use dummy values in examples.
- Use parameterized SQL.
- Validate uploaded files by extension, MIME type, and size when uploads are later added.
- Avoid writing raw exception details to the user in production.
- Keep `customErrors mode="Off"` only for local development.
---
## Error handling rules
- Handle null session user cleanly and redirect to login.
- Handle failed login with a friendly message.
- Handle async postback errors through `ScriptManager.AsyncPostBackError` only if the handler exists.
- Avoid infinite loading states in JavaScript controls.
- For AJAX/server search controls, use client and server timeouts.
- Recommended default search timeout: about 20 to 30 seconds unless the user asks otherwise.
---
## JavaScript and CSS rules
- Global scripts/styles must be registered in `BundleConfig` and rendered by master.
- Page-specific scripts go in placeholder `J`.
- Page-specific CSS/head content goes in placeholder `H`.
- Do not inline large JavaScript blocks in `.aspx` unless page-specific and small.
- Do not remove existing custom legacy files unless the user asks.
- Fix null `addEventListener` errors by checking that the element exists before attaching events.
- Avoid blocking Ctrl+V unless a specific feature requires it.
---
## User control rules
If server controls are migrated:
- Prefer C# classes under `App_Code/SigControls` when creating reusable server controls.
- Do not mix WebForms `.ascx` user controls with App_Code server-control classes unless there is a clear reason.
- Do not register global CSS/JS from the control when master bundles already include them.
- Controls should expose properties/events and avoid direct SQL access.
- Controls should call contracts/services when they need data.
---
## PDF viewer migration rules
If the PDF viewer is added to this project later:
- Implement it in C#.
- Prefer `App_Code/SigControls` for server controls.
- Do not add VB files.
- Do not use `/* */` comments in VB reference files; VB references must remain `.vb.txt` only.
- Keep global PDF viewer CSS/JS references in bundles.
- Do not register duplicate resources from the control and master at the same time.
- Session or temp-folder PDF storage must be explicit and centralized.
---
## Home page rules
Use only one active home page convention.
Allowed options:
```text
Default.aspx
Inicio.aspx
```
Rules:
- Do not keep both as competing entry pages.
- If using `Inicio.aspx`, redirect `Default.aspx` to `Inicio.aspx` or update Forms Authentication default URL.
- If using `Default.aspx`, keep Web.config and menu links pointing to `Default.aspx`.
- The home page must use `Site.master`.
---
## File delivery rules for this project
When generating a single downloadable file:
- If the file is `.js` or `.css` and it is not inside a ZIP, append `.txt` to the filename.
- Example: `custom.js.txt`, `styles.css.txt`.
- If the file is inside a ZIP, keep the real extension `.js` or `.css`.
- For project-level delivery, prefer ZIP files.
---
## What not to add yet
Do not add the following until the user explicitly asks:
- Real SQL login implementation
- Full permission system
- Notifications
- Web Push
- PDF viewer
- BuscadorCatalogo
- DropdownGrid
- PorcentajeBox
- Angular
- APIs
- Background jobs
- OWIN
- Entity Framework
- Dependency injection framework
- New Metronic version
The starter must stay small and focused: commands, master page, login, contracts, dummy user session, bundle structure, copied assets, and centralized SQL foundation.
---
## Current implementation checklist
When modifying or regenerating the project, verify:
- [ ] Project targets .NET Framework 4.8 or 4.8.1.
- [ ] All new code is C#.
- [ ] No active `.vb` files were added.
- [ ] `Styles`, `Scripts`, and `Metronic` were copied as-is.
- [ ] `BundleConfig.cs` contains only bundles rendered by master/login or required by WebForms AJAX.
- [ ] `Site.master` owns the only main form.
- [ ] Normal pages use `Site.master`.
- [ ] Normal pages do not contain full HTML structure.
- [ ] Login is the only standalone full HTML page.
- [ ] User/session data comes from `IUserSessionContract`.
- [ ] Login uses `IAuthContract`.
- [ ] Menu comes from `IMenuContract`.
- [ ] Master layout data comes from `IMasterPageContract`.
- [ ] SQL connection helpers are centralized under `App_Code/Sql`.
- [ ] Pages do not directly instantiate SQL connections.
- [ ] Placeholder IDs are `H`, `C`, and `J`.
- [ ] No hidden old search box was restored.
- [ ] Actions button is preserved when present in the selected layout.
- [ ] `Web.config` login URL matches the actual login page filename.
- [ ] No duplicate `Login.aspx` / `LogIn.aspx` pages exist.
- [ ] No OWIN startup error exists, or OWIN automatic startup is disabled.
- [ ] No missing CodeDom provider error exists.
---
## Preferred next evolution
After the starter works, the safest growth path is:
1. Replace dummy login with SQL-backed `IAuthContract`.
2. Replace dummy menu with SQL-backed `IMenuContract`.
3. Add permission contract.
4. Add base page authorization checks.
5. Add reusable controls one by one.
6. Add catalog search and UI helpers.
7. Add PDF viewer only after the base shell is stable.
Keep every new feature behind contracts so the WebForms UI remains thin and easier to migrate later if needed.

View File

@@ -0,0 +1,63 @@
# Reglas obligatorias para registrar errores de base de datos
Estas reglas complementan `AGENTS.md`, `AGENTS_STORED_PROCEDURES.md` y `AGENTS_DATA_READER_MAPPING.md`.
## Información requerida
Cuando falle una llamada a un procedimiento almacenado, el log técnico debe incluir:
- Nombre completo del procedimiento, incluyendo esquema.
- Nombre de cada parámetro.
- Valor recibido por cada parámetro de entrada.
- Tipo y valor disponible de los parámetros de salida.
- Mensaje y número de la excepción SQL.
- Un bloque `ReplaySql` que pueda copiarse en SQL Server para reproducir la llamada.
Formato mínimo:
```text
StoredProcedure: dbo.PA_Modulo_Accion
Parametros:
@CveProyecto=-1
@CvePeriodo=0
@EsSimulador=0
ReplaySql:
DECLARE @ErrorMostrar varchar(max);
DECLARE @ErrorTipo int;
DECLARE @Return bit;
EXEC [dbo].[PA_Modulo_Accion]
@CveProyecto = -1,
@CvePeriodo = 0,
@EsSimulador = 0,
@ErrorMostrar = @ErrorMostrar OUTPUT,
@ErrorTipo = @ErrorTipo OUTPUT,
@Return = @Return OUTPUT;
SELECT @ErrorMostrar, @ErrorTipo, @Return;
```
## Seguridad
- Nunca registrar valores de contraseñas, hashes, tokens, secretos, API keys ni cadenas de conexión.
- Sustituir esos valores con `[REDACTADO]` tanto en `Parametros` como en `ReplaySql`.
- El SQL reproducible con valores redactados requiere que el desarrollador proporcione manualmente un valor seguro de prueba.
- Escapar comillas simples y generar literales SQL válidos.
- Limitar textos extensos y marcar el valor como truncado.
- No mostrar `ReplaySql`, parámetros ni detalles técnicos al usuario final.
- Los datos técnicos sólo deben escribirse en el archivo log protegido de la aplicación.
## Implementación
- Configurar todos los comandos mediante `StoredProcedureCommand` para que `StoredProcedureDiagnostics` capture el contexto.
- `DbExecutor` debe adjuntar el diagnóstico al log en `SqlException`, timeout, error de salida del PA y excepciones inesperadas.
- Limpiar el contexto de diagnóstico en `finally` para impedir que parámetros de una petición aparezcan en otra.
- Usar `AsyncLocal` para aislar correctamente solicitudes concurrentes y operaciones asíncronas.
- Capturar los parámetros OUTPUT después de cerrar el `DataReader`.
- No establecer `SqlConnection.FireInfoMessageEventOnUserErrors = true`; esa opción transforma errores SQL de severidad 11–16 en eventos informativos y puede ocultar la excepción real.
- Los mensajes `RAISERROR` de severidad 10 pueden registrarse mediante `SqlConnection.InfoMessage` sin activar esa opción.
- Si `@Return`, `@ErrorTipo` y `@ErrorMostrar` regresan sin valor, registrar que la versión instalada del PA no devolvió el contrato requerido.
## Alcance
- Registrar parámetros detallados sólo cuando exista un error o advertencia que requiera diagnóstico.
- No registrar cada ejecución exitosa de manera predeterminada.
- No usar el log como auditoría de operaciones exitosas; la auditoría pertenece a su propio contrato o procedimiento.

View File

@@ -0,0 +1,100 @@
# Reglas obligatorias de mapeo de resultados
Estas reglas complementan `AGENTS.md` y `AGENTS_STORED_PROCEDURES.md`.
## Extensión estándar
- Todo resultado obtenido con `IDataReader` debe convertirse mediante `AppCode/Utilities/DataReaderExtensions.cs`.
- No repetir bloques manuales como `Propiedad = Convert.ToInt32(reader["Columna"])` en repositorios o servicios.
- Resultados admitidos:
```csharp
T item = reader.ToObject<T>();
List<T> items = reader.ToList<T>();
DataTable table = reader.ToDataTable();
JArray json = reader.ToJsonArray();
```
- `ToObject<T>()` devuelve `null` cuando el procedimiento no devuelve filas.
- `ToList<T>()` devuelve una lista vacía cuando no hay filas; no devuelve `null`.
- `ToDataTable()` devuelve un `DataTable` vacío cuando no hay filas.
- `ToJsonArray()` devuelve un `JArray` vacío cuando no hay filas.
## Operaciones asíncronas
- Para código nuevo, preferir las versiones asíncronas cuando el proveedor exponga `DbCommand` y `DbDataReader`.
- Métodos disponibles: `ToObjectAsync<T>()`, `ToListAsync<T>()`, `ToDataTableAsync()` y `ToJsonArrayAsync()`.
- Los métodos asíncronos deben recibir y propagar un `CancellationToken` cuando la llamada superior disponga de uno.
- Abrir el lector con `ExecuteReaderAsync`; no envolver operaciones síncronas de base de datos en `Task.Run`.
- Usar `ConfigureAwait(false)` dentro de infraestructura, repositorios y servicios que no necesiten el contexto de WebForms.
- Cerrar o desechar el lector antes de leer `@ErrorMostrar`, `@ErrorTipo` y `@Return`.
- Una página WebForms que espere directamente debe declarar `Async="true"` y usar `RegisterAsyncTask` o un evento asíncrono compatible.
- Mantener los métodos síncronos únicamente para flujos existentes que todavía no se hayan migrado de extremo a extremo.
Ejemplo:
```csharp
List<UsuarioModel> usuarios;
using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false))
{
usuarios = await reader.ToListAsync<UsuarioModel>(cancellationToken).ConfigureAwait(false);
}
StoredProcedureCommand.EnsureSuccess(command);
return usuarios;
```
## Convención de columnas y propiedades
- El mapeo se realiza por nombre de columna y propiedad sin distinguir mayúsculas/minúsculas.
- Los procedimientos deben usar alias para que las columnas coincidan con el modelo.
- Cuando no sea posible usar el mismo nombre, decorar la propiedad con `[Column("NombreColumna")]`.
- Las columnas adicionales se ignoran.
- Las propiedades sin columna conservan su valor predeterminado.
- `DBNull` se convierte en `null` para propiedades anulables o de referencia; para propiedades no anulables conserva el valor predeterminado.
- Se admiten tipos anulables, enumeraciones, `Guid`, `TimeSpan`, `DateTimeOffset`, booleanos y conversiones primitivas.
- Una conversión incompatible debe producir una excepción para que `DbExecutor` la registre; no ocultar errores de contrato entre el PA y el modelo.
## Uso correcto con procedimientos almacenados
- El repositorio crea y configura el comando mediante `StoredProcedureCommand`.
- El repositorio abre el lector dentro de `using`.
- Debe cerrar o desechar el lector antes de llamar `StoredProcedureCommand.EnsureSuccess(command)`, porque los parámetros OUTPUT se reciben al finalizar el lector.
- La extensión sólo mapea datos; no abre conexiones, no crea comandos, no captura excepciones y no decide mensajes para el usuario.
- Páginas ASPX, controles y master pages no deben manejar `IDataReader` directamente.
Ejemplo para un objeto:
```csharp
UserModel user;
using (IDataReader reader = command.ExecuteReader())
{
user = reader.ToObject<UserModel>();
}
StoredProcedureCommand.EnsureSuccess(command);
return user;
```
Ejemplo para una lista:
```csharp
List<MenuItemModel> items;
using (IDataReader reader = command.ExecuteReader())
{
items = reader.ToList<MenuItemModel>();
}
StoredProcedureCommand.EnsureSuccess(command);
return items;
```
## Elección del tipo de resultado
- Preferir modelos tipados para reglas de negocio, seguridad, usuarios, permisos, menús y módulos nuevos.
- Usar `DataTable` sólo para compatibilidad con componentes WebForms o migración de código heredado que realmente lo requiera.
- Usar `JArray` únicamente cuando el contrato de salida sea JSON dinámico y no exista un modelo estable.
- No convertir primero a `DataTable` para después crear objetos o JSON; mapear directamente desde `IDataReader`.

View File

@@ -0,0 +1,164 @@
# Reglas obligatorias para inicialización JavaScript idempotente
Estas reglas complementan `AGENTS.md` y `AGENTS_WEBFORMS_PARTIAL_POSTBACKS.md`.
## Objetivo
Los inicializadores deben poder ejecutarse después de la carga inicial y después de cualquier postback parcial sin:
- inicializar dos veces el mismo control;
- duplicar eventos;
- destruir y reconstruir controles que siguen vigentes;
- modificar elementos que ya fueron procesados;
- producir errores porque un plugin ya existe.
## Banderas por elemento
- Cada control inicializado debe conservar una bandera en el propio elemento HTML.
- Los inicializadores deben seleccionar únicamente elementos que todavía no tengan su bandera.
- Para código nuevo, usar atributos con namespace `data-sig-<componente>-initialized="1"`.
- La bandera debe identificar el componente, no usar nombres genéricos que puedan colisionar.
Ejemplos:
```html
data-sig-select2-initialized="1"
data-sig-tooltip-initialized="1"
data-sig-dropzone-initialized="1"
data-sig-datepicker-initialized="1"
```
- `custom.js` ya contiene banderas heredadas como `loaded`, `creado` y `ADsortable`. Deben respetarse mientras sus inicializadores sigan activos.
- Al modificar completamente uno de esos inicializadores, puede migrarse a `data-sig-*`, pero deben actualizarse juntos todos sus selectores, lecturas, escrituras y rutinas de destrucción.
- Para banderas nuevas visibles en el DOM, preferir `.attr('data-sig-...', '1')`, `.is('[data-sig-...]')` o `element.dataset`.
- No mezclar `.data()` y `.attr()` para leer/escribir la misma bandera: jQuery mantiene un caché interno y ambos valores pueden quedar diferentes.
## Momento correcto para colocar la bandera
- Comprobar la bandera antes de modificar el elemento o registrar eventos.
- Colocar la bandera sólo después de que el plugin o control termine de inicializar correctamente.
- Si la inicialización falla, el elemento debe permanecer sin bandera para permitir un reintento posterior.
- No colocar la bandera antes de una operación que pueda lanzar una excepción.
Ejemplo:
```javascript
function initializeSelect2(root) {
var $root = $(root || document);
$root.find('.js-select2:not([data-sig-select2-initialized="1"])')
.addBack('.js-select2:not([data-sig-select2-initialized="1"])')
.each(function () {
var $control = $(this);
$control.select2({
width: '100%'
});
$control.attr('data-sig-select2-initialized', '1');
});
}
```
## Postbacks parciales y contenido nuevo
- Cuando un `UpdatePanel` reemplaza su HTML, los elementos nuevos no contienen la bandera de la instancia anterior y serán inicializados normalmente.
- Los elementos fuera del panel actualizado conservan su bandera y deben ignorarse.
- Siempre que sea posible, pasar como `root` únicamente el panel actualizado en lugar de volver a recorrer todo `document`.
- Centralizar la conexión con `Sys.WebForms.PageRequestManager.endRequest` o `Sys.Application.load`; no registrar ese puente desde cada página.
- El puente global debe registrarse una sola vez.
Ejemplo:
```javascript
function initializeSigControls(root) {
initializeSelect2(root);
initializeTooltips(root);
initializeDatePickers(root);
}
if (window.SIG_PARTIAL_INITIALIZERS_REGISTERED !== true) {
window.SIG_PARTIAL_INITIALIZERS_REGISTERED = true;
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(
function (sender, args) {
var panels = args.get_panelsUpdated();
if (!panels || panels.length === 0) {
initializeSigControls(document);
return;
}
for (var index = 0; index < panels.length; index++) {
initializeSigControls(panels[index]);
}
}
);
}
```
## Eventos delegados y eventos directos
- Los eventos delegados sobre `body`, `document` o un contenedor permanente deben registrarse una sola vez mediante una bandera global.
- `custom.js` ya usa `window.SIG_CUSTOM_READY_INITIALIZED`; conservar este patrón para el bloque global existente.
- No ejecutar repetidamente `.off().on()` sobre todos los controles después de cada postback parcial.
- Para eventos directos inevitables, inicializar únicamente elementos sin bandera y usar eventos con namespace.
Ejemplo de evento directo:
```javascript
function initializeAmountInputs(root) {
$(root || document)
.find('.js-amount:not([data-sig-amount-initialized="1"])')
.each(function () {
var $input = $(this);
$input.on('change.sigAmount', function () {
// Manejar cambio.
});
$input.attr('data-sig-amount-initialized', '1');
});
}
```
## Destrucción y reinicialización explícita
- No quitar una bandera sólo para forzar que el inicializador vuelva a ejecutarse.
- Quitar la bandera únicamente cuando exista una necesidad real de destruir o reconfigurar el componente.
- Antes de quitarla, llamar la API de destrucción del plugin y eliminar eventos con su namespace.
- El orden obligatorio es: destruir plugin, retirar eventos propios, quitar bandera y finalmente reinicializar si corresponde.
Ejemplo:
```javascript
function destroySelect2(control) {
var $control = $(control);
if ($control.attr('data-sig-select2-initialized') !== '1') {
return;
}
if ($control.hasClass('select2-hidden-accessible')) {
$control.select2('destroy');
}
$control.off('.sigSelect2');
$control.removeAttr('data-sig-select2-initialized');
}
```
## Clonado y reemplazo de controles
- No copiar accidentalmente banderas de inicialización al clonar controles.
- Preferir clonar la plantilla antes de inicializarla.
- Si se clona un elemento ya inicializado, retirar del clon las banderas `data-sig-*-initialized`, IDs generados por plugins y contenedores auxiliares antes de insertarlo.
- Un control eliminado por un `UpdatePanel` no necesita limpieza manual de bandera, pero los recursos globales o listeners externos sí deben liberarse cuando el plugin lo requiera.
## Organización
- Cada inicializador debe ser una función pequeña e idempotente que acepte un elemento raíz.
- La función central sólo coordina inicializadores; no debe contener toda la lógica de cada plugin.
- No duplicar inicializadores dentro de páginas `.aspx` si pertenecen globalmente a `Scripts/custom.js`.
- El JavaScript específico de una página puede vivir en el placeholder `J`, pero debe seguir las mismas reglas de banderas y postbacks parciales.
- No modificar archivos minificados ni dependencias de terceros para implementar estas banderas.

88
AGENTS_PAGE_STRUCTURE.md Normal file
View File

@@ -0,0 +1,88 @@
# Reglas obligatorias para la ubicación de páginas WebForms
Estas reglas complementan `AGENTS.md` y aplican a toda página `.aspx` nueva.
## Raíz de páginas
- `SIG.Web/Pages/` es la raíz obligatoria para todas las páginas funcionales nuevas.
- No agregar páginas de aplicación directamente dentro de `SIG.Web/Pages/`; cada página debe pertenecer a una subcarpeta de módulo.
- Usar nombres de carpeta en PascalCase y conservar un solo nombre por módulo.
Estructura inicial:
```text
SIG.Web/
Pages/
Security/
Tramites/
Polizas/
```
Ejemplos futuros:
```text
Pages/Security/Usuarios.aspx
Pages/Security/Roles.aspx
Pages/Tramites/Solicitudes.aspx
Pages/Tramites/Seguimiento.aspx
Pages/Polizas/PrePolizas.aspx
```
- Crear nuevas carpetas bajo `Pages/` cuando aparezca un módulo real, por ejemplo `Clientes`, `Fraccionamientos`, `Ventas`, `Catalogos` o `Reportes`.
- No crear carpetas vacías para módulos que todavía no existen, salvo las carpetas iniciales acordadas.
- No colocar páginas de la aplicación dentro de `Metronic`, `Scripts`, `Styles`, `Content`, `AppCode` ni carpetas de paquetes o recursos de terceros.
- Los ejemplos `.aspx` incluidos dentro de dependencias visuales de `Metronic` son archivos heredados del proveedor y no cuentan como páginas funcionales de la aplicación. No usarlos como destino para código nuevo.
## Únicas páginas permitidas fuera de Pages
Las páginas de aplicación que actualmente están en la raíz son excepciones explícitas y son las únicas que pueden permanecer o existir fuera de `Pages/`:
```text
AccessDenied.aspx
Inicio.aspx
Login.aspx
Logout.aspx
```
- No agregar otra página `.aspx` en la raíz de `SIG.Web`.
- No mover estas cuatro páginas sin una solicitud explícita, porque sus rutas forman parte del inicio, autenticación y manejo global de acceso.
- `Login.aspx` sigue siendo la única página con estructura HTML completa propia.
- `Inicio.aspx` y `AccessDenied.aspx` usan `Site.master`.
- `Logout.aspx` puede permanecer como endpoint WebForms sin contenido visual.
## Archivos relacionados
- Mantener juntos los tres archivos de cada página:
```text
Pages/Modulo/Pagina.aspx
Pages/Modulo/Pagina.aspx.cs
Pages/Modulo/Pagina.aspx.designer.cs
```
- Incluir los tres archivos en `SIG.Web.csproj` con `DependentUpon` correcto.
- Usar namespaces que reflejen la carpeta, por ejemplo `SIG.Web.Pages.Security`.
- El atributo `Inherits` del `.aspx` debe coincidir exactamente con el namespace y la clase del code-behind.
- Todas las páginas normales deben usar `MasterPageFile="~/Site.master"` y los placeholders `H`, `C` y `J`.
- Todas las páginas normales deben heredar de `ClsPaginaBase`, salvo una excepción global documentada como Login, Logout o AccessDenied.
## Rutas y permisos
- Usar rutas virtuales completas, por ejemplo `~/Pages/Security/Usuarios.aspx`.
- Actualizar menús, módulos, permisos, redirecciones y breadcrumbs con la ruta real de la subcarpeta.
- No depender de rutas relativas como `../` para navegación de la aplicación.
- `PermissionService` y los procedimientos de acceso deben comparar la ruta incluyendo `Pages/Modulo/`.
- Los enlaces generados deben pasar por `ResolveUrl`, `VirtualPathUtility.ToAbsolute` o el mecanismo central existente.
## Arquitectura
- La ubicación dentro de `Pages/` no autoriza acceso directo a SQL.
- El code-behind debe permanecer delgado y llamar contratos/servicios.
- Toda operación de base de datos continúa usando procedimientos almacenados mediante repositorios.
- Los modelos, contratos, servicios y repositorios no deben colocarse dentro de `Pages/`; permanecen bajo sus carpetas de `AppCode`.
## Excepciones públicas de registro
Además de Login.aspx, las páginas Pages/Security/RegistroCliente.aspx, Pages/Security/RegistroPendiente.aspx y Pages/Security/VerificarCorreo.aspx pueden ser páginas públicas completas con su propio form runat="server". Son las únicas excepciones nuevas; las páginas autenticadas continúan usando Site.master y no declaran form.
RegistroCliente.aspx debe manejar el borrador de ambos pasos en ViewState cifrado y no persistir información hasta terminar todas las validaciones. La persistencia final debe usar servicios/repositorios y un procedimiento almacenado transaccional.

123
AGENTS_STORED_PROCEDURES.md Normal file
View File

@@ -0,0 +1,123 @@
# Reglas obligatorias de procedimientos almacenados
Estas reglas complementan `AGENTS.md` y son obligatorias para toda consulta nueva o modificada en la aplicación.
## Acceso a datos
- Todas las lecturas y escrituras de la aplicación deben usar procedimientos almacenados.
- No escribir `SELECT`, `INSERT`, `UPDATE`, `DELETE` ni `MERGE` directamente en C# dentro de repositorios, servicios, páginas, controles o la master page.
- Los repositorios deben usar `CommandType.StoredProcedure`.
- Todos los valores proporcionados por la aplicación deben enviarse como parámetros.
- Nunca concatenar entrada del usuario en SQL.
## Nombres
- Usar el formato `PA_<Tipo>_<Entidad><Accion>`.
- `PA` significa procedimiento almacenado.
- `<Tipo>` identifica el grupo funcional: `Sis`, `Tra`, `Vta`, `Conta`, `Clientes`, `Fraccionamientos`, etcétera.
- `<Entidad>` identifica el objeto y `<Accion>` describe claramente la operación; por ejemplo `Add`, `Update`, `List`, `Activate`, `Deactivate` o `Delete`.
- Ejemplos: `PA_Sis_UsuarioAdd`, `PA_Sis_UsuarioUpdate`, `PA_Sis_UsuarioList`, `PA_Sis_UsuarioActivate`, `PA_Sis_UsuarioDeactivate` y `PA_Sis_UsuarioDelete`.
## Parámetros y resultado obligatorios
Todos los procedimientos deben declarar:
```sql
@CveUsuarioEjecuta int,
@ErrorMostrar varchar(max) OUTPUT,
@ErrorTipo int OUTPUT,
@Return bit OUTPUT
```
- `@CveUsuarioEjecuta` identifica al usuario autenticado que ejecuta la operación.
- Al inicio, establecer `@ErrorMostrar = NULL`, `@ErrorTipo = 0` y `@Return = 0`.
- Establecer `@Return = 1` únicamente cuando la operación termine correctamente.
- `@ErrorTipo = 0`: sin error ni advertencia.
- `@ErrorTipo = 1`: error.
- `@ErrorTipo = 2`: advertencia.
- `@ErrorMostrar` contiene el mensaje amigable en español que puede mostrarse al usuario.
- No devolver contraseñas, hashes, cadenas de conexión, SQL, stack traces ni detalles internos mediante `@ErrorMostrar`.
- Los procedimientos de consulta pueden devolver un result set, pero también deben poblar los parámetros comunes.
- El repositorio debe convertir los parámetros de salida en un modelo de resultado como `DbResult<T>`.
## Control de errores
- Usar `SET NOCOUNT ON` y `TRY/CATCH` en todos los procedimientos.
- Usar `SET XACT_ABORT ON` cuando exista una mutación o transacción.
- Iniciar la transacción dentro de `TRY`, confirmar sólo después del éxito y ejecutar rollback en `CATCH` cuando `XACT_STATE() <> 0`.
- Una validación fallida debe establecer los parámetros de salida y ejecutar `RETURN`; no debe continuar la mutación.
- Para validar existencia, usar normalmente `EXISTS` en lugar de `COUNT(*)`.
- En `CATCH`, generar un mensaje técnico separado con `ERROR_NUMBER()`, `ERROR_PROCEDURE()`, `ERROR_LINE()` y `ERROR_MESSAGE()` para el log.
- El mensaje técnico puede notificarse con severidad informativa para que el ejecutor lo registre, pero nunca debe mostrarse directamente al usuario.
## Borrado lógico
- Está prohibido usar `DELETE` físico en procedimientos CRUD normales.
- Para borrar: `Borrado = 1` y `FechaBorrado = SYSDATETIME()`.
- Para restaurar: `Borrado = 0` y `FechaBorrado = NULL`.
- Las consultas normales deben filtrar `Borrado = 0` en cada tabla participante, excepto cuando el caso de uso solicite expresamente registros borrados.
- La eliminación permanente sólo puede realizarse mediante el procedimiento controlado de retención y limpieza.
- Ninguna página ASPX puede llamar directamente a la limpieza física.
## Plantilla requerida
```sql
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_RolAdd]
@Rol varchar(50),
@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
IF EXISTS
(
SELECT 1
FROM dbo.Sis_Roles
WHERE NombreRol = @Rol
AND Borrado = 0
)
BEGIN
SET @ErrorMostrar = CONCAT('El rol ''', @Rol, ''' ya se encuentra registrado.');
SET @ErrorTipo = 2;
RETURN;
END;
INSERT INTO dbo.Sis_Roles
(NombreRol, Activo, FechaCreacion)
VALUES
(@Rol, 1, SYSDATETIME());
SET @Return = 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0
ROLLBACK TRANSACTION;
SET @ErrorMostrar = 'Ocurrió un error al agregar el rol; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT(
'Número: ', ERROR_NUMBER(),
'; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_RolAdd'),
'; Línea: ', ERROR_LINE(),
'; Detalle: ', ERROR_MESSAGE()
);
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
```
Cuando el procedimiento use una transacción, debe incluir `BEGIN TRANSACTION` dentro de `TRY` y `COMMIT TRANSACTION` antes de establecer `@Return = 1`.

View File

@@ -0,0 +1,102 @@
# Reglas obligatorias para formularios y postbacks parciales WebForms
Estas reglas complementan `AGENTS.md` y `AGENTS_PAGE_STRUCTURE.md`.
## Formulario principal
- `Site.master` es el único archivo autorizado para declarar el formulario principal:
```aspx
<form id="form1" runat="server">
```
- Ninguna página normal `.aspx` que use `Site.master` debe declarar `<form>`, `<form runat="server">`, `<html>`, `<head>` ni `<body>`.
- No anidar formularios HTML dentro del formulario de `Site.master`.
- Los formularios visuales de captura se construyen con `<div>`, controles WebForms y validadores dentro del placeholder `C`.
- `Login.aspx` es la única excepción actual porque no usa `Site.master` y conserva su estructura HTML completa con su propio formulario de servidor.
- No agregar una segunda excepción sin solicitud explícita.
## ScriptManager
- `Site.master` es el único dueño del `asp:ScriptManager` principal, actualmente `TSM`.
- Las páginas de contenido no deben agregar otro `ScriptManager`.
- Usar `asp:ScriptManagerProxy` solamente cuando una página requiera registrar servicios o scripts específicos que no pertenezcan globalmente al master.
- El manejo global de `AsyncPostBackError` permanece en `Site.master`.
## UpdatePanel
- Usar `asp:UpdatePanel` cuando una interacción deba actualizar únicamente una sección sin refrescar visualmente toda la página.
- No envolver todo el placeholder `C` en un único `UpdatePanel` por comodidad.
- Crear paneles pequeños alrededor de la tabla, formulario, resumen o sección que realmente cambia.
- Preferir esta configuración:
```aspx
<asp:UpdatePanel ID="UpUsuarios" runat="server" UpdateMode="Conditional" ChildrenAsTriggers="false">
<ContentTemplate>
<!-- Sección que se actualiza parcialmente -->
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="BtnBuscar" EventName="Click" />
</Triggers>
</asp:UpdatePanel>
```
- Usar `UpdateMode="Conditional"` de manera predeterminada.
- Preferir `ChildrenAsTriggers="false"` y declarar triggers explícitos para que el comportamiento sea fácil de revisar.
- Llamar `Update()` únicamente en paneles condicionales y sólo cuando el servidor haya modificado su contenido.
- Evitar `UpdatePanel` anidados. Si son necesarios, documentar qué evento actualiza cada panel.
- No actualizar varios paneles si sólo cambió una sección.
- Los controles externos que disparen actualización parcial deben declararse como `AsyncPostBackTrigger` o registrarse mediante `ScriptManager.RegisterAsyncPostBackControl`.
## Operaciones que requieren postback completo
- Usar `PostBackTrigger` para cargas de archivos, descargas que escriben directamente en `Response`, exportaciones y otras operaciones incompatibles con postback asíncrono.
- No intentar procesar `FileUpload` dentro de un postback parcial sin `PostBackTrigger`.
- No iniciar una descarga binaria desde una respuesta parcial de `UpdatePanel`.
## Ciclo de vida y rendimiento
- Un postback parcial ejecuta el ciclo de vida de servidor de la página; no significa que el servidor procese únicamente el `UpdatePanel`.
- Proteger cargas iniciales con `if (!IsPostBack)`.
- No repetir consultas, catálogos o construcción del menú en cada evento si no son necesarios.
- Toda consulta continúa ejecutándose mediante contratos, servicios, repositorios y procedimientos almacenados.
- Preferir operaciones asíncronas de extremo a extremo para consultas lentas cuando la página esté configurada correctamente para async.
- Configurar timeouts razonables y no dejar indicadores de carga activos indefinidamente.
## Experiencia de usuario
- Usar `asp:UpdateProgress` cuando la operación pueda tardar lo suficiente para que el usuario necesite retroalimentación.
- Asociar el indicador al `UpdatePanel` correspondiente cuando sea posible.
- Deshabilitar temporalmente botones que puedan generar envíos duplicados.
- Mostrar mensajes amigables dentro de la sección actualizada o mediante el sistema global de mensajes.
- Los detalles técnicos permanecen exclusivamente en el log.
## JavaScript después de un postback parcial
- Los scripts registrados globalmente no deben duplicarse dentro de un `UpdatePanel`.
- Los componentes JavaScript que pierdan inicialización después de reemplazar HTML deben reinicializarse mediante `Sys.WebForms.PageRequestManager.endRequest`.
- Evitar registrar múltiples veces el mismo handler en cada postback parcial.
- Usar `ScriptManager.RegisterStartupScript` cuando un script de servidor deba ejecutarse después de una actualización parcial.
## Plantilla de página normal
```aspx
<%@ Page Title="Página" Language="C#" MasterPageFile="~/Site.master"
AutoEventWireup="true" CodeBehind="Pagina.aspx.cs"
Inherits="SIG.Web.Pages.Modulo.Pagina" %>
<asp:Content ID="ContentHead" ContentPlaceHolderID="H" runat="server">
</asp:Content>
<asp:Content ID="ContentMain" ContentPlaceHolderID="C" runat="server">
<asp:UpdatePanel ID="UpContenido" runat="server"
UpdateMode="Conditional" ChildrenAsTriggers="false">
<ContentTemplate>
<!-- No agregar otro form ni otro ScriptManager. -->
</ContentTemplate>
</asp:UpdatePanel>
</asp:Content>
<asp:Content ID="ContentScripts" ContentPlaceHolderID="J" runat="server">
</asp:Content>
```

2149
Esquema_BD_AguaHillo.sql Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

4265
Esquema_BD_AguaHillo_v2.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,329 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
SET XACT_ABORT ON;
GO
/*
Borrado lógico y limpieza controlada.
Convención:
Borrado = 0 y FechaBorrado IS NULL: registro vigente.
Borrado = 1 y FechaBorrado IS NOT NULL: registro borrado lógicamente.
La limpieza física está deshabilitada por defecto. Cada tabla debe registrarse
expresamente mediante dbo.Sis_ConfigurarLimpieza y la ejecución inicia siempre
en modo simulación.
*/
IF OBJECT_ID(N'dbo.Sis_PoliticasLimpieza', N'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Sis_PoliticasLimpieza]
(
[Esquema] sysname NOT NULL,
[Tabla] sysname NOT NULL,
[DiasRetencion] int CONSTRAINT [DF_Sis_PoliticasLimpieza_DiasRetencion] DEFAULT ((365)) NOT NULL,
[TamanoLote] int CONSTRAINT [DF_Sis_PoliticasLimpieza_TamanoLote] DEFAULT ((500)) NOT NULL,
[Habilitada] bit CONSTRAINT [DF_Sis_PoliticasLimpieza_Habilitada] DEFAULT ((0)) NOT NULL,
[FechaUltimaLimpieza] datetime2(0) NULL,
[Borrado] bit CONSTRAINT [DF_Sis_PoliticasLimpieza_Borrado] DEFAULT ((0)) NOT NULL,
[FechaBorrado] datetime2(0) NULL,
CONSTRAINT [PK_Sis_PoliticasLimpieza] PRIMARY KEY CLUSTERED ([Esquema], [Tabla]),
CONSTRAINT [CK_Sis_PoliticasLimpieza_DiasRetencion] CHECK ([DiasRetencion] BETWEEN (30) AND (3650)),
CONSTRAINT [CK_Sis_PoliticasLimpieza_TamanoLote] CHECK ([TamanoLote] BETWEEN (1) AND (10000)),
CONSTRAINT [CK_Sis_PoliticasLimpieza_Borrado] CHECK
(
([Borrado] = (0) AND [FechaBorrado] IS NULL)
OR ([Borrado] = (1) AND [FechaBorrado] IS NOT NULL)
)
);
END;
GO
/* Agrega la convención a todas las tablas de usuario existentes. */
DECLARE @Esquema sysname;
DECLARE @Tabla sysname;
DECLARE @Objeto nvarchar(517);
DECLARE @Sql nvarchar(max);
DECLARE @Errores TABLE ([Esquema] sysname, [Tabla] sysname, [Mensaje] nvarchar(4000));
DECLARE TablasBorradoLogico CURSOR LOCAL FAST_FORWARD FOR
SELECT S.[name], T.[name]
FROM sys.tables AS T
INNER JOIN sys.schemas AS S ON S.[schema_id] = T.[schema_id]
WHERE T.[is_ms_shipped] = (0)
AND T.[temporal_type] = (0)
ORDER BY S.[name], T.[name];
OPEN TablasBorradoLogico;
FETCH NEXT FROM TablasBorradoLogico INTO @Esquema, @Tabla;
WHILE @@FETCH_STATUS = (0)
BEGIN
SET @Objeto = QUOTENAME(@Esquema) + N'.' + QUOTENAME(@Tabla);
BEGIN TRY
IF COL_LENGTH(@Esquema + N'.' + @Tabla, N'Borrado') IS NULL
BEGIN
SET @Sql = N'ALTER TABLE ' + @Objeto
+ N' ADD [Borrado] bit NOT NULL DEFAULT ((0)) WITH VALUES;';
EXEC sys.sp_executesql @Sql;
END;
IF COL_LENGTH(@Esquema + N'.' + @Tabla, N'FechaBorrado') IS NULL
BEGIN
SET @Sql = N'ALTER TABLE ' + @Objeto
+ N' ADD [FechaBorrado] datetime2(0) NULL;';
EXEC sys.sp_executesql @Sql;
END;
IF NOT EXISTS
(
SELECT 1
FROM sys.check_constraints AS C
WHERE C.[parent_object_id] = OBJECT_ID(@Esquema + N'.' + @Tabla)
AND C.[name] = N'CK_' + @Tabla + N'_BorradoLogico'
)
AND LEN(N'CK_' + @Tabla + N'_BorradoLogico') <= 128
BEGIN
SET @Sql = N'ALTER TABLE ' + @Objeto
+ 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;
END TRY
BEGIN CATCH
INSERT INTO @Errores ([Esquema], [Tabla], [Mensaje])
VALUES (@Esquema, @Tabla, ERROR_MESSAGE());
END CATCH;
FETCH NEXT FROM TablasBorradoLogico INTO @Esquema, @Tabla;
END;
CLOSE TablasBorradoLogico;
DEALLOCATE TablasBorradoLogico;
IF EXISTS (SELECT 1 FROM @Errores)
SELECT [Esquema], [Tabla], [Mensaje] FROM @Errores ORDER BY [Esquema], [Tabla];
GO
CREATE OR ALTER PROCEDURE [dbo].[Sis_ConfigurarLimpieza]
@Esquema sysname = N'dbo',
@Tabla sysname,
@DiasRetencion int = 365,
@TamanoLote int = 500,
@Habilitada bit = 0
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @DiasRetencion NOT BETWEEN 30 AND 3650
THROW 51000, N'Los días de retención deben estar entre 30 y 3650.', 1;
IF @TamanoLote NOT BETWEEN 1 AND 10000
THROW 51001, N'El tamaño de lote debe estar entre 1 y 10000.', 1;
IF @Esquema = N'dbo' AND @Tabla = N'Sis_PoliticasLimpieza'
THROW 51002, N'No se permite configurar la limpieza de la tabla de políticas.', 1;
IF OBJECT_ID(QUOTENAME(@Esquema) + N'.' + QUOTENAME(@Tabla), N'U') IS NULL
THROW 51003, N'La tabla indicada no existe.', 1;
IF COL_LENGTH(@Esquema + N'.' + @Tabla, N'Borrado') IS NULL
OR COL_LENGTH(@Esquema + N'.' + @Tabla, N'FechaBorrado') IS NULL
THROW 51004, N'La tabla no tiene las columnas de borrado lógico.', 1;
UPDATE [dbo].[Sis_PoliticasLimpieza]
SET [DiasRetencion] = @DiasRetencion,
[TamanoLote] = @TamanoLote,
[Habilitada] = @Habilitada,
[Borrado] = (0),
[FechaBorrado] = NULL
WHERE [Esquema] = @Esquema
AND [Tabla] = @Tabla;
IF @@ROWCOUNT = (0)
BEGIN
INSERT INTO [dbo].[Sis_PoliticasLimpieza]
([Esquema], [Tabla], [DiasRetencion], [TamanoLote], [Habilitada])
VALUES
(@Esquema, @Tabla, @DiasRetencion, @TamanoLote, @Habilitada);
END;
DECLARE @NombreIndice sysname = N'IX_BorradoLogico_FechaBorrado';
DECLARE @SqlIndice nvarchar(max);
IF NOT EXISTS
(
SELECT 1
FROM sys.indexes
WHERE [object_id] = OBJECT_ID(QUOTENAME(@Esquema) + N'.' + QUOTENAME(@Tabla))
AND [name] = @NombreIndice
)
BEGIN
SET @SqlIndice = N'CREATE NONCLUSTERED INDEX ' + QUOTENAME(@NombreIndice)
+ N' ON ' + QUOTENAME(@Esquema) + N'.' + QUOTENAME(@Tabla)
+ N' ([FechaBorrado]) WHERE [Borrado] = (1);';
EXEC sys.sp_executesql @SqlIndice;
END;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[Sis_LimpiarRegistrosBorrados]
@SoloSimular bit = 1,
@ConfirmarBorradoFisico bit = 0,
@MaximoLotesPorTabla int = 100
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @MaximoLotesPorTabla NOT BETWEEN 1 AND 10000
THROW 51010, N'El máximo de lotes debe estar entre 1 y 10000.', 1;
IF @SoloSimular = (0) AND @ConfirmarBorradoFisico = (0)
THROW 51011, N'Para borrar físicamente debe enviar @ConfirmarBorradoFisico = 1.', 1;
DECLARE @Resultados TABLE
(
[Esquema] sysname NOT NULL,
[Tabla] sysname NOT NULL,
[FilasElegibles] bigint NOT NULL,
[FilasEliminadas] bigint NOT NULL,
[Resultado] nvarchar(4000) NOT NULL
);
DECLARE @Esquema sysname;
DECLARE @Tabla sysname;
DECLARE @DiasRetencion int;
DECLARE @TamanoLote int;
DECLARE @Objeto nvarchar(517);
DECLARE @Sql nvarchar(max);
DECLARE @Errores TABLE ([Esquema] sysname, [Tabla] sysname, [Mensaje] nvarchar(4000));
DECLARE @Elegibles bigint;
DECLARE @Eliminadas bigint;
DECLARE @FilasLote int;
DECLARE @NumeroLote int;
DECLARE Politicas CURSOR LOCAL FAST_FORWARD FOR
SELECT [Esquema], [Tabla], [DiasRetencion], [TamanoLote]
FROM [dbo].[Sis_PoliticasLimpieza]
WHERE [Habilitada] = (1)
AND [Borrado] = (0)
ORDER BY [Esquema], [Tabla];
OPEN Politicas;
FETCH NEXT FROM Politicas INTO @Esquema, @Tabla, @DiasRetencion, @TamanoLote;
WHILE @@FETCH_STATUS = (0)
BEGIN
SET @Objeto = QUOTENAME(@Esquema) + N'.' + QUOTENAME(@Tabla);
BEGIN TRY
SET @Elegibles = (0);
SET @Eliminadas = (0);
BEGIN TRY
IF OBJECT_ID(@Objeto, N'U') IS NULL
THROW 51012, N'La tabla configurada ya no existe.', 1;
SET @Sql = N'SELECT @Cantidad = COUNT_BIG(*) FROM ' + @Objeto
+ N' WHERE [Borrado] = (1)'
+ N' AND [FechaBorrado] < DATEADD(DAY, -@Dias, SYSDATETIME());';
EXEC sys.sp_executesql
@Sql,
N'@Dias int, @Cantidad bigint OUTPUT',
@Dias = @DiasRetencion,
@Cantidad = @Elegibles OUTPUT;
IF @SoloSimular = (1)
BEGIN
INSERT INTO @Resultados
([Esquema], [Tabla], [FilasElegibles], [FilasEliminadas], [Resultado])
VALUES
(@Esquema, @Tabla, @Elegibles, (0), N'Simulación; no se eliminó información.');
END
ELSE
BEGIN
SET @NumeroLote = (0);
SET @FilasLote = (1);
WHILE @FilasLote > (0) AND @NumeroLote < @MaximoLotesPorTabla
BEGIN
SET @Sql = N'DELETE TOP (@Lote) FROM ' + @Objeto
+ N' WHERE [Borrado] = (1)'
+ N' AND [FechaBorrado] < DATEADD(DAY, -@Dias, SYSDATETIME());'
+ N' SET @Filas = @@ROWCOUNT;';
EXEC sys.sp_executesql
@Sql,
N'@Lote int, @Dias int, @Filas int OUTPUT',
@Lote = @TamanoLote,
@Dias = @DiasRetencion,
@Filas = @FilasLote OUTPUT;
SET @Eliminadas += @FilasLote;
SET @NumeroLote += (1);
END;
UPDATE [dbo].[Sis_PoliticasLimpieza]
SET [FechaUltimaLimpieza] = SYSDATETIME()
WHERE [Esquema] = @Esquema
AND [Tabla] = @Tabla;
INSERT INTO @Resultados
([Esquema], [Tabla], [FilasElegibles], [FilasEliminadas], [Resultado])
VALUES
(@Esquema, @Tabla, @Elegibles, @Eliminadas, N'Limpieza física ejecutada.');
END;
END TRY
BEGIN CATCH
INSERT INTO @Resultados
([Esquema], [Tabla], [FilasElegibles], [FilasEliminadas], [Resultado])
VALUES
(@Esquema, @Tabla, @Elegibles, @Eliminadas,
CONCAT(N'No se eliminó la tabla: ', ERROR_MESSAGE()));
END CATCH;
FETCH NEXT FROM Politicas INTO @Esquema, @Tabla, @DiasRetencion, @TamanoLote;
END;
CLOSE Politicas;
DEALLOCATE Politicas;
SELECT [Esquema], [Tabla], [FilasElegibles], [FilasEliminadas], [Resultado]
FROM @Resultados
ORDER BY [Esquema], [Tabla];
END;
GO
/*
Ejemplos (no se ejecutan automáticamente):
-- Borrado lógico desde un repositorio/servicio:
UPDATE dbo.Fraccionamientos
SET Borrado = 1, FechaBorrado = SYSDATETIME()
WHERE CveFraccionamiento = @CveFraccionamiento;
-- Restauración:
UPDATE dbo.Fraccionamientos
SET Borrado = 0, FechaBorrado = NULL
WHERE CveFraccionamiento = @CveFraccionamiento;
-- Habilitar limpieza de una tabla después de 365 días:
EXEC dbo.Sis_ConfigurarLimpieza
@Esquema = N'dbo', @Tabla = N'Fraccionamientos',
@DiasRetencion = 365, @TamanoLote = 500, @Habilitada = 1;
-- Revisar qué se eliminaría (modo predeterminado y seguro):
EXEC dbo.Sis_LimpiarRegistrosBorrados;
-- Ejecutar la limpieza física confirmada:
EXEC dbo.Sis_LimpiarRegistrosBorrados
@SoloSimular = 0, @ConfirmarBorradoFisico = 1;
*/

View File

@@ -0,0 +1,611 @@
/*
Módulo de fraccionamientos, trámites, ventas y pagos.
Este archivo se ejecuta después de Esquema_BD_AguaHillo.sql.
Nombres reemplazados:
Des_Desarrolladoras -> Clientes
Des_Desarrollos -> Fraccionamientos
Des_EtapasDesarrollo -> FraccionamientosEtapas
Des_Lotes -> FraccionamientosLotes
Sis_Users -> Sis_Usuarios
Regla del fondo inicial:
- Se registra una Venta con TipoDocto = 'Ant'.
- La venta usa un producto genérico de ProductosServicios, por ejemplo "Fondo inicial".
- El detalle del fondo no se relaciona con una solicitud específica.
- Sólo el dinero efectivamente registrado en Vta_Pagos queda disponible.
- Vta_AplicacionesSaldoFavor distribuye ese dinero entre trámites posteriores.
- Ventas, pagos y aplicaciones pueden generar su propia Conta_PrePoliza.
*/
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
SET XACT_ABORT ON;
GO
/* ==================== FRACCIONAMIENTOS ==================== */
CREATE TABLE [dbo].[Fraccionamientos] (
[CveFraccionamiento] int IDENTITY(1,1) NOT NULL,
[CveCliente] int NOT NULL,
[Fraccionamiento] varchar(200) COLLATE Modern_Spanish_CS_AI NOT NULL,
[Hectareas] decimal(18,4) NULL,
[Ubicacion] varchar(500) COLLATE Modern_Spanish_CS_AI NULL,
[Estatus] varchar(50) COLLATE Modern_Spanish_CS_AI NULL,
[EsActivo] bit CONSTRAINT [DF_Fraccionamientos_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_Fraccionamientos_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Fraccionamientos] PRIMARY KEY CLUSTERED ([CveFraccionamiento]),
CONSTRAINT [UQ_Fraccionamientos_Cliente_Nombre] UNIQUE NONCLUSTERED ([CveCliente], [Fraccionamiento]),
CONSTRAINT [UQ_Fraccionamientos_Cliente] UNIQUE NONCLUSTERED ([CveFraccionamiento], [CveCliente]),
CONSTRAINT [CK_Fraccionamientos_Hectareas] CHECK ([Hectareas] IS NULL OR [Hectareas] >= (0))
);
GO
CREATE TABLE [dbo].[FraccionamientosEtapas] (
[CveFraccionamientoEtapa] int IDENTITY(1,1) NOT NULL,
[CveFraccionamiento] int NOT NULL,
[Etapa] varchar(150) COLLATE Modern_Spanish_CS_AI NOT NULL,
[Hectareas] decimal(18,4) NULL,
[Orden] int CONSTRAINT [DF_FraccionamientosEtapas_Orden] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_FraccionamientosEtapas_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_FraccionamientosEtapas_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_FraccionamientosEtapas] PRIMARY KEY CLUSTERED ([CveFraccionamientoEtapa]),
CONSTRAINT [UQ_FraccionamientosEtapas_Fraccionamiento_Etapa] UNIQUE NONCLUSTERED ([CveFraccionamiento], [Etapa]),
CONSTRAINT [UQ_FraccionamientosEtapas_Fraccionamiento] UNIQUE NONCLUSTERED ([CveFraccionamientoEtapa], [CveFraccionamiento]),
CONSTRAINT [CK_FraccionamientosEtapas_Hectareas] CHECK ([Hectareas] IS NULL OR [Hectareas] >= (0))
);
GO
CREATE TABLE [dbo].[FraccionamientosLotes] (
[CveFraccionamientoLote] int IDENTITY(1,1) NOT NULL,
[CveFraccionamientoEtapa] int NOT NULL,
[Tipo] varchar(100) COLLATE Modern_Spanish_CS_AI NULL,
[Calle] varchar(200) COLLATE Modern_Spanish_CS_AI NULL,
[Catastro] varchar(100) COLLATE Modern_Spanish_CS_AI NULL,
[MetrosCuadrados] decimal(18,4) NULL,
[Numero] varchar(50) COLLATE Modern_Spanish_CS_AI NULL,
[Duplicador] varchar(50) COLLATE Modern_Spanish_CS_AI NULL,
[EsActivo] bit CONSTRAINT [DF_FraccionamientosLotes_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_FraccionamientosLotes_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_FraccionamientosLotes] PRIMARY KEY CLUSTERED ([CveFraccionamientoLote]),
CONSTRAINT [CK_FraccionamientosLotes_MetrosCuadrados] CHECK ([MetrosCuadrados] IS NULL OR [MetrosCuadrados] >= (0))
);
GO
/* ==================== CATÁLOGOS DE TRÁMITES ==================== */
CREATE TABLE [dbo].[Tra_TiposTramite] (
[CveTipoTramite] int IDENTITY(1,1) NOT NULL,
[Codigo] varchar(50) COLLATE Modern_Spanish_CS_AI NOT NULL,
[TipoTramite] varchar(200) COLLATE Modern_Spanish_CS_AI NOT NULL,
[Descripcion] varchar(500) COLLATE Modern_Spanish_CS_AI NULL,
[Orden] int CONSTRAINT [DF_Tra_TiposTramite_Orden] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_TiposTramite_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_TiposTramite_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Tra_TiposTramite] PRIMARY KEY CLUSTERED ([CveTipoTramite]),
CONSTRAINT [UQ_Tra_TiposTramite_Codigo] UNIQUE NONCLUSTERED ([Codigo])
);
GO
CREATE TABLE [dbo].[Tra_EstatusSolicitud] (
[CveEstatusSolicitud] int IDENTITY(1,1) NOT NULL,
[Codigo] varchar(50) COLLATE Modern_Spanish_CS_AI NOT NULL,
[EstatusSolicitud] varchar(100) COLLATE Modern_Spanish_CS_AI NOT NULL,
[Orden] int CONSTRAINT [DF_Tra_EstatusSolicitud_Orden] DEFAULT ((0)) NOT NULL,
[EsFinal] bit CONSTRAINT [DF_Tra_EstatusSolicitud_EsFinal] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_EstatusSolicitud_EsActivo] DEFAULT ((1)) NOT NULL,
CONSTRAINT [PK_Tra_EstatusSolicitud] PRIMARY KEY CLUSTERED ([CveEstatusSolicitud]),
CONSTRAINT [UQ_Tra_EstatusSolicitud_Codigo] UNIQUE NONCLUSTERED ([Codigo])
);
GO
CREATE TABLE [dbo].[Tra_TiposDocumento] (
[CveTipoDocumento] int IDENTITY(1,1) NOT NULL,
[TipoDocumento] varchar(200) COLLATE Modern_Spanish_CS_AI NOT NULL,
[Descripcion] varchar(500) COLLATE Modern_Spanish_CS_AI NULL,
[ExtensionesPermitidas] varchar(200) COLLATE Modern_Spanish_CS_AI NULL,
[TamanoMaximoMb] int NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_TiposDocumento_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_TiposDocumento_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Tra_TiposDocumento] PRIMARY KEY CLUSTERED ([CveTipoDocumento]),
CONSTRAINT [UQ_Tra_TiposDocumento_Nombre] UNIQUE NONCLUSTERED ([TipoDocumento]),
CONSTRAINT [CK_Tra_TiposDocumento_Tamano] CHECK ([TamanoMaximoMb] IS NULL OR [TamanoMaximoMb] > (0))
);
GO
CREATE TABLE [dbo].[Tra_TiposPregunta] (
[CveTipoPregunta] int IDENTITY(1,1) NOT NULL,
[Codigo] varchar(50) COLLATE Modern_Spanish_CS_AI NOT NULL,
[TipoPregunta] varchar(100) COLLATE Modern_Spanish_CS_AI NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_TiposPregunta_EsActivo] DEFAULT ((1)) NOT NULL,
CONSTRAINT [PK_Tra_TiposPregunta] PRIMARY KEY CLUSTERED ([CveTipoPregunta]),
CONSTRAINT [UQ_Tra_TiposPregunta_Codigo] UNIQUE NONCLUSTERED ([Codigo])
);
GO
CREATE TABLE [dbo].[Tra_Preguntas] (
[CvePregunta] int IDENTITY(1,1) NOT NULL,
[CveTipoPregunta] int NOT NULL,
[Pregunta] varchar(500) COLLATE Modern_Spanish_CS_AI NOT NULL,
[Descripcion] varchar(1000) COLLATE Modern_Spanish_CS_AI NULL,
[EsRequerida] bit CONSTRAINT [DF_Tra_Preguntas_EsRequerida] DEFAULT ((0)) NOT NULL,
[Orden] int CONSTRAINT [DF_Tra_Preguntas_Orden] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_Preguntas_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_Preguntas_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Tra_Preguntas] PRIMARY KEY CLUSTERED ([CvePregunta])
);
GO
CREATE TABLE [dbo].[Tra_OpcionesPregunta] (
[CveOpcionPregunta] int IDENTITY(1,1) NOT NULL,
[CvePregunta] int NOT NULL,
[TextoOpcion] varchar(300) COLLATE Modern_Spanish_CS_AI NOT NULL,
[ValorOpcion] varchar(300) COLLATE Modern_Spanish_CS_AI NULL,
[Orden] int CONSTRAINT [DF_Tra_OpcionesPregunta_Orden] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_OpcionesPregunta_EsActivo] DEFAULT ((1)) NOT NULL,
CONSTRAINT [PK_Tra_OpcionesPregunta] PRIMARY KEY CLUSTERED ([CveOpcionPregunta]),
CONSTRAINT [UQ_Tra_OpcionesPregunta_Pregunta_Texto] UNIQUE NONCLUSTERED ([CvePregunta], [TextoOpcion])
);
GO
/* ==================== SOLICITUDES Y FLUJO ==================== */
CREATE TABLE [dbo].[Tra_Solicitudes] (
[CveSolicitud] int IDENTITY(1,1) NOT NULL,
[Folio] varchar(50) COLLATE Modern_Spanish_CS_AI NOT NULL,
[CveCliente] int NOT NULL,
[CveFraccionamiento] int NOT NULL,
[CveFraccionamientoEtapa] int NOT NULL,
[CveTipoTramite] int NOT NULL,
[CveEstatusSolicitud] int NOT NULL,
[CveUsuarioSolicita] int NULL,
[FechaInicio] datetime NULL,
[FechaEnvio] datetime NULL,
[FechaAutorizacion] datetime NULL,
[FechaRechazo] datetime NULL,
[ComentariosCliente] varchar(max) COLLATE Modern_Spanish_CS_AI NULL,
[ComentariosAdministrativos] varchar(max) COLLATE Modern_Spanish_CS_AI NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_Solicitudes_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
CONSTRAINT [PK_Tra_Solicitudes] PRIMARY KEY CLUSTERED ([CveSolicitud]),
CONSTRAINT [UQ_Tra_Solicitudes_Folio] UNIQUE NONCLUSTERED ([Folio]),
CONSTRAINT [UQ_Tra_Solicitudes_Etapa_TipoTramite] UNIQUE NONCLUSTERED ([CveFraccionamientoEtapa], [CveTipoTramite])
);
GO
CREATE TABLE [dbo].[Tra_SolicitudesDependencias] (
[CveSolicitudDependencia] int IDENTITY(1,1) NOT NULL,
[CveSolicitud] int NOT NULL,
[CveSolicitudRequerida] int NOT NULL,
[EsObligatoria] bit CONSTRAINT [DF_Tra_SolicitudesDependencias_EsObligatoria] DEFAULT ((1)) NOT NULL,
[Condicion] varchar(500) COLLATE Modern_Spanish_CS_AI NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_SolicitudesDependencias_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Tra_SolicitudesDependencias] PRIMARY KEY CLUSTERED ([CveSolicitudDependencia]),
CONSTRAINT [UQ_Tra_SolicitudesDependencias] UNIQUE NONCLUSTERED ([CveSolicitud], [CveSolicitudRequerida]),
CONSTRAINT [CK_Tra_SolicitudesDependencias_Diferentes] CHECK ([CveSolicitud] <> [CveSolicitudRequerida])
);
GO
CREATE TABLE [dbo].[Tra_HistorialEstatusSolicitud] (
[CveHistorialEstatusSolicitud] int IDENTITY(1,1) NOT NULL,
[CveSolicitud] int NOT NULL,
[CveEstatusSolicitud] int NOT NULL,
[CveUsuario] int NULL,
[Comentarios] varchar(1000) COLLATE Modern_Spanish_CS_AI NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_HistorialEstatusSolicitud_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
CONSTRAINT [PK_Tra_HistorialEstatusSolicitud] PRIMARY KEY CLUSTERED ([CveHistorialEstatusSolicitud])
);
GO
CREATE TABLE [dbo].[Tra_DocumentosRequeridos] (
[CveDocumentoRequerido] int IDENTITY(1,1) NOT NULL,
[CveTipoTramite] int NOT NULL,
[CveTipoDocumento] int NOT NULL,
[EsRequerido] bit CONSTRAINT [DF_Tra_DocumentosRequeridos_EsRequerido] DEFAULT ((1)) NOT NULL,
[Orden] int CONSTRAINT [DF_Tra_DocumentosRequeridos_Orden] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_DocumentosRequeridos_EsActivo] DEFAULT ((1)) NOT NULL,
CONSTRAINT [PK_Tra_DocumentosRequeridos] PRIMARY KEY CLUSTERED ([CveDocumentoRequerido]),
CONSTRAINT [UQ_Tra_DocumentosRequeridos] UNIQUE NONCLUSTERED ([CveTipoTramite], [CveTipoDocumento])
);
GO
CREATE TABLE [dbo].[Tra_DocumentosSolicitud] (
[CveDocumentoSolicitud] int IDENTITY(1,1) NOT NULL,
[CveSolicitud] int NOT NULL,
[CveTipoDocumento] int NOT NULL,
[NombreArchivo] varchar(255) COLLATE Modern_Spanish_CS_AI NOT NULL,
[NombreArchivoFisico] varchar(255) COLLATE Modern_Spanish_CS_AI NOT NULL,
[RutaArchivo] varchar(1000) COLLATE Modern_Spanish_CS_AI NOT NULL,
[ContentType] varchar(100) COLLATE Modern_Spanish_CS_AI NULL,
[TamanoBytes] bigint NULL,
[ComentarioCliente] varchar(1000) COLLATE Modern_Spanish_CS_AI NULL,
[ComentarioAdministrativo] varchar(1000) COLLATE Modern_Spanish_CS_AI NULL,
[EsAprobado] bit NULL,
[CveUsuarioCarga] int NULL,
[CveUsuarioRevisa] int NULL,
[FechaRevision] datetime NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_DocumentosSolicitud_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
CONSTRAINT [PK_Tra_DocumentosSolicitud] PRIMARY KEY CLUSTERED ([CveDocumentoSolicitud]),
CONSTRAINT [CK_Tra_DocumentosSolicitud_Tamano] CHECK ([TamanoBytes] IS NULL OR [TamanoBytes] >= (0))
);
GO
CREATE TABLE [dbo].[Tra_PreguntasTramite] (
[CvePreguntaTramite] int IDENTITY(1,1) NOT NULL,
[CveTipoTramite] int NOT NULL,
[CvePregunta] int NOT NULL,
[EsRequerida] bit CONSTRAINT [DF_Tra_PreguntasTramite_EsRequerida] DEFAULT ((0)) NOT NULL,
[Orden] int CONSTRAINT [DF_Tra_PreguntasTramite_Orden] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_PreguntasTramite_EsActivo] DEFAULT ((1)) NOT NULL,
CONSTRAINT [PK_Tra_PreguntasTramite] PRIMARY KEY CLUSTERED ([CvePreguntaTramite]),
CONSTRAINT [UQ_Tra_PreguntasTramite] UNIQUE NONCLUSTERED ([CveTipoTramite], [CvePregunta])
);
GO
CREATE TABLE [dbo].[Tra_RespuestasSolicitud] (
[CveRespuestaSolicitud] int IDENTITY(1,1) NOT NULL,
[CveSolicitud] int NOT NULL,
[CvePregunta] int NOT NULL,
[CveOpcionPregunta] int NULL,
[Valor] varchar(max) COLLATE Modern_Spanish_CS_AI NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_RespuestasSolicitud_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NULL,
CONSTRAINT [PK_Tra_RespuestasSolicitud] PRIMARY KEY CLUSTERED ([CveRespuestaSolicitud]),
CONSTRAINT [CK_Tra_RespuestasSolicitud_Valor] CHECK ([CveOpcionPregunta] IS NOT NULL OR [Valor] IS NOT NULL)
);
GO
CREATE TABLE [dbo].[Tra_CostosTramite] (
[CveCostoTramite] int IDENTITY(1,1) NOT NULL,
[CveTipoTramite] int NOT NULL,
[CvePS] int NOT NULL,
[CveMoneda] int NOT NULL,
[Descripcion] varchar(300) COLLATE Modern_Spanish_CS_AI NOT NULL,
[Importe] decimal(18,2) CONSTRAINT [DF_Tra_CostosTramite_Importe] DEFAULT ((0)) NOT NULL,
[FechaInicioVigencia] date NULL,
[FechaFinVigencia] date NULL,
[EsActivo] bit CONSTRAINT [DF_Tra_CostosTramite_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_CostosTramite_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[FechaModificacion] datetime NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Tra_CostosTramite] PRIMARY KEY CLUSTERED ([CveCostoTramite]),
CONSTRAINT [UQ_Tra_CostosTramite_Tipo_Producto_Inicio] UNIQUE NONCLUSTERED ([CveTipoTramite], [CvePS], [FechaInicioVigencia]),
CONSTRAINT [CK_Tra_CostosTramite_Importe] CHECK ([Importe] >= (0)),
CONSTRAINT [CK_Tra_CostosTramite_Vigencia] CHECK ([FechaFinVigencia] IS NULL OR [FechaInicioVigencia] IS NULL OR [FechaFinVigencia] >= [FechaInicioVigencia])
);
GO
CREATE TABLE [dbo].[Tra_NotasSolicitud] (
[CveNotaSolicitud] int IDENTITY(1,1) NOT NULL,
[CveSolicitud] int NOT NULL,
[CveUsuario] int NULL,
[TipoNota] varchar(50) COLLATE Modern_Spanish_CS_AI CONSTRAINT [DF_Tra_NotasSolicitud_TipoNota] DEFAULT ('General') NOT NULL,
[Comentario] varchar(max) COLLATE Modern_Spanish_CS_AI NOT NULL,
[EsVisibleParaCliente] bit CONSTRAINT [DF_Tra_NotasSolicitud_Visible] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_Tra_NotasSolicitud_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
CONSTRAINT [PK_Tra_NotasSolicitud] PRIMARY KEY CLUSTERED ([CveNotaSolicitud])
);
GO
/* ==================== VENTAS, PAGOS Y SALDO A FAVOR ==================== */
CREATE TABLE [dbo].[VentasDetalle] (
[CveVentaDetalle] int IDENTITY(1,1) NOT NULL,
[CveVenta] int NOT NULL,
[CvePS] int NOT NULL,
[CveSolicitud] int NULL,
[CveCostoTramite] int NULL,
[Cantidad] decimal(18,4) CONSTRAINT [DF_VentasDetalle_Cantidad] DEFAULT ((1)) NOT NULL,
[PrecioUnitario] decimal(18,2) CONSTRAINT [DF_VentasDetalle_PrecioUnitario] DEFAULT ((0)) NOT NULL,
[Descuento] decimal(18,2) CONSTRAINT [DF_VentasDetalle_Descuento] DEFAULT ((0)) NOT NULL,
[IVA] decimal(18,2) CONSTRAINT [DF_VentasDetalle_IVA] DEFAULT ((0)) NOT NULL,
[Importe] decimal(18,2) CONSTRAINT [DF_VentasDetalle_Importe] DEFAULT ((0)) NOT NULL,
[EsFondoInicial] bit CONSTRAINT [DF_VentasDetalle_EsFondoInicial] DEFAULT ((0)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_VentasDetalle_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_VentasDetalle] PRIMARY KEY CLUSTERED ([CveVentaDetalle]),
CONSTRAINT [CK_VentasDetalle_Importes] CHECK ([Cantidad] > (0) AND [PrecioUnitario] >= (0) AND [Descuento] >= (0) AND [IVA] >= (0) AND [Importe] >= (0)),
CONSTRAINT [CK_VentasDetalle_FondoInicial] CHECK ([EsFondoInicial] = (0) OR ([CveSolicitud] IS NULL AND [CveCostoTramite] IS NULL))
);
GO
CREATE TABLE [dbo].[Vta_Pagos] (
[CvePago] int IDENTITY(1,1) NOT NULL,
[CveCliente] int NOT NULL,
[CveVenta] int NOT NULL,
[CveSolicitud] int NULL,
[CveFormaPago] tinyint NOT NULL,
[CveMoneda] int NOT NULL,
[CvePrePoliza] int NULL,
[FechaPago] datetime NOT NULL,
[Importe] decimal(18,2) NOT NULL,
[Referencia] varchar(200) COLLATE Modern_Spanish_CS_AI NULL,
[EsSaldoFavor] bit CONSTRAINT [DF_Vta_Pagos_EsSaldoFavor] DEFAULT ((0)) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Vta_Pagos_EsActivo] DEFAULT ((1)) NOT NULL,
[FechaAlta] datetime CONSTRAINT [DF_Vta_Pagos_FechaAlta] DEFAULT (GETDATE()) NOT NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Vta_Pagos] PRIMARY KEY CLUSTERED ([CvePago]),
CONSTRAINT [CK_Vta_Pagos_Importe] CHECK ([Importe] > (0)),
CONSTRAINT [CK_Vta_Pagos_SaldoFavor] CHECK ([EsSaldoFavor] = (0) OR [CveSolicitud] IS NULL)
);
GO
CREATE TABLE [dbo].[Vta_AplicacionesSaldoFavor] (
[CveAplicacionSaldoFavor] int IDENTITY(1,1) NOT NULL,
[CvePagoOrigen] int NOT NULL,
[CveVentaDestino] int NOT NULL,
[CveVentaDetalleDestino] int NULL,
[CveSolicitud] int NOT NULL,
[CvePrePoliza] int NULL,
[Importe] decimal(18,2) NOT NULL,
[FechaAplicacion] datetime CONSTRAINT [DF_Vta_AplicacionesSaldoFavor_Fecha] DEFAULT (GETDATE()) NOT NULL,
[EsActivo] bit CONSTRAINT [DF_Vta_AplicacionesSaldoFavor_EsActivo] DEFAULT ((1)) NOT NULL,
[CveUsuario] int NOT NULL,
CONSTRAINT [PK_Vta_AplicacionesSaldoFavor] PRIMARY KEY CLUSTERED ([CveAplicacionSaldoFavor]),
CONSTRAINT [CK_Vta_AplicacionesSaldoFavor_Importe] CHECK ([Importe] > (0))
);
GO
/* Relación exacta entre los renglones contables y el movimiento que los originó. */
ALTER TABLE [dbo].[Conta_PrePolizasDetalle] ADD
[CveSolicitud] int NULL,
[CvePago] int NULL,
[CveAplicacionSaldoFavor] int NULL;
GO
/* ==================== LLAVES FORÁNEAS ==================== */
ALTER TABLE [dbo].[Fraccionamientos] ADD CONSTRAINT [FK_Fraccionamientos_Clientes]
FOREIGN KEY ([CveCliente]) REFERENCES [dbo].[Clientes] ([CveCliente]);
GO
ALTER TABLE [dbo].[Fraccionamientos] ADD CONSTRAINT [FK_Fraccionamientos_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[FraccionamientosEtapas] ADD CONSTRAINT [FK_FraccionamientosEtapas_Fraccionamientos]
FOREIGN KEY ([CveFraccionamiento]) REFERENCES [dbo].[Fraccionamientos] ([CveFraccionamiento]);
GO
ALTER TABLE [dbo].[FraccionamientosEtapas] ADD CONSTRAINT [FK_FraccionamientosEtapas_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[FraccionamientosLotes] ADD CONSTRAINT [FK_FraccionamientosLotes_FraccionamientosEtapas]
FOREIGN KEY ([CveFraccionamientoEtapa]) REFERENCES [dbo].[FraccionamientosEtapas] ([CveFraccionamientoEtapa]);
GO
ALTER TABLE [dbo].[FraccionamientosLotes] ADD CONSTRAINT [FK_FraccionamientosLotes_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_TiposTramite] ADD CONSTRAINT [FK_Tra_TiposTramite_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_TiposDocumento] ADD CONSTRAINT [FK_Tra_TiposDocumento_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_Preguntas] ADD CONSTRAINT [FK_Tra_Preguntas_Tra_TiposPregunta]
FOREIGN KEY ([CveTipoPregunta]) REFERENCES [dbo].[Tra_TiposPregunta] ([CveTipoPregunta]);
GO
ALTER TABLE [dbo].[Tra_Preguntas] ADD CONSTRAINT [FK_Tra_Preguntas_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_OpcionesPregunta] ADD CONSTRAINT [FK_Tra_OpcionesPregunta_Tra_Preguntas]
FOREIGN KEY ([CvePregunta]) REFERENCES [dbo].[Tra_Preguntas] ([CvePregunta]);
GO
ALTER TABLE [dbo].[Tra_Solicitudes] ADD CONSTRAINT [FK_Tra_Solicitudes_Clientes]
FOREIGN KEY ([CveCliente]) REFERENCES [dbo].[Clientes] ([CveCliente]);
GO
ALTER TABLE [dbo].[Tra_Solicitudes] ADD CONSTRAINT [FK_Tra_Solicitudes_Fraccionamientos_Cliente]
FOREIGN KEY ([CveFraccionamiento], [CveCliente]) REFERENCES [dbo].[Fraccionamientos] ([CveFraccionamiento], [CveCliente]);
GO
ALTER TABLE [dbo].[Tra_Solicitudes] ADD CONSTRAINT [FK_Tra_Solicitudes_FraccionamientosEtapas]
FOREIGN KEY ([CveFraccionamientoEtapa], [CveFraccionamiento]) REFERENCES [dbo].[FraccionamientosEtapas] ([CveFraccionamientoEtapa], [CveFraccionamiento]);
GO
ALTER TABLE [dbo].[Tra_Solicitudes] ADD CONSTRAINT [FK_Tra_Solicitudes_Tra_TiposTramite]
FOREIGN KEY ([CveTipoTramite]) REFERENCES [dbo].[Tra_TiposTramite] ([CveTipoTramite]);
GO
ALTER TABLE [dbo].[Tra_Solicitudes] ADD CONSTRAINT [FK_Tra_Solicitudes_Tra_EstatusSolicitud]
FOREIGN KEY ([CveEstatusSolicitud]) REFERENCES [dbo].[Tra_EstatusSolicitud] ([CveEstatusSolicitud]);
GO
ALTER TABLE [dbo].[Tra_Solicitudes] ADD CONSTRAINT [FK_Tra_Solicitudes_Sis_Usuarios]
FOREIGN KEY ([CveUsuarioSolicita]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_SolicitudesDependencias] ADD CONSTRAINT [FK_Tra_SolicitudesDependencias_Solicitud]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Tra_SolicitudesDependencias] ADD CONSTRAINT [FK_Tra_SolicitudesDependencias_Requerida]
FOREIGN KEY ([CveSolicitudRequerida]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Tra_SolicitudesDependencias] ADD CONSTRAINT [FK_Tra_SolicitudesDependencias_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_HistorialEstatusSolicitud] ADD CONSTRAINT [FK_Tra_HistorialEstatusSolicitud_Solicitudes]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Tra_HistorialEstatusSolicitud] ADD CONSTRAINT [FK_Tra_HistorialEstatusSolicitud_Estatus]
FOREIGN KEY ([CveEstatusSolicitud]) REFERENCES [dbo].[Tra_EstatusSolicitud] ([CveEstatusSolicitud]);
GO
ALTER TABLE [dbo].[Tra_HistorialEstatusSolicitud] ADD CONSTRAINT [FK_Tra_HistorialEstatusSolicitud_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_DocumentosRequeridos] ADD CONSTRAINT [FK_Tra_DocumentosRequeridos_TiposTramite]
FOREIGN KEY ([CveTipoTramite]) REFERENCES [dbo].[Tra_TiposTramite] ([CveTipoTramite]);
GO
ALTER TABLE [dbo].[Tra_DocumentosRequeridos] ADD CONSTRAINT [FK_Tra_DocumentosRequeridos_TiposDocumento]
FOREIGN KEY ([CveTipoDocumento]) REFERENCES [dbo].[Tra_TiposDocumento] ([CveTipoDocumento]);
GO
ALTER TABLE [dbo].[Tra_DocumentosSolicitud] ADD CONSTRAINT [FK_Tra_DocumentosSolicitud_Solicitudes]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Tra_DocumentosSolicitud] ADD CONSTRAINT [FK_Tra_DocumentosSolicitud_TiposDocumento]
FOREIGN KEY ([CveTipoDocumento]) REFERENCES [dbo].[Tra_TiposDocumento] ([CveTipoDocumento]);
GO
ALTER TABLE [dbo].[Tra_DocumentosSolicitud] ADD CONSTRAINT [FK_Tra_DocumentosSolicitud_UsuarioCarga]
FOREIGN KEY ([CveUsuarioCarga]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_DocumentosSolicitud] ADD CONSTRAINT [FK_Tra_DocumentosSolicitud_UsuarioRevisa]
FOREIGN KEY ([CveUsuarioRevisa]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_PreguntasTramite] ADD CONSTRAINT [FK_Tra_PreguntasTramite_TiposTramite]
FOREIGN KEY ([CveTipoTramite]) REFERENCES [dbo].[Tra_TiposTramite] ([CveTipoTramite]);
GO
ALTER TABLE [dbo].[Tra_PreguntasTramite] ADD CONSTRAINT [FK_Tra_PreguntasTramite_Preguntas]
FOREIGN KEY ([CvePregunta]) REFERENCES [dbo].[Tra_Preguntas] ([CvePregunta]);
GO
ALTER TABLE [dbo].[Tra_RespuestasSolicitud] ADD CONSTRAINT [FK_Tra_RespuestasSolicitud_Solicitudes]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Tra_RespuestasSolicitud] ADD CONSTRAINT [FK_Tra_RespuestasSolicitud_Preguntas]
FOREIGN KEY ([CvePregunta]) REFERENCES [dbo].[Tra_Preguntas] ([CvePregunta]);
GO
ALTER TABLE [dbo].[Tra_RespuestasSolicitud] ADD CONSTRAINT [FK_Tra_RespuestasSolicitud_Opciones]
FOREIGN KEY ([CveOpcionPregunta]) REFERENCES [dbo].[Tra_OpcionesPregunta] ([CveOpcionPregunta]);
GO
ALTER TABLE [dbo].[Tra_RespuestasSolicitud] ADD CONSTRAINT [FK_Tra_RespuestasSolicitud_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_CostosTramite] ADD CONSTRAINT [FK_Tra_CostosTramite_TiposTramite]
FOREIGN KEY ([CveTipoTramite]) REFERENCES [dbo].[Tra_TiposTramite] ([CveTipoTramite]);
GO
ALTER TABLE [dbo].[Tra_CostosTramite] ADD CONSTRAINT [FK_Tra_CostosTramite_ProductosServicios]
FOREIGN KEY ([CvePS]) REFERENCES [dbo].[ProductosServicios] ([CvePS]);
GO
ALTER TABLE [dbo].[Tra_CostosTramite] ADD CONSTRAINT [FK_Tra_CostosTramite_Gral_Monedas]
FOREIGN KEY ([CveMoneda]) REFERENCES [dbo].[Gral_Monedas] ([CveMoneda]);
GO
ALTER TABLE [dbo].[Tra_CostosTramite] ADD CONSTRAINT [FK_Tra_CostosTramite_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Tra_NotasSolicitud] ADD CONSTRAINT [FK_Tra_NotasSolicitud_Solicitudes]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Tra_NotasSolicitud] ADD CONSTRAINT [FK_Tra_NotasSolicitud_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[VentasDetalle] ADD CONSTRAINT [FK_VentasDetalle_Ventas]
FOREIGN KEY ([CveVenta]) REFERENCES [dbo].[Ventas] ([CveVenta]);
GO
ALTER TABLE [dbo].[VentasDetalle] ADD CONSTRAINT [FK_VentasDetalle_ProductosServicios]
FOREIGN KEY ([CvePS]) REFERENCES [dbo].[ProductosServicios] ([CvePS]);
GO
ALTER TABLE [dbo].[VentasDetalle] ADD CONSTRAINT [FK_VentasDetalle_Tra_Solicitudes]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[VentasDetalle] ADD CONSTRAINT [FK_VentasDetalle_Tra_CostosTramite]
FOREIGN KEY ([CveCostoTramite]) REFERENCES [dbo].[Tra_CostosTramite] ([CveCostoTramite]);
GO
ALTER TABLE [dbo].[VentasDetalle] ADD CONSTRAINT [FK_VentasDetalle_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Vta_Pagos] ADD CONSTRAINT [FK_Vta_Pagos_Clientes]
FOREIGN KEY ([CveCliente]) REFERENCES [dbo].[Clientes] ([CveCliente]);
GO
ALTER TABLE [dbo].[Vta_Pagos] ADD CONSTRAINT [FK_Vta_Pagos_Ventas]
FOREIGN KEY ([CveVenta]) REFERENCES [dbo].[Ventas] ([CveVenta]);
GO
ALTER TABLE [dbo].[Vta_Pagos] ADD CONSTRAINT [FK_Vta_Pagos_Tra_Solicitudes]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Vta_Pagos] ADD CONSTRAINT [FK_Vta_Pagos_Vta_FormaPago]
FOREIGN KEY ([CveFormaPago]) REFERENCES [dbo].[Vta_FormaPago] ([CveFormaPago]);
GO
ALTER TABLE [dbo].[Vta_Pagos] ADD CONSTRAINT [FK_Vta_Pagos_Gral_Monedas]
FOREIGN KEY ([CveMoneda]) REFERENCES [dbo].[Gral_Monedas] ([CveMoneda]);
GO
ALTER TABLE [dbo].[Vta_Pagos] ADD CONSTRAINT [FK_Vta_Pagos_Conta_PrePolizas]
FOREIGN KEY ([CvePrePoliza]) REFERENCES [dbo].[Conta_PrePolizas] ([CvePrePoliza]);
GO
ALTER TABLE [dbo].[Vta_Pagos] ADD CONSTRAINT [FK_Vta_Pagos_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Vta_AplicacionesSaldoFavor] ADD CONSTRAINT [FK_Vta_AplicacionesSaldoFavor_PagoOrigen]
FOREIGN KEY ([CvePagoOrigen]) REFERENCES [dbo].[Vta_Pagos] ([CvePago]);
GO
ALTER TABLE [dbo].[Vta_AplicacionesSaldoFavor] ADD CONSTRAINT [FK_Vta_AplicacionesSaldoFavor_VentaDestino]
FOREIGN KEY ([CveVentaDestino]) REFERENCES [dbo].[Ventas] ([CveVenta]);
GO
ALTER TABLE [dbo].[Vta_AplicacionesSaldoFavor] ADD CONSTRAINT [FK_Vta_AplicacionesSaldoFavor_DetalleDestino]
FOREIGN KEY ([CveVentaDetalleDestino]) REFERENCES [dbo].[VentasDetalle] ([CveVentaDetalle]);
GO
ALTER TABLE [dbo].[Vta_AplicacionesSaldoFavor] ADD CONSTRAINT [FK_Vta_AplicacionesSaldoFavor_Solicitud]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Vta_AplicacionesSaldoFavor] ADD CONSTRAINT [FK_Vta_AplicacionesSaldoFavor_PrePoliza]
FOREIGN KEY ([CvePrePoliza]) REFERENCES [dbo].[Conta_PrePolizas] ([CvePrePoliza]);
GO
ALTER TABLE [dbo].[Vta_AplicacionesSaldoFavor] ADD CONSTRAINT [FK_Vta_AplicacionesSaldoFavor_Sis_Usuarios]
FOREIGN KEY ([CveUsuario]) REFERENCES [dbo].[Sis_Usuarios] ([CveUsuario]);
GO
ALTER TABLE [dbo].[Conta_PrePolizasDetalle] ADD CONSTRAINT [FK_Conta_PrePolizasDetalle_Tra_Solicitudes]
FOREIGN KEY ([CveSolicitud]) REFERENCES [dbo].[Tra_Solicitudes] ([CveSolicitud]);
GO
ALTER TABLE [dbo].[Conta_PrePolizasDetalle] ADD CONSTRAINT [FK_Conta_PrePolizasDetalle_Vta_Pagos]
FOREIGN KEY ([CvePago]) REFERENCES [dbo].[Vta_Pagos] ([CvePago]);
GO
ALTER TABLE [dbo].[Conta_PrePolizasDetalle] ADD CONSTRAINT [FK_Conta_PrePolizasDetalle_Vta_AplicacionesSaldoFavor]
FOREIGN KEY ([CveAplicacionSaldoFavor]) REFERENCES [dbo].[Vta_AplicacionesSaldoFavor] ([CveAplicacionSaldoFavor]);
GO
/* ==================== ÍNDICES DE OPERACIÓN ==================== */
CREATE NONCLUSTERED INDEX [IX_Tra_Solicitudes_Cliente_Fraccionamiento_Etapa]
ON [dbo].[Tra_Solicitudes] ([CveCliente], [CveFraccionamiento], [CveFraccionamientoEtapa]);
GO
CREATE NONCLUSTERED INDEX [IX_Tra_Solicitudes_Estatus]
ON [dbo].[Tra_Solicitudes] ([CveEstatusSolicitud], [FechaAlta]);
GO
CREATE NONCLUSTERED INDEX [IX_Tra_HistorialEstatusSolicitud_Solicitud_Fecha]
ON [dbo].[Tra_HistorialEstatusSolicitud] ([CveSolicitud], [FechaAlta]);
GO
CREATE NONCLUSTERED INDEX [IX_VentasDetalle_Solicitud]
ON [dbo].[VentasDetalle] ([CveSolicitud]) INCLUDE ([CveVenta], [CvePS], [Importe]);
GO
CREATE NONCLUSTERED INDEX [IX_Vta_Pagos_Cliente_Venta]
ON [dbo].[Vta_Pagos] ([CveCliente], [CveVenta], [EsActivo]);
GO
CREATE NONCLUSTERED INDEX [IX_Vta_AplicacionesSaldoFavor_PagoOrigen]
ON [dbo].[Vta_AplicacionesSaldoFavor] ([CvePagoOrigen], [EsActivo]) INCLUDE ([Importe], [CveSolicitud]);
GO
/* El saldo nunca se almacena: se calcula del pago recibido menos sus aplicaciones activas. */
CREATE VIEW [dbo].[Vta_SaldosFavor]
AS
SELECT
P.[CvePago],
P.[CveCliente],
P.[CveVenta],
P.[CveMoneda],
P.[Importe] AS [ImporteRecibido],
ISNULL(SUM(CASE WHEN A.[EsActivo] = (1) THEN A.[Importe] ELSE (0) END), (0)) AS [ImporteAplicado],
P.[Importe] - ISNULL(SUM(CASE WHEN A.[EsActivo] = (1) THEN A.[Importe] ELSE (0) END), (0)) AS [SaldoDisponible]
FROM [dbo].[Vta_Pagos] AS P
LEFT JOIN [dbo].[Vta_AplicacionesSaldoFavor] AS A
ON A.[CvePagoOrigen] = P.[CvePago]
WHERE P.[EsSaldoFavor] = (1)
AND P.[EsActivo] = (1)
GROUP BY P.[CvePago], P.[CveCliente], P.[CveVenta], P.[CveMoneda], P.[Importe];
GO

View File

@@ -0,0 +1,20 @@
# Run these commands in PowerShell.
# Adjust $OldProject only if your previous project is in another folder.
$Target = "C:\Proyectos\Agua de hermosillo\Plataforma de Desarrolladores"
$OldProject = "C:\Ruta\ProyectoAnterior\Plataforma de Desarrolladores"
New-Item -ItemType Directory -Force -Path $Target | Out-Null
# Extract this ZIP into $Target first, or copy the SIG.WebForms.Starter folder content there.
# Copy existing static folders exactly as-is:
robocopy "$OldProject\Styles" "$Target\SIG.Web\Styles" /E
robocopy "$OldProject\Scripts" "$Target\SIG.Web\Scripts" /E
robocopy "$OldProject\Metronic" "$Target\SIG.Web\Metronic" /E
# Open the solution:
Start-Process "$Target\SIG.WebForms.Starter.sln"
# In Visual Studio Package Manager Console, run:
# Update-Package -Reinstall

View File

@@ -0,0 +1,423 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_UsuarioGetByName]
@NombreUsuario varchar(100),
@CveUsuarioEjecuta int,
@ErrorMostrar varchar(max) OUTPUT,
@ErrorTipo int OUTPUT,
@Return bit OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT @ErrorMostrar = NULL, @ErrorTipo = 0, @Return = 0;
BEGIN TRY
SELECT TOP (1)
U.[CveUsuario], U.[NombreUsuario], U.[HashContrasena], U.[CorreoElectronico],
U.[CveRol], R.[NombreRol], U.[Activo], U.[DebeCambiarContrasena],
U.[IntentosFallidosInicioSesion], U.[BloqueadoHasta],
U.[FechaUltimoInicioSesion], U.[FechaUltimaConexion], U.[CorreoVerificado]
FROM [dbo].[Sis_Usuarios] AS U INNER JOIN [dbo].[Sis_Roles] AS R ON R.[CveRol] = U.[CveRol]
WHERE U.[NombreUsuario] = @NombreUsuario
AND U.[Borrado] = (0)
AND R.[Borrado] = (0);
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al consultar el usuario; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_UsuarioGetByName'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_UsuarioLoginSuccessUpdate]
@CveUsuario int,
@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
UPDATE [dbo].[Sis_Usuarios]
SET [FechaUltimoInicioSesion] = GETDATE(),
[FechaUltimaConexion] = GETDATE(),
[IntentosFallidosInicioSesion] = (0),
[BloqueadoHasta] = NULL,
[FechaActualizacion] = GETDATE()
WHERE [CveUsuario] = @CveUsuario
AND [Borrado] = (0);
IF @@ROWCOUNT = (0)
BEGIN
SET @ErrorMostrar = 'El usuario ya no está disponible.';
SET @ErrorTipo = 2;
RETURN;
END;
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al actualizar el inicio de sesión; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_UsuarioLoginSuccessUpdate'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_UsuarioLoginFailedUpdate]
@CveUsuario int,
@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
UPDATE [dbo].[Sis_Usuarios]
SET [IntentosFallidosInicioSesion] = [IntentosFallidosInicioSesion] + (1),
[BloqueadoHasta] = CASE
WHEN [IntentosFallidosInicioSesion] + (1) >= (5)
THEN DATEADD(MINUTE, (15), GETDATE())
ELSE [BloqueadoHasta]
END,
[FechaActualizacion] = GETDATE()
WHERE [CveUsuario] = @CveUsuario
AND [Borrado] = (0);
IF @@ROWCOUNT = (0)
BEGIN
SET @ErrorMostrar = 'El usuario ya no está disponible.';
SET @ErrorTipo = 2;
RETURN;
END;
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al registrar el intento de acceso; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_UsuarioLoginFailedUpdate'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_IntentoInicioSesionAdd]
@CveUsuario int = NULL,
@NombreUsuario varchar(100),
@Exitoso bit,
@MotivoFallo varchar(200) = NULL,
@DireccionIP varchar(50) = NULL,
@AgenteUsuario varchar(1000) = NULL,
@NombreDominio varchar(255) = NULL,
@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
INSERT INTO [dbo].[Sis_IntentosInicioSesion]
([CveUsuario], [NombreUsuario], [Exitoso], [MotivoFallo],
[DireccionIP], [AgenteUsuario], [NombreDominio])
VALUES
(@CveUsuario, @NombreUsuario, @Exitoso, @MotivoFallo,
@DireccionIP, @AgenteUsuario, @NombreDominio);
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al registrar el intento de acceso; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_IntentoInicioSesionAdd'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_UsuarioSesionAdd]
@CveUsuario int,
@TokenSesion varchar(200),
@DireccionIP varchar(50) = NULL,
@AgenteUsuario varchar(1000) = NULL,
@NombreDominio varchar(255) = NULL,
@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
IF NOT EXISTS
(
SELECT 1 FROM [dbo].[Sis_Usuarios]
WHERE [CveUsuario] = @CveUsuario AND [Activo] = (1) AND [Borrado] = (0)
)
BEGIN
SET @ErrorMostrar = 'El usuario no está disponible para iniciar sesión.';
SET @ErrorTipo = 2;
RETURN;
END;
INSERT INTO [dbo].[Sis_UsuariosSesiones]
([CveUsuario], [TokenSesion], [DireccionIP], [AgenteUsuario],
[NombreDominio], [FechaUltimaActividad])
VALUES
(@CveUsuario, @TokenSesion, @DireccionIP, @AgenteUsuario,
@NombreDominio, GETDATE());
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al crear la sesión; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_UsuarioSesionAdd'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_UsuarioSesionEnd]
@TokenSesion varchar(200),
@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
UPDATE [dbo].[Sis_UsuariosSesiones]
SET [FechaFin] = GETDATE(), [Activo] = (0)
WHERE [TokenSesion] = @TokenSesion
AND [Borrado] = (0);
/* Cerrar una sesión inexistente o ya cerrada es una operación idempotente. */
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al cerrar la sesión; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_UsuarioSesionEnd'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_PermisoCheck]
@CodigoPermiso varchar(100),
@CveUsuarioEjecuta int,
@ErrorMostrar varchar(max) OUTPUT,
@ErrorTipo int OUTPUT,
@Return bit OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT @ErrorMostrar = NULL, @ErrorTipo = 0, @Return = 0;
BEGIN TRY
DECLARE @CveRol int;
DECLARE @PermisoUsuario bit;
SELECT @CveRol = [CveRol]
FROM [dbo].[Sis_Usuarios]
WHERE [CveUsuario] = @CveUsuarioEjecuta
AND [Activo] = (1)
AND [Borrado] = (0);
SELECT TOP (1) @PermisoUsuario = UP.[Permitido]
FROM [dbo].[Sis_UsuariosPermisos] AS UP
INNER JOIN [dbo].[Sis_Permisos] AS P ON P.[CvePermiso] = UP.[CvePermiso]
WHERE UP.[CveUsuario] = @CveUsuarioEjecuta
AND P.[CodigoPermiso] = @CodigoPermiso
AND P.[Activo] = (1)
AND UP.[Borrado] = (0)
AND P.[Borrado] = (0);
IF @PermisoUsuario IS NOT NULL
SELECT CAST(@PermisoUsuario AS bit) AS [Permitido];
ELSE IF EXISTS
(
SELECT 1
FROM [dbo].[Sis_RolesPermisos] AS RP
INNER JOIN [dbo].[Sis_Permisos] AS P ON P.[CvePermiso] = RP.[CvePermiso]
WHERE RP.[CveRol] = @CveRol
AND P.[CodigoPermiso] = @CodigoPermiso
AND RP.[Permitido] = (1)
AND P.[Activo] = (1)
AND RP.[Borrado] = (0)
AND P.[Borrado] = (0)
)
SELECT CAST((1) AS bit) AS [Permitido];
ELSE
SELECT CAST((0) AS bit) AS [Permitido];
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al consultar los permisos; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_PermisoCheck'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_PaginaAccessCheck]
@PaginaUrl varchar(500),
@CveUsuarioEjecuta int,
@ErrorMostrar varchar(max) OUTPUT,
@ErrorTipo int OUTPUT,
@Return bit OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT @ErrorMostrar = NULL, @ErrorTipo = 0, @Return = 0;
BEGIN TRY
DECLARE @CveRol int;
DECLARE @PuedeAcceder bit = (0);
SELECT @CveRol = [CveRol]
FROM [dbo].[Sis_Usuarios]
WHERE [CveUsuario] = @CveUsuarioEjecuta
AND [Activo] = (1)
AND [Borrado] = (0);
IF EXISTS
(
SELECT 1
FROM [dbo].[Sis_Modulos] AS M
INNER JOIN [dbo].[Sis_RolesModulos] AS RM ON RM.[CveModulo] = M.[CveModulo]
WHERE RM.[CveRol] = @CveRol
AND RM.[PuedeVer] = (1)
AND M.[Activo] = (1)
AND M.[Visible] = (1)
AND RM.[Borrado] = (0)
AND M.[Borrado] = (0)
AND
(
LOWER(REPLACE(M.[Url], '~/', '')) = LOWER(@PaginaUrl)
OR LOWER(REPLACE(M.[Url], '/', '')) = LOWER(REPLACE(@PaginaUrl, '/', ''))
)
)
SET @PuedeAcceder = (1);
ELSE IF EXISTS
(
SELECT 1
FROM [dbo].[Sis_Modulos] AS M
WHERE M.[Activo] = (1)
AND M.[Borrado] = (0)
AND LOWER(REPLACE(M.[Url], '~/', '')) = LOWER(@PaginaUrl)
)
SET @PuedeAcceder = (0);
ELSE IF EXISTS
(
SELECT 1
FROM [dbo].[Sis_UsuariosPermisos] AS UP
INNER JOIN [dbo].[Sis_Permisos] AS P ON P.[CvePermiso] = UP.[CvePermiso]
WHERE UP.[CveUsuario] = @CveUsuarioEjecuta
AND P.[CodigoPermiso] = 'System.Access'
AND UP.[Permitido] = (1)
AND P.[Activo] = (1)
AND UP.[Borrado] = (0)
AND P.[Borrado] = (0)
)
SET @PuedeAcceder = (1);
ELSE IF EXISTS
(
SELECT 1
FROM [dbo].[Sis_RolesPermisos] AS RP
INNER JOIN [dbo].[Sis_Permisos] AS P ON P.[CvePermiso] = RP.[CvePermiso]
WHERE RP.[CveRol] = @CveRol
AND P.[CodigoPermiso] = 'System.Access'
AND RP.[Permitido] = (1)
AND P.[Activo] = (1)
AND RP.[Borrado] = (0)
AND P.[Borrado] = (0)
)
SET @PuedeAcceder = (1);
SELECT @PuedeAcceder AS [PuedeAcceder];
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al validar el acceso; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_PaginaAccessCheck'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE [dbo].[PA_Sis_MenuList]
@CveRol int,
@CveUsuarioEjecuta int,
@ErrorMostrar varchar(max) OUTPUT,
@ErrorTipo int OUTPUT,
@Return bit OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT @ErrorMostrar = NULL, @ErrorTipo = 0, @Return = 0;
BEGIN TRY
SELECT DISTINCT
M.[CveModulo], M.[CveModuloPadre], M.[NombreModulo], M.[Url],
M.[ClaseCssIcono], M.[ClaseCssEstilo], M.[OrdenVisualizacion], M.[Visible]
FROM [dbo].[Sis_Modulos] AS M
INNER JOIN [dbo].[Sis_RolesModulos] AS RM ON RM.[CveModulo] = M.[CveModulo]
WHERE RM.[CveRol] = @CveRol
AND RM.[PuedeVer] = (1)
AND M.[Activo] = (1)
AND M.[Visible] = (1)
AND RM.[Borrado] = (0)
AND M.[Borrado] = (0)
ORDER BY M.[OrdenVisualizacion], M.[NombreModulo];
SET @Return = 1;
END TRY
BEGIN CATCH
SET @ErrorMostrar = 'Ocurrió un error al consultar el menú; favor de contactar a Sistemas.';
SET @ErrorTipo = 1;
SET @Return = 0;
DECLARE @ErrorTecnico nvarchar(2048) = CONCAT('Número: ', ERROR_NUMBER(), '; Procedimiento: ', COALESCE(ERROR_PROCEDURE(), 'PA_Sis_MenuList'), '; Línea: ', ERROR_LINE(), '; Detalle: ', ERROR_MESSAGE());
RAISERROR(N'%s', 10, 1, @ErrorTecnico);
END CATCH;
END;
GO

100
README.md Normal file
View File

@@ -0,0 +1,100 @@
# Plataforma de Desarrolladores Agua de Hermosillo
Aplicación ASP.NET WebForms para administrar desarrolladores, clientes, fraccionamientos, trámites, pagos y su futura integración contable.
## Tecnología
- ASP.NET WebForms con C#.
- .NET Framework 4.8.1.
- SQL Server y procedimientos almacenados.
- Metronic como base visual.
- AjaxControlToolkit y `UpdatePanel` para actualizaciones parciales.
- Visual Studio 2022.
## Estructura principal
```text
SIG.Web/
AppCode/
Contracts/
Models/
Repositories/
Services/
Security/
Utilities/
Pages/
Security/
Tramites/
Polizas/
Metronic/
Scripts/
Styles/
Site.master
Login.aspx
Inicio.aspx
Web.config
```
Las páginas y la página maestra consumen contratos y servicios. El acceso a SQL se mantiene en repositorios y ejecutores de base de datos.
```text
ASPX / Site.master
-> Contracts
-> Services
-> Repositories / SQL helpers
-> SQL Server
```
## Requisitos
1. Windows con IIS Express.
2. Visual Studio 2022 con la carga de trabajo **Desarrollo de ASP.NET y web**.
3. .NET Framework 4.8.1 Developer Pack.
4. SQL Server accesible desde el equipo local.
## Configuración local
1. Abrir `SIG.WebForms.Starter.sln` en Visual Studio 2022.
2. Restaurar los paquetes NuGet.
3. Revisar la cadena `Conexion` de `SIG.Web/Web.config`.
4. Crear o actualizar la base de datos con `Esquema_BD_AguaHillo_Idempotente.sql`.
5. Para instalar solamente usuarios, clientes y verificación de correo, utilizar `Usuarios_Clientes_Registro.sql`.
6. Compilar la solución en `Debug` y ejecutarla con IIS Express.
La ruta raíz utiliza `Inicio.aspx` como documento predeterminado. Los usuarios sin sesión son enviados a `Login.aspx`, conservando la ruta solicitada en `retornoURL`.
## Correo de verificación
El autorregistro requiere configurar estos valores en `SIG.Web/Web.config`:
```xml
<add key="SmtpHost" value="" />
<add key="SmtpPort" value="587" />
<add key="SmtpEnableSsl" value="true" />
<add key="SmtpUser" value="" />
<add key="SmtpPassword" value="" />
<add key="EmailFrom" value="" />
```
No deben confirmarse credenciales reales en el repositorio. Para cada ambiente, coloque los valores mediante una transformación segura o durante el despliegue.
## Módulo de usuarios
- `Pages/Security/Usuarios.aspx`: administración autenticada de usuarios y clientes relacionados.
- `Pages/Security/RegistroCliente.aspx`: autorregistro público en dos pasos.
- `Pages/Security/RegistroPendiente.aspx`: aviso para revisar el correo.
- `Pages/Security/VerificarCorreo.aspx`: validación de la liga y activación de la cuenta.
El registro conserva temporalmente los datos de ambos pasos en `ViewState` cifrado. Usuario, cliente, relación y token se guardan juntos únicamente después de concluir las validaciones.
## Reglas de base de datos
- Las operaciones desde C# utilizan procedimientos almacenados.
- Los procedimientos incluyen `TRY/CATCH`, `@ErrorMostrar`, `@ErrorTipo` y `@Return`.
- Las entradas se envían como parámetros; no se concatena información del usuario en SQL.
- El borrado funcional es lógico mediante `Borrado` y `FechaBorrado`.
- Los errores registran el procedimiento y parámetros de diagnóstico, ocultando valores sensibles.
## Archivos que no se versionan
El `.gitignore` excluye compilados, paquetes restaurables, configuración personal de Visual Studio, logs, bases locales, temporales, publicaciones y carpetas de trabajo local. Las carpetas visuales `Metronic`, `Styles` y `Scripts` sí forman parte del proyecto.

105
SIG.Web/AccessDenied.aspx Normal file
View File

@@ -0,0 +1,105 @@
<%@ Page Title="Acceso denegado" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeBehind="AccessDenied.aspx.cs" Inherits="SIG.Web.AccessDenied" %>
<asp:Content ID="ContentHead" ContentPlaceHolderID="H" runat="server">
<style type="text/css">
.access-denied-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 190px);
padding: 30px 15px;
}
.access-denied-card {
width: 100%;
max-width: 680px;
padding: 42px 38px;
background: #ffffff;
border-top: 4px solid #e7505a;
box-shadow: 0 4px 18px rgba(0, 0, 0, .10);
text-align: center;
}
.access-denied-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 84px;
height: 84px;
margin-bottom: 18px;
border-radius: 50%;
color: #e7505a;
background: #fbeaec;
font-size: 38px;
}
.access-denied-code {
margin: 0 0 4px;
color: #e7505a;
font-size: 18px;
font-weight: 700;
letter-spacing: 2px;
}
.access-denied-title {
margin: 0 0 14px;
color: #34495e;
font-size: 30px;
font-weight: 600;
}
.access-denied-message {
max-width: 520px;
margin: 0 auto 28px;
color: #697882;
font-size: 15px;
line-height: 1.7;
}
.access-denied-actions .btn {
margin: 4px;
}
@media (max-width: 600px) {
.access-denied-card {
padding: 32px 20px;
}
.access-denied-title {
font-size: 25px;
}
}
</style>
</asp:Content>
<asp:Content ID="ContentMain" ContentPlaceHolderID="C" runat="server">
<div class="access-denied-wrapper">
<section class="access-denied-card" aria-labelledby="accessDeniedTitle">
<div class="access-denied-icon" aria-hidden="true">
<i class="fa fa-lock"></i>
</div>
<p class="access-denied-code">ERROR 403</p>
<h1 id="accessDeniedTitle" class="access-denied-title">Acceso denegado</h1>
<p class="access-denied-message">
Tu sesión está activa, pero tu usuario no cuenta con permiso para consultar esta página.
Si necesitas acceso, solicita la autorización correspondiente al administrador del sistema.
</p>
<div class="access-denied-actions">
<a class="btn blue" href="<%= ResolveUrl("~/Inicio.aspx") %>">
<i class="fa fa-home"></i> Ir al inicio
</a>
<button type="button" class="btn default" onclick="window.history.back();">
<i class="fa fa-arrow-left"></i> Regresar
</button>
<a class="btn red" href="<%= ResolveUrl("~/Logout.aspx") %>">
<i class="fa fa-sign-out"></i> Cerrar sesión
</a>
</div>
</section>
</div>
</asp:Content>
<asp:Content ID="ContentScripts" ContentPlaceHolderID="J" runat="server">
</asp:Content>

View File

@@ -0,0 +1,14 @@
using System;
using System.Web.UI;
namespace SIG.Web
{
public partial class AccessDenied : Page
{
protected void Page_Load(object sender, EventArgs e)
{
Response.StatusCode = 403;
Response.TrySkipIisCustomErrors = true;
}
}
}

6
SIG.Web/AccessDenied.aspx.designer.cs generated Normal file
View File

@@ -0,0 +1,6 @@
namespace SIG.Web
{
public partial class AccessDenied
{
}
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8" ?>
<ajaxControlToolkit>
<controlBundles>
<controlBundle name="Accordion">
<control name="AccordionExtender"></control>
<control name="CollapsiblePanelExtender"></control>
</controlBundle>
<!--<controlBundle name="CustomControls">
<control name="AjaxControlToolkitWelcomeLabel.ActWelcomeLabel" assembly="CustomAjaxServerControlSample"></control>
<control name="CollapsiblePanelExtender"></control>
</controlBundle>-->
<controlBundle>
<control name="AjaxFileUpload"></control>
<control name="AlwaysVisibleControlExtender"></control>
<control name="AreaChart"></control>
<control name="AsyncFileUpload"></control>
<control name="AutoCompleteExtender"></control>
<control name="BalloonPopupExtender"></control>
<control name="BarChart"></control>
<control name="BubbleChart"></control>
<control name="CalendarExtender"></control>
<control name="CascadingDropDown"></control>
<control name="CollapsiblePanelExtender"></control>
<control name="ColorPickerExtender"></control>
<control name="ComboBox"></control>
<control name="ConfirmButtonExtender"></control>
<control name="DragPanelExtender"></control>
<control name="DropDownExtender"></control>
<control name="DropShadowExtender"></control>
<control name="DynamicPopulateExtender"></control>
<control name="FilteredTextBoxExtender"></control>
<control name="Gravatar"></control>
<control name="HoverMenuExtender"></control>
<control name="HtmlEditor.Editor"></control>
<control name="HtmlEditorExtender"></control>
<control name="LineChart"></control>
<control name="ListSearchExtender"></control>
<control name="MaskedEditExtender"></control>
<control name="ModalPopupExtender"></control>
<control name="MultiHandleSliderExtender"></control>
<control name="MutuallyExclusiveCheckBoxExtender"></control>
<control name="NoBotExtender"></control>
<control name="NumericUpDownExtender"></control>
<control name="PagingBulletedListExtender"></control>
<control name="PasswordStrength"></control>
<control name="PieChart"></control>
<control name="PopupControlExtender"></control>
<control name="RatingExtender"></control>
<control name="ReorderList"></control>
<control name="ResizableControlExtender"></control>
<control name="RoundedCornersExtender"></control>
<control name="Seadragon"></control>
<control name="SliderExtender"></control>
<control name="SlideShowExtender"></control>
<control name="TabContainer"></control>
<control name="TextBoxWatermarkExtender"></control>
<control name="ToggleButtonExtender"></control>
<control name="Twitter"></control>
<control name="UpdatePanelAnimationExtender"></control>
<control name="ValidatorCalloutExtender"></control>
</controlBundle>
</controlBundles>
</ajaxControlToolkit>

View File

@@ -0,0 +1,59 @@
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Contracts.Search;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Notifications.Contracts;
using SIG.Web.AppCode.Notifications.Services;
using SIG.Web.AppCode.Repositories.Security;
using SIG.Web.AppCode.Services.Database;
using SIG.Web.AppCode.Services.Menu;
using SIG.Web.AppCode.Services.Search;
using SIG.Web.AppCode.Services.Security;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Configuration
{
public static class AppServices
{
public static IDbConnectionFactory DbConnectionFactory { get; private set; }
public static IDbExecutor DbExecutor { get; private set; }
public static IPasswordHasher PasswordHasher { get; private set; }
public static IUserRepository UserRepository { get; private set; }
public static IUserAdministrationRepository UserAdministrationRepository { get; private set; }
public static IPublicRegistrationService PublicRegistrationService { get; private set; }
public static ILoginAttemptRepository LoginAttemptRepository { get; private set; } public static IUserSessionRepository UserSessionRepository { get; private set; }
public static ILoginService LoginService { get; private set; }
public static IUserContextProvider UserContext { get; private set; }
public static IPermissionService PermissionService { get; private set; }
public static IMenuProvider MenuProvider { get; private set; }
public static INotificationCenter NotificationCenter { get; private set; }
public static IBuscadorCatalogoDataProvider SearchProvider { get; private set; }
public static void Configure()
{
DbConnectionFactory = new DbConnectionFactory();
DbExecutor = new DbExecutor(DbConnectionFactory);
PasswordHasher = new PasswordHasher();
UserRepository = new UserRepository(DbExecutor);
UserAdministrationRepository = new UserAdministrationRepository(DbExecutor);
LoginAttemptRepository = new LoginAttemptRepository(DbExecutor); UserSessionRepository = new UserSessionRepository(DbExecutor);
UserContext = new WebFormsUserContextProvider();
PermissionService = new PermissionService(DbExecutor);
MenuProvider = new MenuProvider(DbExecutor);
NotificationCenter = new DummyNotificationCenter();
SearchProvider = new DummyBuscadorCatalogoDataProvider();
LoginService = new LoginService(
UserRepository,
PasswordHasher,
LoginAttemptRepository,
UserSessionRepository);
PublicRegistrationService = new PublicRegistrationService(
UserAdministrationRepository,
PasswordHasher,
new SmtpEmailSender());
} }
}

View File

@@ -0,0 +1,127 @@
using System.Web.Optimization;
using System.Web.UI;
namespace SIG.Web.AppCode.Configuration
{
public static class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Metronic/assets/global/plugins/jquery.min.js"
));
ScriptManager.ScriptResourceMapping.AddDefinition("jquery", null, new ScriptResourceDefinition
{
Path = "~/bundles/jquery"
});
bundles.Add(new ScriptBundle("~/bundles/scripts").Include(
"~/Metronic/assets/global/plugins/jquery-ui/jquery-ui.min.js",
"~/Metronic/assets/global/plugins/jquery-migrate.min.js",
"~/Scripts/funciones.js",
"~/Scripts/FixFocus.js",
"~/Scripts/jquery.number.js",
"~/Scripts/jquery.maskMoney.js",
"~/Scripts/jquery.thickbox.js",
"~/Scripts/jquery.fancybox-1.3.4.js",
"~/Scripts/jquery.maskedinput-1.2.2.min.js",
"~/Scripts/raphael.2.1.0.min.js",
"~/Scripts/justgage.1.0.1.min.js",
"~/Scripts/multiselect.js",
"~/Scripts/jquery.mask.min.js",
"~/Scripts/gridviewScroll.js",
"~/Metronic/Carusel/slick.min.js"
));
bundles.Add(new ScriptBundle("~/Metronic/assets/global/scripts/JSV1").Include(
"~/Metronic/assets/global/plugins/bootstrap/js/bootstrap.min.js",
"~/Metronic/assets/global/plugins/bootstrap-hover-dropdown/bootstrap-hover-dropdown.min.js",
"~/Metronic/assets/global/plugins/jquery-slimscroll/jquery.slimscroll.min.js",
"~/Metronic/assets/global/plugins/jquery.blockui.min.js",
"~/Metronic/assets/global/plugins/jquery.cokie.min.js",
"~/Metronic/assets/global/plugins/bootstrap-switch/js/bootstrap-switch.min.js",
"~/Metronic/assets/global/plugins/select2/select2.full.min.js",
"~/Metronic/assets/global/plugins/select2/i18n/es.js",
"~/Metronic/assets/global/plugins/bootstrap-confirmation/bootstrap-confirmation.min.js",
"~/Metronic/assets/global/plugins/bootbox/bootbox.min.js",
"~/Metronic/assets/global/plugins/bootstrap-toastr/toastr.min.js",
"~/Metronic/assets/global/scripts/metronic.js",
"~/Metronic/assets/admin/layout/scripts/layout.js",
"~/Metronic/assets/admin/layout/scripts/quick-sidebar.js",
"~/Metronic/assets/admin/layout/scripts/demo.js",
"~/Scripts/porcentaje-box.js",
"~/Scripts/DropdownGrid.js",
"~/Scripts/UcBuscadorCatalogo.js",
"~/Scripts/SigPdfViewer.js",
"~/Scripts/SigPdfDrawer.js",
"~/Scripts/custom.js"
));
bundles.Add(new ScriptBundle("~/bundles/WebFormsJs").Include(
"~/Scripts/WebForms/WebForms.js",
"~/Scripts/WebForms/WebUIValidation.js",
"~/Scripts/WebForms/MenuStandards.js",
"~/Scripts/WebForms/Focus.js",
"~/Scripts/WebForms/GridView.js",
"~/Scripts/WebForms/DetailsView.js",
"~/Scripts/WebForms/TreeView.js",
"~/Scripts/WebForms/WebParts.js"
));
ScriptManager.ScriptResourceMapping.AddDefinition("WebFormsBundle", new ScriptResourceDefinition
{
Path = "~/bundles/WebFormsJs"
});
bundles.Add(new ScriptBundle("~/bundles/MsAjaxJs").Include(
"~/Scripts/WebForms/MsAjax/MicrosoftAjax.js",
"~/Scripts/WebForms/MsAjax/MicrosoftAjaxApplicationServices.js",
"~/Scripts/WebForms/MsAjax/MicrosoftAjaxTimer.js",
"~/Scripts/WebForms/MsAjax/MicrosoftAjaxWebForms.js"
));
ScriptManager.ScriptResourceMapping.AddDefinition("MsAjaxBundle", new ScriptResourceDefinition
{
Path = "~/bundles/MsAjaxJs"
});
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include("~/Scripts/modernizr-*"));
ScriptManager.ScriptResourceMapping.AddDefinition("respond", new ScriptResourceDefinition
{
Path = "~/Scripts/respond.min.js",
DebugPath = "~/Scripts/respond.js"
});
bundles.Add(new StyleBundle("~/Content/AjaxControlToolkit/Styles/Bundle").Include(
"~/Content/AjaxControlToolkit/Styles/AjaxFileUpload.min.css",
"~/Content/AjaxControlToolkit/Styles/Calendar.min.css",
"~/Content/AjaxControlToolkit/Styles/Tabs.min.css",
"~/Content/AjaxControlToolkit/Styles/ValidatorCallout.min.css",
"~/Content/AjaxControlToolkit/Styles/HtmlEditorExtender.min.css"
));
bundles.Add(new StyleBundle("~/Metronic/assets/global/css/cssV1")
.Include("~/Metronic/assets/global/plugins/font-awesome/css/font-awesome.min.css", new CssRewriteUrlTransform())
.Include("~/Metronic/assets/global/plugins/bootstrap/css/bootstrap.min.css", new CssRewriteUrlTransform())
.Include("~/Metronic/assets/global/plugins/simple-line-icons/simple-line-icons.min.css", new CssRewriteUrlTransform())
.Include(
"~/Styles/porcentaje-box.css",
"~/Styles/DropdownGrid.css",
"~/Styles/UcBuscadorCatalogo.css",
"~/Styles/SigPdfViewer.css",
"~/Styles/SigPdfDrawer.css",
"~/Styles/Estilos2.css",
"~/Metronic/assets/global/plugins/bootstrap-switch/css/bootstrap-switch.min.css",
"~/Metronic/assets/global/plugins/bootstrap-toastr/toastr.min.css",
"~/Metronic/assets/global/plugins/select2/select2.css",
"~/Metronic/assets/global/css/components.css",
"~/Metronic/assets/global/css/plugins.css",
"~/Metronic/assets/admin/layout/css/layout.css",
"~/Metronic/assets/admin/layout/css/custom.css")
.Include("~/Metronic/Carusel/slick.css", new CssRewriteUrlTransform())
.Include("~/Metronic/Carusel/slick-theme.css", new CssRewriteUrlTransform()));
}
}
}

View File

@@ -0,0 +1,294 @@
using System;
using System.Data.SqlClient;
using System.Text;
using System.Web;
using SIG.Web.AppCode.Services.Errors;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Configuration
{
public static class ErrorHandlerConfig
{
public static void Register()
{
}
public static void HandleServerError(
Exception exception,
HttpContext context)
{
if (exception == null || context == null)
{
return;
}
try
{
UserSessionModel usuario = ObtenerUsuarioActual();
string cveUsuario = usuario != null
? usuario.CveUsuario.ToString()
: string.Empty;
string nombreUsuario = usuario != null
? usuario.Nombre
: string.Empty;
int cveRole = usuario != null
? usuario.CveRol
: 0;
string codigo = SIGErrorManager.Registrar(
exception,
context,
"Application_Error",
cveUsuario,
nombreUsuario
);
bool esPeticionAsincrona =
SIGErrorManager.EsPeticionAsincrona(context);
/*
* El detalle t<>cnico completo siempre se guarda en log.
* Solo el administrador puede verlo en la respuesta.
*/
bool mostrarDetalle = cveRole == 1;
context.Server.ClearError();
if (esPeticionAsincrona)
{
EscribirErrorAsincrono(
context,
codigo,
exception,
mostrarDetalle
);
return;
}
EscribirPaginaError(
context,
codigo,
exception,
mostrarDetalle,
cveUsuario,
nombreUsuario,
cveRole
);
}
catch (Exception handlerException)
{
try
{
SIGErrorManager.Registrar(
handlerException,
context,
"ErrorHandlerConfig.HandleServerError",
string.Empty,
string.Empty
);
}
catch
{
// El manejador global nunca debe provocar otro error.
}
try
{
context.Server.ClearError();
}
catch
{
}
}
}
public static string GetFriendlyMessage(
Exception exception)
{
if (exception == null)
{
return "Ocurri<72> un error inesperado.";
}
Exception baseException =
exception.GetBaseException();
SqlException sqlException =
baseException as SqlException;
if (sqlException != null)
{
switch (sqlException.Number)
{
case 207:
return "No fue posible completar la operaci<63>n porque falta una columna requerida en la base de datos.";
case 208:
return "No fue posible completar la operaci<63>n porque falta una tabla o vista requerida en la base de datos.";
case 4060:
return "No fue posible abrir la base de datos configurada.";
case 18456:
return "No fue posible iniciar sesi<73>n en la base de datos.";
case -2:
return "La base de datos tard<72> demasiado en responder.";
case 1205:
return "La operaci<63>n no pudo completarse porque ocurri<72> un bloqueo en la base de datos. Intente nuevamente.";
case 547:
return "No fue posible completar la operaci<63>n porque existen registros relacionados.";
case 2627:
case 2601:
return "Ya existe un registro con la misma informaci<63>n.";
default:
return "Ocurri<72> un error al consultar la base de datos.";
}
}
if (baseException is TimeoutException)
{
return "La solicitud tard<72> demasiado en responder. Intente nuevamente.";
}
if (baseException is OperationCanceledException)
{
return "La operaci<63>n fue cancelada antes de completarse.";
}
HttpException httpException =
baseException as HttpException;
if (httpException != null)
{
int statusCode =
httpException.GetHttpCode();
switch (statusCode)
{
case 400:
return "La solicitud enviada no es v<>lida.";
case 401:
return "Su sesi<73>n no tiene autorizaci<63>n para realizar esta operaci<63>n.";
case 403:
return "No tiene permisos para realizar esta operaci<63>n.";
case 404:
return "No se encontr<74> el recurso solicitado.";
case 408:
return "La solicitud tard<72> demasiado en responder.";
case 413:
return "El archivo o contenido enviado supera el tama<6D>o permitido.";
default:
return "Ocurri<72> un error al procesar la solicitud.";
}
}
return "Ocurri<72> un error inesperado. Intente nuevamente.";
}
private static void EscribirErrorAsincrono(
HttpContext context,
string codigo,
Exception exception,
bool mostrarDetalle)
{
string respuesta =
"SIG_ERROR|" + codigo;
if (mostrarDetalle)
{
string detalle =
SIGErrorManager.ObtenerDetalleTecnico(
exception
);
string detalleBase64 =
Convert.ToBase64String(
Encoding.UTF8.GetBytes(detalle)
);
respuesta += "|" + detalleBase64;
}
context.Response.Clear();
context.Response.ClearHeaders();
context.Response.StatusCode = 500;
context.Response.TrySkipIisCustomErrors = true;
context.Response.ContentType =
"text/plain; charset=utf-8";
context.Response.AddHeader(
"X-SIG-Error-Code",
codigo
);
context.Response.Write(respuesta);
context.ApplicationInstance.CompleteRequest();
}
private static void EscribirPaginaError(
HttpContext context,
string codigo,
Exception exception,
bool mostrarDetalle,
string cveUsuario,
string nombreUsuario,
int cveRole)
{
string html =
SIGErrorManager.CrearHtmlError(
codigo,
exception,
mostrarDetalle,
cveUsuario,
nombreUsuario,
cveRole
);
context.Response.Clear();
context.Response.ClearHeaders();
context.Response.StatusCode = 500;
context.Response.TrySkipIisCustomErrors = true;
context.Response.ContentType =
"text/html; charset=utf-8";
context.Response.Write(html);
context.ApplicationInstance.CompleteRequest();
}
private static UserSessionModel ObtenerUsuarioActual()
{
try
{
if (AppServices.UserContext == null)
{
return null;
}
return AppServices.UserContext.GetCurrentUser();
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace SIG.Web.AppCode.Contracts.Data
{
public enum DbProviderKind
{
SqlServer,
PostgreSql
}
}

View File

@@ -0,0 +1,10 @@
namespace SIG.Web.AppCode.Contracts.Data
{
public sealed class DomainConnectionSettings
{
public string Domain { get; set; }
public DbProviderKind Provider { get; set; }
public string ConnectionStringName { get; set; }
public bool IsDefault { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Data;
namespace SIG.Web.AppCode.Contracts.Data
{
public interface IDbConnectionFactory
{
IDbConnection Create(string domain);
}
}

View File

@@ -0,0 +1,7 @@
namespace SIG.Web.AppCode.Contracts.Data
{
public interface IDomainConnectionResolver
{
DomainConnectionSettings Resolve(string domain);
}
}

View File

@@ -0,0 +1,5 @@
using System.Data;
namespace SIG.Web.AppCode.Contracts.Database
{
public interface IDbConnectionFactory { IDbConnection CreateConnection(string domainName); }
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Data;
using SIG.Web.AppCode.Models.Common;
namespace SIG.Web.AppCode.Contracts.Database
{
public interface IDbExecutor
{
DbResult<T> Execute<T>(string domainName, Func<IDbConnection, T> action);
}
}

View File

@@ -0,0 +1,7 @@
namespace SIG.Web.AppCode.Contracts.Login
{
public interface ILoginService
{
LoginResult Login(string userName, string password, string domain);
}
}

View File

@@ -0,0 +1,21 @@
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Contracts.Login
{
public sealed class LoginResult
{
public bool Exitoso { get; private set; }
public string Mensaje { get; private set; }
public UserSessionModel Usuario { get; private set; }
public static LoginResult Ok(UserSessionModel usuario)
{
return new LoginResult { Exitoso = true, Usuario = usuario, Mensaje = "OK" };
}
public static LoginResult Fail(string mensaje)
{
return new LoginResult { Exitoso = false, Mensaje = mensaje };
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace SIG.Web.AppCode.Contracts.Search
{
public sealed class BuscadorCatalogoFiltro
{
public string Campo { get; set; }
public string Operador { get; set; }
public string Valor { get; set; }
}
public sealed class BuscadorCatalogoOrden
{
public string Campo { get; set; }
public string Direccion { get; set; } = "ASC";
}
public sealed class BuscadorCatalogoColumna
{
public string Campo { get; set; }
public string Titulo { get; set; }
public bool Visible { get; set; } = true;
public bool Busqueda { get; set; } = true;
public string CssClass { get; set; }
}
public sealed class BuscadorCatalogoCallbackSolicitud
{
public string Catalogo { get; set; }
public string Termino { get; set; }
public int Pagina { get; set; } = 1;
public int TamanoPagina { get; set; } = 20;
public List<BuscadorCatalogoFiltro> Filtros { get; set; } = new List<BuscadorCatalogoFiltro>();
public List<BuscadorCatalogoOrden> Orden { get; set; } = new List<BuscadorCatalogoOrden>();
}
public sealed class BuscadorCatalogoCallbackRespuesta
{
public bool Success { get; set; } = true;
public string Message { get; set; }
public int Total { get; set; }
public List<Dictionary<string, object>> Rows { get; set; } = new List<Dictionary<string, object>>();
}
}

View File

@@ -0,0 +1,9 @@
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Contracts.Search
{
public interface IBuscadorCatalogoDataProvider
{
BuscadorCatalogoCallbackRespuesta Search(BuscadorCatalogoCallbackSolicitud request, UserSessionModel user);
}
}

View File

@@ -0,0 +1,8 @@
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IEmailSender
{
bool IsConfigured { get; }
void SendVerification(string recipient, string verificationUrl);
}
}

View File

@@ -0,0 +1,9 @@
using SIG.Web.AppCode.Models.Common;
namespace SIG.Web.AppCode.Contracts.Security
{
public interface ILoginAttemptRepository
{
DbResult<bool> Save(int? cveUser, string username, bool wasSuccessful, string failureReason, string ipAddress, string userAgent, string domainName);
}
}

View File

@@ -0,0 +1,9 @@
using SIG.Web.AppCode.Models.Security;
namespace SIG.Web.AppCode.Contracts.Security
{
public interface ILoginService
{
LoginResult Login(LoginRequest request);
void Logout(string sessionToken, string domainName);
}
}

View File

@@ -0,0 +1,8 @@
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IPasswordHasher
{
string HashPassword(string password);
bool VerifyPassword(string password, string storedHash);
}
}

View File

@@ -0,0 +1,8 @@
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IPermissionService
{
bool HasPermission(int cveUser, string permissionCode);
bool CanAccessPage(int cveUser, string pageUrl);
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Models.Security;
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IPublicRegistrationService
{
DbResult<bool> Validate(PublicRegistrationDraft draft, string domainName);
DbResult<List<StateOptionModel>> ListStates(string domainName);
DbResult<bool> Register(PublicRegistrationDraft draft, string verificationBaseUrl, string domainName);
DbResult<bool> Verify(string token, string domainName);
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Models.Security;
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IUserAdministrationRepository
{
DbResult<List<UserAdministrationModel>> ListUsers(string text, int executingUser, string domainName);
DbResult<UserAdministrationModel> GetUser(int userId, int executingUser, string domainName);
DbResult<List<RoleOptionModel>> ListRoles(int executingUser, string domainName);
DbResult<List<StateOptionModel>> ListStates(string domainName);
DbResult<int> SaveUser(UserAdministrationModel user, string passwordHash, int executingUser, string domainName);
DbResult<bool> DeleteUser(int userId, int executingUser, string domainName);
DbResult<List<UserClientModel>> ListUserClients(int userId, int executingUser, string domainName);
DbResult<bool> SaveUserClient(int userId, int clientId, int order, bool primary, int executingUser, string domainName);
DbResult<bool> ValidateRegistration(string username, string email, string rfc, string domainName);
DbResult<RegistrationCreatedModel> CreateRegistration(PublicRegistrationDraft draft, string passwordHash, string tokenHash, string domainName);
DbResult<bool> VerifyEmail(string tokenHash, string domainName);
}
}

View File

@@ -0,0 +1,8 @@
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IUserContextProvider
{
bool IsAuthenticated(); UserSessionModel GetCurrentUser(); void SetCurrentUser(UserSessionModel user); void Clear();
}
}

View File

@@ -0,0 +1,12 @@
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Models.Security;
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IUserRepository
{
DbResult<UserModel> ObtenerPorNombreUsuario(string username, string domainName);
DbResult<bool> RegisterSuccessfulLogin(int cveUser, string domainName);
DbResult<bool> RegisterFailedLogin(int cveUser, string domainName);
}
}

View File

@@ -0,0 +1,10 @@
using SIG.Web.AppCode.Models.Common;
namespace SIG.Web.AppCode.Contracts.Security
{
public interface IUserSessionRepository
{
DbResult<bool> StartSession(int cveUser, string sessionToken, string ipAddress, string userAgent, string domainName);
DbResult<bool> EndSession(string sessionToken, string domainName);
}
}

View File

@@ -0,0 +1,37 @@
using System;
namespace SIG.Web.AppCode.Models.Common
{
public sealed class DbResult<T>
{
public bool Success { get; private set; }
public T Data { get; private set; }
public string UserMessage { get; private set; }
public string TechnicalMessage { get; private set; }
public Exception Exception { get; private set; }
public static DbResult<T> Ok(T data)
{
return new DbResult<T>
{
Success = true,
Data = data,
UserMessage = string.Empty,
TechnicalMessage = string.Empty,
Exception = null
};
}
public static DbResult<T> Fail(string userMessage, string technicalMessage, Exception exception)
{
return new DbResult<T>
{
Success = false,
Data = default(T),
UserMessage = userMessage,
TechnicalMessage = technicalMessage,
Exception = exception
};
}
}
}

View File

@@ -0,0 +1,9 @@
namespace SIG.Web.AppCode.Models.Common
{
public sealed class StoredProcedureResult
{
public bool Return { get; set; }
public int ErrorTipo { get; set; }
public string ErrorMostrar { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace SIG.Web.AppCode.Models.Security
{
public sealed class LoginRequest
{
public string NombreUsuario { get; set; }
public string Contrasena { get; set; }
public string NombreDominio { get; set; }
public string DireccionIP { get; set; }
public string AgenteUsuario { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Models.Security
{
public sealed class LoginResult
{
public bool Exitoso { get; private set; }
public string Mensaje { get; private set; }
public UserSessionModel Usuario { get; private set; }
public static LoginResult Ok(UserSessionModel usuario) { return new LoginResult { Exitoso = true, Usuario = usuario, Mensaje = string.Empty }; }
public static LoginResult Fail(string mensaje) { return new LoginResult { Exitoso = false, Mensaje = mensaje }; }
}
}

View File

@@ -0,0 +1,70 @@
using System;
namespace SIG.Web.AppCode.Models.Security
{
[Serializable]
public sealed class PublicRegistrationDraft
{
public string NombreUsuario { get; set; }
public string CorreoElectronico { get; set; }
public string Contrasena { get; set; }
public string RFC { get; set; }
public string RazonSocial { get; set; }
public string NombreComercial { get; set; }
public string Direccion { get; set; }
public string NoExt { get; set; }
public string NoInt { get; set; }
public string Colonia { get; set; }
public string Ciudad { get; set; }
public int CveSATEstado { get; set; }
public string Pais { get; set; }
public string CP { get; set; }
public string Telefono { get; set; }
}
public sealed class UserAdministrationModel
{
public int CveUsuario { get; set; }
public string NombreUsuario { get; set; }
public string CorreoElectronico { get; set; }
public int CveRol { get; set; }
public string NombreRol { get; set; }
public bool Activo { get; set; }
public bool DebeCambiarContrasena { get; set; }
public bool CorreoVerificado { get; set; }
public DateTime FechaCreacion { get; set; }
}
public sealed class RoleOptionModel
{
public int CveRol { get; set; }
public string NombreRol { get; set; }
public string Descripcion { get; set; }
}
public sealed class StateOptionModel
{
public int CveEstado { get; set; }
public string ClaveEstado { get; set; }
public string NombreEstado { get; set; }
}
public sealed class UserClientModel
{
public int CveUsuarioCliente { get; set; }
public int CveUsuario { get; set; }
public int CveCliente { get; set; }
public string Nombre { get; set; }
public string RFC { get; set; }
public int Orden { get; set; }
public bool EsPrincipal { get; set; }
public bool Activo { get; set; }
public bool ClienteActivo { get; set; }
}
public sealed class RegistrationCreatedModel
{
public int CveUsuario { get; set; }
public int CveCliente { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace SIG.Web.AppCode.Models.Security
{
public sealed class UserModel
{
public int CveUsuario { get; set; }
public string NombreUsuario { get; set; }
public string HashContrasena { get; set; }
public string CorreoElectronico { get; set; }
public int CveRol { get; set; }
public string NombreRol { get; set; }
public bool Activo { get; set; }
public bool CorreoVerificado { get; set; }
public bool DebeCambiarContrasena { get; set; } public int IntentosFallidosInicioSesion { get; set; }
public DateTime? BloqueadoHasta { get; set; }
public DateTime? FechaUltimoInicioSesion { get; set; }
public DateTime? FechaUltimaConexion { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using SIG.Web.AppCode.Notifications.Models;
namespace SIG.Web.AppCode.Notifications.Contracts
{
public interface INotificationCenter
{
void Queue(NotificationMessage message);
void MarkAsShown(int notificationId, int userId);
IReadOnlyList<NotificationMessage> GetBellMessages(int userId);
int CountPending(int userId);
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNet.SignalR;
namespace SIG.Web.AppCode.Notifications.Hubs
{
public class NotificationHub : Hub
{
public void Ping()
{
Clients.Caller.pong("SignalR is working.");
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
namespace SIG.Web.AppCode.Notifications.Models
{
[Serializable]
public sealed class NotificationMessage
{
public int CveNotificacion { get; set; }
public int CveUsuario { get; set; }
public string TipoNotificacion { get; set; }
public string EstadoNotificacion { get; set; }
public string Titulo { get; set; }
public string Mensaje { get; set; }
public string Url { get; set; }
public bool Leida { get; set; }
public bool MostradaEnCampana { get; set; }
public DateTime? FechaEnvio { get; set; }
public DateTime? FechaLectura { get; set; }
public DateTime FechaCreacion { get; set; }
public DateTime? FechaActualizacion { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using SIG.Web.AppCode.Notifications.Contracts;
using SIG.Web.AppCode.Notifications.Models;
namespace SIG.Web.AppCode.Notifications.Services
{
public sealed class DummyNotificationCenter : INotificationCenter
{
private static readonly List<NotificationMessage> Items = new List<NotificationMessage>();
public void Queue(NotificationMessage message)
{
if (message == null) return;
message.CveNotificacion = Items.Count + 1;
message.FechaCreacion = System.DateTime.Now;
message.EstadoNotificacion = "Pendiente";
Items.Add(message);
}
public void MarkAsShown(int notificationId, int userId)
{
var item = Items.FirstOrDefault(x => x.CveNotificacion == notificationId && x.CveUsuario == userId);
if (item == null) return;
item.MostradaEnCampana = true;
}
public IReadOnlyList<NotificationMessage> GetBellMessages(int userId)
{
return Items.Where(x => x.CveUsuario == userId && !x.Leida).ToList();
}
public int CountPending(int userId)
{
return GetBellMessages(userId).Count;
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Data;
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Services.Database;
namespace SIG.Web.AppCode.Repositories.Security
{
public sealed class LoginAttemptRepository : ILoginAttemptRepository
{
private readonly IDbExecutor _dbExecutor;
public LoginAttemptRepository(IDbExecutor dbExecutor)
{
_dbExecutor = dbExecutor;
}
public DbResult<bool> Save(int? cveUser, string username, bool wasSuccessful, string failureReason, string ipAddress, string userAgent, string domainName)
{
return _dbExecutor.Execute(domainName, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_IntentoInicioSesionAdd", cveUser ?? 0);
StoredProcedureCommand.AddInput(command, "@CveUsuario", cveUser.HasValue ? (object)cveUser.Value : null, DbType.Int32);
StoredProcedureCommand.AddInput(command, "@NombreUsuario", username, DbType.String);
StoredProcedureCommand.AddInput(command, "@Exitoso", wasSuccessful, DbType.Boolean);
StoredProcedureCommand.AddInput(command, "@MotivoFallo", failureReason, DbType.String);
StoredProcedureCommand.AddInput(command, "@DireccionIP", ipAddress, DbType.String);
StoredProcedureCommand.AddInput(command, "@AgenteUsuario", userAgent, DbType.String);
StoredProcedureCommand.AddInput(command, "@NombreDominio", domainName, DbType.String);
command.ExecuteNonQuery();
StoredProcedureCommand.EnsureSuccess(command);
return true;
}
});
}
}
}

View File

@@ -0,0 +1,143 @@
using System.Collections.Generic;
using System.Data;
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Models.Security;
using SIG.Web.AppCode.Services.Database;
using SIG.Web.AppCode.Utilities;
namespace SIG.Web.AppCode.Repositories.Security
{
public sealed class UserAdministrationRepository : IUserAdministrationRepository
{
private readonly IDbExecutor _db;
public UserAdministrationRepository(IDbExecutor db) { _db = db; }
public DbResult<List<UserAdministrationModel>> ListUsers(string text, int executingUser, string domainName)
{ return QueryList<UserAdministrationModel>("dbo.PA_Sis_UsuarioList", executingUser, domainName, c => StoredProcedureCommand.AddInput(c, "@Texto", text, DbType.String)); }
public DbResult<UserAdministrationModel> GetUser(int userId, int executingUser, string domainName)
{ return QueryOne<UserAdministrationModel>("dbo.PA_Sis_UsuarioGet", executingUser, domainName, c => StoredProcedureCommand.AddInput(c, "@CveUsuario", userId, DbType.Int32)); }
public DbResult<List<RoleOptionModel>> ListRoles(int executingUser, string domainName)
{ return QueryList<RoleOptionModel>("dbo.PA_Sis_RolList", executingUser, domainName, null); }
public DbResult<List<StateOptionModel>> ListStates(string domainName)
{ return QueryList<StateOptionModel>("dbo.PA_Gral_EstadoList", 0, domainName, null); }
public DbResult<int> SaveUser(UserAdministrationModel user, string passwordHash, int executingUser, string domainName)
{
return _db.Execute(domainName, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_UsuarioSave", executingUser);
StoredProcedureCommand.AddInput(command, "@CveUsuario", user.CveUsuario <= 0 ? (object)null : user.CveUsuario, DbType.Int32);
StoredProcedureCommand.AddInput(command, "@NombreUsuario", user.NombreUsuario, DbType.String);
StoredProcedureCommand.AddInput(command, "@CorreoElectronico", user.CorreoElectronico, DbType.String);
StoredProcedureCommand.AddInput(command, "@CveRol", user.CveRol, DbType.Int32);
StoredProcedureCommand.AddInput(command, "@HashContrasena", passwordHash, DbType.String);
StoredProcedureCommand.AddInput(command, "@Activo", user.Activo, DbType.Boolean);
StoredProcedureCommand.AddInput(command, "@DebeCambiarContrasena", user.DebeCambiarContrasena, DbType.Boolean);
int id;
using (IDataReader reader = command.ExecuteReader()) { id = reader.Read() ? reader.GetInt32(0) : 0; }
StoredProcedureCommand.EnsureSuccess(command);
return id;
}
});
}
public DbResult<bool> DeleteUser(int userId, int executingUser, string domainName)
{ return Execute("dbo.PA_Sis_UsuarioDelete", executingUser, domainName, c => StoredProcedureCommand.AddInput(c, "@CveUsuario", userId, DbType.Int32)); }
public DbResult<List<UserClientModel>> ListUserClients(int userId, int executingUser, string domainName)
{ return QueryList<UserClientModel>("dbo.PA_Sis_UsuarioClienteList", executingUser, domainName, c => StoredProcedureCommand.AddInput(c, "@CveUsuario", userId, DbType.Int32)); }
public DbResult<bool> SaveUserClient(int userId, int clientId, int order, bool primary, int executingUser, string domainName)
{
return Execute("dbo.PA_Sis_UsuarioClienteSave", executingUser, domainName, c =>
{
StoredProcedureCommand.AddInput(c, "@CveUsuario", userId, DbType.Int32);
StoredProcedureCommand.AddInput(c, "@CveCliente", clientId, DbType.Int32);
StoredProcedureCommand.AddInput(c, "@Orden", order, DbType.Int32);
StoredProcedureCommand.AddInput(c, "@EsPrincipal", primary, DbType.Boolean);
});
}
public DbResult<bool> ValidateRegistration(string username, string email, string rfc, string domainName)
{
return Execute("dbo.PA_Sis_RegistroClienteValidate", 0, domainName, c =>
{
StoredProcedureCommand.AddInput(c, "@NombreUsuario", username, DbType.String);
StoredProcedureCommand.AddInput(c, "@CorreoElectronico", email, DbType.String);
StoredProcedureCommand.AddInput(c, "@RFC", rfc, DbType.String);
});
}
public DbResult<RegistrationCreatedModel> CreateRegistration(PublicRegistrationDraft draft, string passwordHash, string tokenHash, string domainName)
{
return QueryOne<RegistrationCreatedModel>("dbo.PA_Sis_RegistroClienteAdd", 0, domainName, c =>
{
StoredProcedureCommand.AddInput(c, "@NombreUsuario", draft.NombreUsuario, DbType.String);
StoredProcedureCommand.AddInput(c, "@HashContrasena", passwordHash, DbType.String);
StoredProcedureCommand.AddInput(c, "@CorreoElectronico", draft.CorreoElectronico, DbType.String);
StoredProcedureCommand.AddInput(c, "@RFC", draft.RFC, DbType.String);
StoredProcedureCommand.AddInput(c, "@RazonSocial", draft.RazonSocial, DbType.String);
StoredProcedureCommand.AddInput(c, "@NombreComercial", draft.NombreComercial, DbType.String);
StoredProcedureCommand.AddInput(c, "@Direccion", draft.Direccion, DbType.String);
StoredProcedureCommand.AddInput(c, "@NoExt", draft.NoExt, DbType.String);
StoredProcedureCommand.AddInput(c, "@NoInt", draft.NoInt, DbType.String);
StoredProcedureCommand.AddInput(c, "@Colonia", draft.Colonia, DbType.String);
StoredProcedureCommand.AddInput(c, "@Ciudad", draft.Ciudad, DbType.String);
StoredProcedureCommand.AddInput(c, "@CveSATEstado", draft.CveSATEstado, DbType.Int32);
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, "@HashToken", tokenHash, DbType.StringFixedLength);
StoredProcedureCommand.AddInput(c, "@HorasExpiracion", 24, DbType.Int32);
});
}
public DbResult<bool> VerifyEmail(string tokenHash, string domainName)
{ return Execute("dbo.PA_Sis_CorreoVerify", 0, domainName, c => StoredProcedureCommand.AddInput(c, "@HashToken", tokenHash, DbType.StringFixedLength)); }
private DbResult<List<T>> QueryList<T>(string procedure, int user, string domain, System.Action<IDbCommand> parameters) where T : class, new()
{
return _db.Execute(domain, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, procedure, user); if (parameters != null) parameters(command);
List<T> value; using (IDataReader reader = command.ExecuteReader()) { value = reader.ToList<T>(); }
StoredProcedureCommand.EnsureSuccess(command); return value;
}
});
}
private DbResult<T> QueryOne<T>(string procedure, int user, string domain, System.Action<IDbCommand> parameters) where T : class, new()
{
return _db.Execute(domain, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, procedure, user); if (parameters != null) parameters(command);
T value; using (IDataReader reader = command.ExecuteReader()) { value = reader.ToObject<T>(); }
StoredProcedureCommand.EnsureSuccess(command); return value;
}
});
}
private DbResult<bool> Execute(string procedure, int user, string domain, System.Action<IDbCommand> parameters)
{
return _db.Execute(domain, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, procedure, user); if (parameters != null) parameters(command);
command.ExecuteNonQuery(); StoredProcedureCommand.EnsureSuccess(command); return true;
}
});
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Data;
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Models.Security;
using SIG.Web.AppCode.Services.Database;
using SIG.Web.AppCode.Utilities;
namespace SIG.Web.AppCode.Repositories.Security
{
public sealed class UserRepository : IUserRepository
{
private readonly IDbExecutor _dbExecutor;
public UserRepository(IDbExecutor dbExecutor)
{
_dbExecutor = dbExecutor;
}
public DbResult<UserModel> ObtenerPorNombreUsuario(string username, string domainName)
{
return _dbExecutor.Execute(domainName, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_UsuarioGetByName", 0);
StoredProcedureCommand.AddInput(command, "@NombreUsuario", username, DbType.String);
UserModel user;
using (IDataReader reader = command.ExecuteReader())
{
user = reader.ToObject<UserModel>();
}
StoredProcedureCommand.EnsureSuccess(command);
return user;
}
});
}
public DbResult<bool> RegisterSuccessfulLogin(int cveUser, string domainName)
{
return ExecuteUserUpdate("dbo.PA_Sis_UsuarioLoginSuccessUpdate", cveUser, domainName);
}
public DbResult<bool> RegisterFailedLogin(int cveUser, string domainName)
{
return ExecuteUserUpdate("dbo.PA_Sis_UsuarioLoginFailedUpdate", cveUser, domainName);
}
private DbResult<bool> ExecuteUserUpdate(string procedureName, int cveUser, string domainName)
{
return _dbExecutor.Execute(domainName, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, procedureName, cveUser);
StoredProcedureCommand.AddInput(command, "@CveUsuario", cveUser, DbType.Int32);
command.ExecuteNonQuery();
StoredProcedureCommand.EnsureSuccess(command);
return true;
}
});
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Data;
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Services.Database;
namespace SIG.Web.AppCode.Repositories.Security
{
public sealed class UserSessionRepository : IUserSessionRepository
{
private readonly IDbExecutor _dbExecutor;
public UserSessionRepository(IDbExecutor dbExecutor)
{
_dbExecutor = dbExecutor;
}
public DbResult<bool> StartSession(int cveUser, string sessionToken, string ipAddress, string userAgent, string domainName)
{
return _dbExecutor.Execute(domainName, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_UsuarioSesionAdd", cveUser);
StoredProcedureCommand.AddInput(command, "@CveUsuario", cveUser, DbType.Int32);
StoredProcedureCommand.AddInput(command, "@TokenSesion", sessionToken, DbType.String);
StoredProcedureCommand.AddInput(command, "@DireccionIP", ipAddress, DbType.String);
StoredProcedureCommand.AddInput(command, "@AgenteUsuario", userAgent, DbType.String);
StoredProcedureCommand.AddInput(command, "@NombreDominio", domainName, DbType.String);
command.ExecuteNonQuery();
StoredProcedureCommand.EnsureSuccess(command);
return true;
}
});
}
public DbResult<bool> EndSession(string sessionToken, string domainName)
{
return _dbExecutor.Execute(domainName, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_UsuarioSesionEnd", 0);
StoredProcedureCommand.AddInput(command, "@TokenSesion", sessionToken, DbType.String);
command.ExecuteNonQuery();
StoredProcedureCommand.EnsureSuccess(command);
return true;
}
});
}
}
}

View File

@@ -0,0 +1,109 @@
using SIG.Web.AppCode.Configuration;
using SIG.Web.AppCode.Services.Logging;
using SIG.Web.AppCode.Session;
using System;
using System.Web;
using System.Web.UI;
namespace SIG.Web.AppCode.Security
{
public abstract class ClsPaginaBase : Page
{
protected UserSessionModel CurrentUser { get; private set; }
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
string currentPath = Request.AppRelativeCurrentExecutionFilePath;
if (IsPublicPage(currentPath))
{
return;
}
CurrentUser = AppServices.UserContext.GetCurrentUser();
if (CurrentUser == null)
{
Response.Redirect(BuildLoginUrl(currentPath), false);
Context.ApplicationInstance.CompleteRequest();
return;
}
if (!IsHomePage(currentPath) &&
!AppServices.PermissionService.CanAccessPage(CurrentUser.CveUsuario, currentPath))
{
Response.Redirect("~/AccessDenied.aspx", false);
Context.ApplicationInstance.CompleteRequest();
}
}
private string BuildLoginUrl(string currentPath)
{
string loginUrl = ResolveUrl("~/Login.aspx");
if (IsHomePage(currentPath))
{
return loginUrl;
}
string retornoURL =
Request.Url == null
? null
: Request.Url.PathAndQuery;
if (string.IsNullOrWhiteSpace(retornoURL) ||
retornoURL.Equals("/", StringComparison.OrdinalIgnoreCase))
{
return loginUrl;
}
return loginUrl +
"?retornoURL=" +
HttpUtility.UrlEncode(retornoURL);
}
private static bool IsHomePage(string appRelativePath)
{
return string.IsNullOrWhiteSpace(appRelativePath) ||
appRelativePath.Equals("~/Inicio.aspx", StringComparison.OrdinalIgnoreCase);
}
private static bool IsPublicPage(string appRelativePath)
{
return appRelativePath != null &&
(appRelativePath.Equals("~/Login.aspx", StringComparison.OrdinalIgnoreCase) ||
appRelativePath.Equals("~/Logout.aspx", StringComparison.OrdinalIgnoreCase) ||
appRelativePath.Equals("~/AccessDenied.aspx", StringComparison.OrdinalIgnoreCase));
}
protected override void OnError(EventArgs e)
{
Exception exception = Server.GetLastError();
if (exception == null)
{
base.OnError(e);
return;
}
ExceptionLogger.Log(
exception,
GetType().FullName
);
string message =
ErrorHandlerConfig.GetFriendlyMessage(exception);
Server.ClearError();
Site siteMaster = Master as Site;
if (siteMaster != null)
{
siteMaster.ShowGlobalError(message);
}
}
}
}

View File

@@ -0,0 +1,12 @@
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Security
{
public sealed class DummyPermissionService : IPermissionService
{
public bool CanAccess(UserSessionModel user, string appRelativePath)
{
return user != null;
}
}
}

View File

@@ -0,0 +1,9 @@
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Security
{
public interface IPermissionService
{
bool CanAccess(UserSessionModel user, string appRelativePath);
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Configuration;
using System.Data;
using Microsoft.Data.SqlClient;
using Npgsql;
using SIG.Web.AppCode.Contracts.Data;
namespace SIG.Web.AppCode.Services.Data
{
public sealed class DbConnectionFactory : IDbConnectionFactory
{
private readonly IDomainConnectionResolver _resolver;
public DbConnectionFactory(IDomainConnectionResolver resolver)
{
_resolver = resolver;
}
public IDbConnection Create(string domain)
{
var settings = _resolver.Resolve(domain);
var connection = ConfigurationManager.ConnectionStrings[settings.ConnectionStringName];
if (connection == null)
throw new InvalidOperationException("Connection string not found: " + settings.ConnectionStringName);
switch (settings.Provider)
{
case DbProviderKind.SqlServer:
return new SqlConnection(connection.ConnectionString);
case DbProviderKind.PostgreSql:
return new NpgsqlConnection(connection.ConnectionString);
default:
throw new NotSupportedException("Unsupported database provider: " + settings.Provider);
}
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Configuration;
using SIG.Web.AppCode.Contracts.Data;
namespace SIG.Web.AppCode.Services.Data
{
public sealed class DomainConnectionResolver : IDomainConnectionResolver
{
public DomainConnectionSettings Resolve(string domain)
{
// Default first. Later this can read a table or config mapping by host/domain.
var providerText = ConfigurationManager.AppSettings["DefaultDbProvider"] ?? "SqlServer";
var connectionName = ConfigurationManager.AppSettings["DefaultConnectionStringName"] ?? "SIG_DefaultSql";
DbProviderKind provider;
if (!Enum.TryParse(providerText, true, out provider))
provider = DbProviderKind.SqlServer;
return new DomainConnectionSettings
{
Domain = domain,
Provider = provider,
ConnectionStringName = connectionName,
IsDefault = true
};
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using SIG.Web.AppCode.Contracts.Database;
namespace SIG.Web.AppCode.Services.Database
{
public sealed class DbConnectionFactory : IDbConnectionFactory
{
public IDbConnection CreateConnection(string domainName)
{
string name = ConfigurationManager.AppSettings["DefaultConnectionStringName"];
if (string.IsNullOrWhiteSpace(name)) name = "SIG_DefaultSql";
ConnectionStringSettings settings = ConfigurationManager.ConnectionStrings[name];
if (settings == null) throw new InvalidOperationException("Connection string not found: " + name);
return new SqlConnection(settings.ConnectionString);
}
}
}

View File

@@ -0,0 +1,162 @@
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Services.Logging;
using System;
using System.Data;
using System.Data.SqlClient;
namespace SIG.Web.AppCode.Services.Database
{
public sealed class DbExecutor : IDbExecutor
{
private readonly IDbConnectionFactory _connectionFactory;
public DbExecutor(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public DbResult<T> Execute<T>(string domainName, Func<IDbConnection, T> action)
{
IDbConnection connection = null;
SqlConnection sqlConnection = null;
SqlInfoMessageEventHandler infoMessageHandler = null;
try
{
if (action == null)
{
throw new ArgumentNullException("action");
}
connection = _connectionFactory.CreateConnection(domainName);
sqlConnection = connection as SqlConnection;
if (sqlConnection != null)
{
/*
* No activar FireInfoMessageEventOnUserErrors: al hacerlo, SQL Server
* degrada errores de severidad 11-16 a InfoMessage y evita el catch.
* Los RAISERROR informativos de severidad 10 llegan al evento sin esa opción.
*/
infoMessageHandler = delegate(object sender, SqlInfoMessageEventArgs eventArgs)
{
foreach (SqlError error in eventArgs.Errors)
{
ExceptionLogger.Log(
CreateDiagnosticException(new InvalidOperationException(error.Message)),
"DbExecutor.SqlInfoMessage");
}
};
sqlConnection.InfoMessage += infoMessageHandler;
}
if (connection.State != ConnectionState.Open)
{
connection.Open();
}
T data = action(connection);
return DbResult<T>.Ok(data);
}
catch (StoredProcedureException ex)
{
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.StoredProcedure", ex.UserMessage);
return DbResult<T>.Fail(ex.UserMessage, BuildTechnicalMessage(ex), ex);
}
catch (SqlException ex)
{
string userMessage;
switch (ex.Number)
{
case 208:
userMessage = "Falta una tabla, vista o procedimiento requerido en la base de datos.";
break;
case 207:
userMessage = "Falta una columna requerida en la base de datos.";
break;
case 2812:
userMessage = "Falta un procedimiento requerido en la base de datos.";
break;
case 4060:
case 18456:
userMessage = "No fue posible abrir la base de datos configurada.";
break;
case -2:
userMessage = "La base de datos tardó demasiado en responder.";
break;
default:
userMessage = "Ocurrió un error al consultar la base de datos.";
break;
}
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.SqlException", userMessage);
return DbResult<T>.Fail(userMessage, BuildTechnicalMessage(ex), ex);
}
catch (TimeoutException ex)
{
const string userMessage = "La base de datos tardó demasiado en responder. Intente nuevamente.";
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.TimeoutException", userMessage);
return DbResult<T>.Fail(userMessage, BuildTechnicalMessage(ex), ex);
}
catch (Exception ex)
{
const string userMessage = "Ocurrió un error al consultar la base de datos.";
ExceptionLogger.Log(CreateDiagnosticException(ex), "DbExecutor.Exception", userMessage);
return DbResult<T>.Fail(userMessage, BuildTechnicalMessage(ex), ex);
}
finally
{
if (sqlConnection != null && infoMessageHandler != null)
{
sqlConnection.InfoMessage -= infoMessageHandler;
}
if (connection != null)
{
try
{
if (connection.State != ConnectionState.Closed)
{
connection.Close();
}
}
finally
{
connection.Dispose();
}
}
StoredProcedureDiagnostics.Clear();
}
}
private static Exception CreateDiagnosticException(Exception exception)
{
string diagnostic = StoredProcedureDiagnostics.BuildDiagnostic();
if (string.IsNullOrWhiteSpace(diagnostic))
{
return exception;
}
return new InvalidOperationException(
exception.Message + Environment.NewLine + diagnostic,
exception);
}
private static string BuildTechnicalMessage(Exception exception)
{
string diagnostic = StoredProcedureDiagnostics.BuildDiagnostic();
return string.IsNullOrWhiteSpace(diagnostic)
? exception.Message
: exception.Message + Environment.NewLine + diagnostic;
}
}
}

View File

@@ -0,0 +1,133 @@
using System;
using System.Data;
using SIG.Web.AppCode.Models.Common;
namespace SIG.Web.AppCode.Services.Database
{
public static class StoredProcedureCommand
{
public static void Configure(IDbCommand command, string procedureName, int cveUsuarioEjecuta)
{
if (command == null)
{
throw new ArgumentNullException("command");
}
StoredProcedureDiagnostics.Start(procedureName);
command.CommandText = procedureName;
command.CommandType = CommandType.StoredProcedure;
AddInput(command, "@CveUsuarioEjecuta", cveUsuarioEjecuta, DbType.Int32);
AddOutput(command, "@ErrorMostrar", DbType.String, -1);
AddOutput(command, "@ErrorTipo", DbType.Int32, 0);
AddOutput(command, "@Return", DbType.Boolean, 0);
}
public static IDbDataParameter AddInput(IDbCommand command, string name, object value)
{
return AddInput(command, name, value, null);
}
public static IDbDataParameter AddInput(IDbCommand command, string name, object value, DbType? dbType)
{
IDbDataParameter parameter = command.CreateParameter();
parameter.ParameterName = name;
parameter.Direction = ParameterDirection.Input;
parameter.Value = value ?? DBNull.Value;
if (dbType.HasValue)
{
parameter.DbType = dbType.Value;
}
command.Parameters.Add(parameter);
StoredProcedureDiagnostics.AddParameter(
parameter.ParameterName,
parameter.Value,
parameter.DbType,
parameter.Direction);
return parameter;
}
public static StoredProcedureResult ReadResult(IDbCommand command)
{
StoredProcedureDiagnostics.CaptureOutputs(command);
return new StoredProcedureResult
{
Return = ReadBoolean(command, "@Return"),
ErrorTipo = ReadInteger(command, "@ErrorTipo"),
ErrorMostrar = ReadString(command, "@ErrorMostrar")
};
}
public static void EnsureSuccess(IDbCommand command)
{
StoredProcedureResult result = ReadResult(command);
if (result.Return)
{
return;
}
object returnValue = ((IDataParameter)command.Parameters["@Return"]).Value;
bool outputsReceived = returnValue != null && returnValue != DBNull.Value;
string message;
if (!string.IsNullOrWhiteSpace(result.ErrorMostrar))
{
message = result.ErrorMostrar;
}
else if (!outputsReceived)
{
message = "La versión instalada del procedimiento no devolvió los parámetros requeridos; favor de contactar a Sistemas.";
}
else
{
message = "La operación no pudo completarse.";
}
throw new StoredProcedureException(message, result.ErrorTipo);
}
private static void AddOutput(IDbCommand command, string name, DbType dbType, int size)
{
IDbDataParameter parameter = command.CreateParameter();
parameter.ParameterName = name;
parameter.Direction = ParameterDirection.Output;
parameter.DbType = dbType;
if (size != 0)
{
parameter.Size = size;
}
command.Parameters.Add(parameter);
StoredProcedureDiagnostics.AddParameter(
parameter.ParameterName,
parameter.Value,
parameter.DbType,
parameter.Direction);
}
private static bool ReadBoolean(IDbCommand command, string name)
{
object value = ((IDataParameter)command.Parameters[name]).Value;
return value != null && value != DBNull.Value && Convert.ToBoolean(value);
}
private static int ReadInteger(IDbCommand command, string name)
{
object value = ((IDataParameter)command.Parameters[name]).Value;
return value == null || value == DBNull.Value ? 0 : Convert.ToInt32(value);
}
private static string ReadString(IDbCommand command, string name)
{
object value = ((IDataParameter)command.Parameters[name]).Value;
return value == null || value == DBNull.Value ? string.Empty : Convert.ToString(value);
}
}
}

View File

@@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
namespace SIG.Web.AppCode.Services.Database
{
public static class StoredProcedureDiagnostics
{
private static readonly AsyncLocal<DiagnosticContext> CurrentContext =
new AsyncLocal<DiagnosticContext>();
public static void Start(string procedureName)
{
CurrentContext.Value = new DiagnosticContext
{
ProcedureName = procedureName
};
}
public static void AddParameter(string name, object value, DbType dbType, ParameterDirection direction)
{
DiagnosticContext context = CurrentContext.Value;
if (context == null)
{
return;
}
context.Parameters.Add(new DiagnosticParameter
{
Name = name,
Value = value,
DbType = dbType,
Direction = direction
});
}
public static void CaptureOutputs(IDbCommand command)
{
DiagnosticContext context = CurrentContext.Value;
if (context == null || command == null)
{
return;
}
foreach (IDataParameter commandParameter in command.Parameters)
{
if (commandParameter.Direction == ParameterDirection.Input)
{
continue;
}
DiagnosticParameter parameter = context.Parameters.FirstOrDefault(
item => item.Name.Equals(commandParameter.ParameterName, StringComparison.OrdinalIgnoreCase));
if (parameter != null)
{
parameter.Value = commandParameter.Value;
}
}
}
public static string BuildDiagnostic()
{
DiagnosticContext context = CurrentContext.Value;
if (context == null || string.IsNullOrWhiteSpace(context.ProcedureName))
{
return string.Empty;
}
var result = new StringBuilder();
result.AppendLine("StoredProcedure: " + context.ProcedureName);
result.AppendLine("Parametros:");
foreach (DiagnosticParameter parameter in context.Parameters)
{
result.AppendLine(parameter.Name + "=" + FormatSqlValue(parameter));
}
result.AppendLine("ReplaySql:");
AppendReplaySql(result, context);
return result.ToString();
}
public static void Clear()
{
CurrentContext.Value = null;
}
private static void AppendReplaySql(StringBuilder result, DiagnosticContext context)
{
IList<DiagnosticParameter> outputs = context.Parameters
.Where(parameter => parameter.Direction != ParameterDirection.Input)
.ToList();
foreach (DiagnosticParameter output in outputs)
{
result.Append("DECLARE ")
.Append(output.Name)
.Append(' ')
.Append(GetSqlType(output.DbType))
.AppendLine(";");
}
result.Append("EXEC ").Append(QuoteProcedureName(context.ProcedureName));
for (int index = 0; index < context.Parameters.Count; index++)
{
DiagnosticParameter parameter = context.Parameters[index];
result.Append(index == 0 ? Environment.NewLine + " " : "," + Environment.NewLine + " ");
result.Append(parameter.Name).Append(" = ");
if (parameter.Direction == ParameterDirection.Input)
{
result.Append(FormatSqlValue(parameter));
}
else
{
result.Append(parameter.Name).Append(" OUTPUT");
}
}
result.AppendLine(";");
if (outputs.Count > 0)
{
result.Append("SELECT ");
for (int index = 0; index < outputs.Count; index++)
{
if (index > 0)
{
result.Append(", ");
}
result.Append(outputs[index].Name);
}
result.AppendLine(";");
}
}
private static string FormatSqlValue(DiagnosticParameter parameter)
{
if (IsSensitive(parameter.Name))
{
return "N'[REDACTADO]'";
}
object value = parameter.Value;
if (value == null || value == DBNull.Value)
{
return "NULL";
}
if (value is bool)
{
return (bool)value ? "1" : "0";
}
if (value is DateTime)
{
return "'" + ((DateTime)value).ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture) + "'";
}
if (value is DateTimeOffset)
{
return "'" + ((DateTimeOffset)value).ToString("o", CultureInfo.InvariantCulture) + "'";
}
if (value is Guid)
{
return "'" + value + "'";
}
if (value is byte[])
{
byte[] bytes = (byte[])value;
int length = Math.Min(bytes.Length, 64);
return "0x" + BitConverter.ToString(bytes, 0, length).Replace("-", string.Empty)
+ (bytes.Length > length ? "/*TRUNCADO*/" : string.Empty);
}
if (value is string || value is char)
{
string text = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
if (text.Length > 1000)
{
text = text.Substring(0, 1000) + " [TRUNCADO]";
}
return "N'" + text.Replace("'", "''") + "'";
}
if (value is float || value is double || value is decimal ||
value is byte || value is sbyte || value is short || value is ushort ||
value is int || value is uint || value is long || value is ulong)
{
return Convert.ToString(value, CultureInfo.InvariantCulture);
}
string fallback = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
return "N'" + fallback.Replace("'", "''") + "'";
}
private static bool IsSensitive(string parameterName)
{
string name = (parameterName ?? string.Empty).ToLowerInvariant();
return name.Contains("contrasena") ||
name.Contains("password") ||
name.Contains("hash") ||
name.Contains("token") ||
name.Contains("secreto") ||
name.Contains("secret") ||
name.Contains("apikey") ||
name.Contains("api_key");
}
private static string GetSqlType(DbType dbType)
{
switch (dbType)
{
case DbType.Boolean: return "bit";
case DbType.Byte: return "tinyint";
case DbType.Int16: return "smallint";
case DbType.Int32: return "int";
case DbType.Int64: return "bigint";
case DbType.Decimal: return "decimal(38, 10)";
case DbType.Double: return "float";
case DbType.Single: return "real";
case DbType.Date: return "date";
case DbType.DateTime:
case DbType.DateTime2: return "datetime2(3)";
case DbType.DateTimeOffset: return "datetimeoffset(7)";
case DbType.Guid: return "uniqueidentifier";
case DbType.Binary: return "varbinary(max)";
case DbType.AnsiString: return "varchar(max)";
default: return "nvarchar(max)";
}
}
private static string QuoteProcedureName(string procedureName)
{
return string.Join(".", (procedureName ?? string.Empty)
.Split('.')
.Select(part => "[" + part.Replace("]", "]]" ) + "]"));
}
private sealed class DiagnosticContext
{
public DiagnosticContext()
{
Parameters = new List<DiagnosticParameter>();
}
public string ProcedureName { get; set; }
public IList<DiagnosticParameter> Parameters { get; private set; }
}
private sealed class DiagnosticParameter
{
public string Name { get; set; }
public object Value { get; set; }
public DbType DbType { get; set; }
public ParameterDirection Direction { get; set; }
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace SIG.Web.AppCode.Services.Database
{
public sealed class StoredProcedureException : Exception
{
public StoredProcedureException(string userMessage, int errorType)
: base(userMessage)
{
UserMessage = userMessage;
ErrorType = errorType;
}
public string UserMessage { get; private set; }
public int ErrorType { get; private set; }
}
}

View File

@@ -0,0 +1,720 @@
using System;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Hosting;
using SIG.Web.AppCode.Configuration;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Services.Errors
{
public sealed class SIGErrorRegistro
{
public string Codigo { get; set; }
public DateTime Fecha { get; set; }
public string Origen { get; set; }
public string CveUsuario { get; set; }
public string NombreUsuario { get; set; }
public string MetodoHttp { get; set; }
public string Ruta { get; set; }
public string Ip { get; set; }
public string AgenteUsuario { get; set; }
public bool EsPostbackAsincrono { get; set; }
public string DetalleTecnico { get; set; }
}
public interface ISIGErrorStore
{
void Guardar(SIGErrorRegistro registro);
void MarcarReportado(
string codigo,
DateTime fecha,
string cveUsuario,
string nombreUsuario,
string ruta);
}
public sealed class SIGTextFileErrorStore : ISIGErrorStore
{
private static readonly object BloqueoArchivo = new object();
public void Guardar(SIGErrorRegistro registro)
{
if (registro == null)
{
return;
}
StringBuilder texto = new StringBuilder();
texto.AppendLine();
texto.AppendLine("============================================================");
texto.AppendLine("ERROR");
texto.AppendLine("============================================================");
texto.AppendLine("Codigo: " + Safe(registro.Codigo));
texto.AppendLine("Fecha: " + registro.Fecha.ToString("yyyy-MM-dd HH:mm:ss.fff"));
texto.AppendLine("Origen: " + Safe(registro.Origen));
texto.AppendLine("CveUsuario: " + Safe(registro.CveUsuario));
texto.AppendLine("Nombre: " + Safe(registro.NombreUsuario));
texto.AppendLine("Metodo: " + Safe(registro.MetodoHttp));
texto.AppendLine("Ruta: " + Safe(registro.Ruta));
texto.AppendLine("IP: " + Safe(registro.Ip));
texto.AppendLine("AgenteUsuario: " + Safe(registro.AgenteUsuario));
texto.AppendLine("AsyncPostBack: " + registro.EsPostbackAsincrono);
texto.AppendLine();
texto.AppendLine("Detalle tecnico:");
texto.AppendLine(Safe(registro.DetalleTecnico));
texto.AppendLine("============================================================");
Escribir(texto.ToString());
}
public void MarcarReportado(
string codigo,
DateTime fecha,
string cveUsuario,
string nombreUsuario,
string ruta)
{
StringBuilder texto = new StringBuilder();
texto.AppendLine();
texto.AppendLine("------------------------------------------------------------");
texto.AppendLine("REPORTE DEL USUARIO");
texto.AppendLine("Codigo: " + Safe(codigo));
texto.AppendLine("Fecha: " + fecha.ToString("yyyy-MM-dd HH:mm:ss.fff"));
texto.AppendLine("CveUsuario: " + Safe(cveUsuario));
texto.AppendLine("Nombre: " + Safe(nombreUsuario));
texto.AppendLine("Ruta: " + Safe(ruta));
texto.AppendLine("------------------------------------------------------------");
Escribir(texto.ToString());
}
private static void Escribir(string texto)
{
string ruta = HostingEnvironment.MapPath("~/log/log.txt");
if (string.IsNullOrWhiteSpace(ruta))
{
ruta = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"log",
"log.txt");
}
string directorio = Path.GetDirectoryName(ruta);
lock (BloqueoArchivo)
{
if (!Directory.Exists(directorio))
{
Directory.CreateDirectory(directorio);
}
using (FileStream stream = new FileStream(
ruta,
FileMode.Append,
FileAccess.Write,
FileShare.Read))
using (StreamWriter writer = new StreamWriter(
stream,
new UTF8Encoding(false)))
{
writer.Write(texto);
}
}
}
private static string Safe(string value)
{
return string.IsNullOrEmpty(value) ? string.Empty : value;
}
}
public static class SIGErrorManager
{
private const string ErrorCodeDataKey = "SIG.ErrorCode";
private static ISIGErrorStore _store =
new SIGTextFileErrorStore();
public static ISIGErrorStore Store
{
get { return _store; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
_store = value;
}
}
public static string Registrar(
Exception exception,
HttpContext context,
string origen,
string cveUsuario,
string nombreUsuario)
{
if (exception == null)
{
exception = new Exception(
"No se recibio la excepcion original.");
}
string codigoExistente =
ObtenerCodigoExistente(exception);
if (!string.IsNullOrWhiteSpace(codigoExistente))
{
return codigoExistente;
}
string codigo =
"SIG-" +
DateTime.Now.ToString("yyyyMMdd") +
"-" +
Guid.NewGuid()
.ToString("N")
.Substring(0, 6)
.ToUpperInvariant();
AsignarCodigo(exception, codigo);
string metodo = string.Empty;
string ruta = string.Empty;
string ip = string.Empty;
string userAgent = string.Empty;
bool esAsync = false;
try
{
if (context != null && context.Request != null)
{
metodo = context.Request.HttpMethod;
ruta = context.Request.RawUrl;
ip = context.Request.UserHostAddress;
userAgent = context.Request.UserAgent;
esAsync = EsPeticionAsincrona(context);
}
}
catch
{
// El contexto no debe provocar otro error.
}
SIGErrorRegistro registro = new SIGErrorRegistro
{
Codigo = codigo,
Fecha = DateTime.Now,
Origen = origen,
CveUsuario = cveUsuario,
NombreUsuario = nombreUsuario,
MetodoHttp = metodo,
Ruta = ruta,
Ip = ip,
AgenteUsuario = userAgent,
EsPostbackAsincrono = esAsync,
DetalleTecnico = ObtenerDetalleTecnico(exception)
};
try
{
_store.Guardar(registro);
}
catch
{
// Nunca lanzar otro error desde el manejador global.
}
return codigo;
}
public static void MarcarReportado(
string codigo,
HttpContext context,
string cveUsuario,
string nombreUsuario)
{
string ruta = string.Empty;
try
{
if (context != null && context.Request != null)
{
ruta = context.Request.Form["ruta"];
if (string.IsNullOrWhiteSpace(ruta))
{
ruta = context.Request.RawUrl;
}
}
}
catch
{
ruta = string.Empty;
}
try
{
_store.MarcarReportado(
codigo,
DateTime.Now,
cveUsuario,
nombreUsuario,
ruta);
}
catch
{
// Nunca lanzar desde el registro de reporte.
}
}
public static void ObtenerUsuarioActual(
out string cveUsuario,
out string nombreUsuario,
out int cveRole)
{
cveUsuario = string.Empty;
nombreUsuario = string.Empty;
cveRole = 0;
try
{
if (AppServices.UserContext != null)
{
UserSessionModel usuario =
AppServices.UserContext.GetCurrentUser();
if (usuario != null)
{
cveUsuario =
Convert.ToString(usuario.CveUsuario);
nombreUsuario =
Convert.ToString(usuario.Nombre);
cveRole = usuario.CveRol;
return;
}
}
}
catch
{
// Si Session no esta disponible, continuar con Items.
}
try
{
HttpContext context = HttpContext.Current;
if (context == null)
{
return;
}
object itemCveUsuario =
context.Items["SIG.Error.CveUsuario"];
if (itemCveUsuario == null)
{
return;
}
cveUsuario =
Convert.ToString(itemCveUsuario);
nombreUsuario =
Convert.ToString(
context.Items["SIG.Error.NombreUsuario"]);
int.TryParse(
Convert.ToString(
context.Items["SIG.Error.CveRol"]),
out cveRole);
if (cveRole == 0)
{
int.TryParse(
Convert.ToString(
context.Items["SIG.Error.CveRol"]),
out cveRole);
}
}
catch
{
cveUsuario = string.Empty;
nombreUsuario = string.Empty;
cveRole = 0;
}
}
public static bool EsPeticionAsincrona(
HttpContext context)
{
if (context == null || context.Request == null)
{
return false;
}
try
{
string encabezado =
context.Request.Headers["X-MicrosoftAjax"];
if (!string.IsNullOrWhiteSpace(encabezado) &&
encabezado.IndexOf(
"Delta=true",
StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
return string.Equals(
context.Request.Form["__ASYNCPOST"],
"true",
StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
public static string CrearHtmlError(
string codigo,
Exception exception,
bool mostrarDetalle,
string cveUsuarioActual,
string nombreUsuarioActual,
int cveRoleActual)
{
string codigoSeguro =
HttpUtility.HtmlEncode(codigo);
string codigoJavaScript =
HttpUtility.JavaScriptStringEncode(codigo);
string cveUsuarioJavaScript =
HttpUtility.JavaScriptStringEncode(
cveUsuarioActual ?? string.Empty);
string nombreUsuarioSeguro =
HttpUtility.HtmlEncode(
nombreUsuarioActual ?? string.Empty);
string urlInicio =
VirtualPathUtility.ToAbsolute("~/Inicio.aspx");
string urlReporte =
VirtualPathUtility.ToAbsolute(
"~/ashx/ReportarError.ashx");
string detalleHtml = string.Empty;
if (mostrarDetalle && exception != null)
{
detalleHtml =
"<details class=\"sig-error-details\">" +
"<summary>Ver detalle tecnico</summary>" +
"<pre>" +
HttpUtility.HtmlEncode(
ObtenerDetalleTecnico(exception)) +
"</pre>" +
"</details>";
}
StringBuilder html = new StringBuilder();
html.AppendLine("<!DOCTYPE html>");
html.AppendLine("<html lang=\"es\">");
html.AppendLine("<head>");
html.AppendLine(" <meta charset=\"utf-8\">");
html.AppendLine(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
html.AppendLine(" <title>SIG - Error inesperado</title>");
html.AppendLine(" <link rel=\"shortcut icon\" href=\"/Imagenes/favicon.ico\">");
html.AppendLine(" <style>");
html.AppendLine(" html,body{margin:0;min-height:100%;font-family:'Open Sans',Arial,sans-serif;background:#eef1f5;color:#333}");
html.AppendLine(" *{box-sizing:border-box}");
html.AppendLine(" .sig-header{position:fixed;top:0;left:0;right:0;height:46px;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:0 18px;background:#2b3643;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.18)}");
html.AppendLine(" .sig-brand{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:600}");
html.AppendLine(" .sig-brand-box{display:flex;align-items:center;justify-content:center;width:34px;height:30px;border-radius:3px;background:#3598dc;font-size:13px;font-weight:700;letter-spacing:.5px}");
html.AppendLine(" .sig-header-status{display:flex;flex-direction:column;align-items:flex-end;color:#c6cfda;font-size:12px;line-height:16px}");
html.AppendLine(" .sig-header-user{color:#fff;font-weight:600}");
html.AppendLine(" .sig-sidebar{position:fixed;top:46px;bottom:0;left:0;width:235px;padding-top:18px;background:#364150}");
html.AppendLine(" .sig-sidebar-title{padding:7px 20px 14px;color:#708096;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px}");
html.AppendLine(" .sig-restored-menu{margin:0;padding:0;list-style:none}");
html.AppendLine(" .sig-restored-menu li{margin:0;padding:0;list-style:none}");
html.AppendLine(" .sig-restored-menu a,.sig-restored-menu button{display:flex;align-items:center;gap:12px;width:100%;padding:12px 20px;border:0;background:transparent;color:#b4bcc8;font-family:inherit;font-size:13px;text-decoration:none;text-align:left;cursor:pointer}");
html.AppendLine(" .sig-restored-menu a:hover,.sig-restored-menu button:hover{background:#2c3542;color:#fff}");
html.AppendLine(" .sig-restored-menu .sub-menu{display:none;margin:0;padding:0;background:#2c3542}");
html.AppendLine(" .sig-restored-menu li.open>.sub-menu{display:block}");
html.AppendLine(" .sig-restored-menu .sub-menu a{padding-left:42px;font-size:12px}");
html.AppendLine(" .sig-main{min-height:100vh;margin-left:235px;padding:70px 28px 35px}");
html.AppendLine(" .sig-page-title{margin:0 0 14px;color:#666;font-size:22px;font-weight:300}");
html.AppendLine(" .sig-breadcrumb{margin-bottom:22px;padding:10px 14px;background:#fff;color:#888;font-size:12px}");
html.AppendLine(" .sig-error-card{max-width:820px;background:#fff;border:1px solid #e5e5e5;border-left:4px solid #e43a45;box-shadow:0 2px 5px rgba(0,0,0,.07)}");
html.AppendLine(" .sig-error-card-header{display:flex;align-items:center;gap:13px;padding:18px 22px;border-bottom:1px solid #eef1f5}");
html.AppendLine(" .sig-error-icon{display:flex;align-items:center;justify-content:center;width:38px;height:38px;flex:0 0 38px;border-radius:50%;background:#fbeaec;color:#e43a45;font-size:21px;font-weight:700}");
html.AppendLine(" .sig-error-title{margin:0 0 3px;color:#333;font-size:17px;font-weight:600}");
html.AppendLine(" .sig-error-subtitle{color:#777;font-size:13px}");
html.AppendLine(" .sig-error-body{padding:22px}");
html.AppendLine(" .sig-error-body p{margin:0 0 14px;line-height:1.6}");
html.AppendLine(" .sig-error-code-label{margin-bottom:6px;color:#777;font-size:12px;font-weight:600;text-transform:uppercase}");
html.AppendLine(" .sig-error-code{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 13px;background:#f4f6f8;border:1px solid #e7e9ec;border-radius:3px;font-family:Consolas,monospace;font-size:14px;font-weight:700}");
html.AppendLine(" .sig-copy-button{border:0;background:transparent;color:#3598dc;font-family:inherit;font-size:12px;cursor:pointer}");
html.AppendLine(" .sig-error-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px;margin-top:20px}");
html.AppendLine(" .sig-btn{min-height:34px;padding:7px 15px;border:1px solid transparent;border-radius:2px;font-family:inherit;font-size:13px;cursor:pointer;text-decoration:none}");
html.AppendLine(" .sig-btn-default{border-color:#d8d8d8;background:#fff;color:#555}");
html.AppendLine(" .sig-btn-primary{border-color:#258fd7;background:#3598dc;color:#fff}");
html.AppendLine(" .sig-btn-danger{border-color:#d52b37;background:#e43a45;color:#fff}");
html.AppendLine(" .sig-btn:disabled{cursor:wait;opacity:.6}");
html.AppendLine(" .sig-report-result{min-height:18px;margin-top:14px;color:#555;font-size:12px}");
html.AppendLine(" .sig-error-details{margin-top:20px;border-top:1px solid #eee;padding-top:15px}");
html.AppendLine(" .sig-error-details summary{color:#3598dc;font-size:13px;font-weight:600;cursor:pointer}");
html.AppendLine(" .sig-error-details pre{max-height:380px;margin:13px 0 0;padding:14px;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:3px;background:#263238;color:#f2f2f2;font-family:Consolas,monospace;font-size:12px;line-height:1.45}");
html.AppendLine(" @media(max-width:800px){.sig-sidebar{display:none}.sig-main{margin-left:0;padding:65px 14px 25px}.sig-header-status{display:none}.sig-error-actions{flex-direction:column}.sig-btn{width:100%;text-align:center}}");
html.AppendLine(" </style>");
html.AppendLine("</head>");
html.AppendLine("<body>");
html.AppendLine("<header class=\"sig-header\">");
html.AppendLine(" <div class=\"sig-brand\">");
html.AppendLine(" <div class=\"sig-brand-box\">SIG</div>");
html.AppendLine(" <span>Sistema de Informacion Gerencial</span>");
html.AppendLine(" </div>");
html.AppendLine(" <div class=\"sig-header-status\">");
if (!string.IsNullOrWhiteSpace(nombreUsuarioSeguro))
{
html.AppendLine(" <span class=\"sig-header-user\">" + nombreUsuarioSeguro + "</span>");
}
html.AppendLine(" </div>");
html.AppendLine("</header>");
html.AppendLine("<aside class=\"sig-sidebar\">");
html.AppendLine(" <div class=\"sig-sidebar-title\">Navegacion</div>");
html.AppendLine(" <ul id=\"sigMenuRestaurado\" class=\"sig-restored-menu\">");
html.AppendLine(" <li><a href=\"" + HttpUtility.HtmlAttributeEncode(urlInicio) + "\"><span>&#8962;</span><span>Inicio</span></a></li>");
html.AppendLine(" <li><button type=\"button\" onclick=\"history.back();\"><span>&#8592;</span><span>Regresar</span></button></li>");
html.AppendLine(" </ul>");
html.AppendLine("</aside>");
html.AppendLine("<main class=\"sig-main\">");
html.AppendLine(" <h1 class=\"sig-page-title\">Error inesperado</h1>");
html.AppendLine(" <div class=\"sig-breadcrumb\">Inicio &nbsp;/&nbsp; Error</div>");
html.AppendLine(" <section class=\"sig-error-card\">");
html.AppendLine(" <div class=\"sig-error-card-header\">");
html.AppendLine(" <div class=\"sig-error-icon\">!</div>");
html.AppendLine(" <div>");
html.AppendLine(" <h2 class=\"sig-error-title\">No fue posible completar la operacion</h2>");
html.AppendLine(" <div class=\"sig-error-subtitle\">La aplicacion sigue disponible.</div>");
html.AppendLine(" </div>");
html.AppendLine(" </div>");
html.AppendLine(" <div class=\"sig-error-body\">");
html.AppendLine(" <p>Ocurrio un error inesperado al cargar esta seccion. Puedes regresar, intentar nuevamente o reportarlo al area de soporte.</p>");
html.AppendLine(" <div class=\"sig-error-code-label\">Codigo para soporte</div>");
html.AppendLine(" <div class=\"sig-error-code\">");
html.AppendLine(" <span id=\"sigCodigoError\">" + codigoSeguro + "</span>");
html.AppendLine(" <button type=\"button\" class=\"sig-copy-button\" onclick=\"sigCopiarCodigo();\">Copiar codigo</button>");
html.AppendLine(" </div>");
html.AppendLine(" <div id=\"sigResultadoReporte\" class=\"sig-report-result\"></div>");
html.AppendLine(detalleHtml);
html.AppendLine(" <div class=\"sig-error-actions\">");
html.AppendLine(" <button type=\"button\" class=\"sig-btn sig-btn-default\" onclick=\"history.back();\">Regresar</button>");
html.AppendLine(" <a class=\"sig-btn sig-btn-default\" href=\"" + HttpUtility.HtmlAttributeEncode(urlInicio) + "\">Ir al inicio</a>");
html.AppendLine(" <button type=\"button\" class=\"sig-btn sig-btn-primary\" onclick=\"location.reload();\">Reintentar</button>");
html.AppendLine(" <button type=\"button\" id=\"sigBtnReportar\" class=\"sig-btn sig-btn-danger\" onclick=\"sigReportarError();\">Reportar a soporte</button>");
html.AppendLine(" </div>");
html.AppendLine(" </div>");
html.AppendLine(" </section>");
html.AppendLine("</main>");
html.AppendLine("<script>");
html.AppendLine("var sigCveUsuarioSesion='" + cveUsuarioJavaScript + "';");
html.AppendLine("var sigCodigoError='" + codigoJavaScript + "';");
html.AppendLine("var sigUrlReporte='" + HttpUtility.JavaScriptStringEncode(urlReporte) + "';");
html.AppendLine("var sigMenuStorageKey='sig.currentMenu.v1';");
html.AppendLine("function sigBuscarSubMenu(elemento){");
html.AppendLine(" var hijos=elemento.children;");
html.AppendLine(" for(var i=0;i<hijos.length;i++){");
html.AppendLine(" if(hijos[i].tagName==='UL' && (' '+hijos[i].className+' ').indexOf(' sub-menu ')>=0){return hijos[i];}");
html.AppendLine(" }");
html.AppendLine(" return null;");
html.AppendLine("}");
html.AppendLine("function sigInicializarMenu(menu){");
html.AppendLine(" menu.onclick=function(evento){");
html.AppendLine(" evento=evento||window.event;");
html.AppendLine(" var enlace=evento.target||evento.srcElement;");
html.AppendLine(" while(enlace&&enlace!==menu&&enlace.tagName!=='A'){enlace=enlace.parentNode;}");
html.AppendLine(" if(!enlace||enlace===menu){return;}");
html.AppendLine(" var li=enlace.parentNode;");
html.AppendLine(" var subMenu=sigBuscarSubMenu(li);");
html.AppendLine(" if(!subMenu){return;}");
html.AppendLine(" if(evento.preventDefault){evento.preventDefault();}else{evento.returnValue=false;}");
html.AppendLine(" var abierto=(' '+li.className+' ').indexOf(' open ')>=0;");
html.AppendLine(" li.className=abierto?li.className.replace(/\\bopen\\b/g,'').replace(/^\\s+|\\s+$/g,''):(li.className+' open').replace(/^\\s+|\\s+$/g,'');");
html.AppendLine(" };");
html.AppendLine("}");
html.AppendLine("function sigRestaurarMenu(){");
html.AppendLine(" if(!sigCveUsuarioSesion){return;}");
html.AppendLine(" try{");
html.AppendLine(" var contenido=localStorage.getItem(sigMenuStorageKey);");
html.AppendLine(" if(!contenido){return;}");
html.AppendLine(" var datos=JSON.parse(contenido);");
html.AppendLine(" if(!datos||!datos.menuHtml||!datos.cveUsuario){return;}");
html.AppendLine(" if(String(datos.cveUsuario)!==String(sigCveUsuarioSesion)){return;}");
html.AppendLine(" var menu=document.getElementById('sigMenuRestaurado');");
html.AppendLine(" if(!menu){return;}");
html.AppendLine(" menu.innerHTML=datos.menuHtml;");
html.AppendLine(" sigInicializarMenu(menu);");
html.AppendLine(" }catch(ex){if(window.console&&console.warn){console.warn('No fue posible restaurar el menu de SIG.',ex);}}");
html.AppendLine("}");
html.AppendLine("function sigCopiarCodigo(){");
html.AppendLine(" if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(sigCodigoError);}else{");
html.AppendLine(" var temporal=document.createElement('textarea');");
html.AppendLine(" temporal.value=sigCodigoError;");
html.AppendLine(" document.body.appendChild(temporal);");
html.AppendLine(" temporal.select();");
html.AppendLine(" document.execCommand('copy');");
html.AppendLine(" document.body.removeChild(temporal);");
html.AppendLine(" }");
html.AppendLine(" document.getElementById('sigResultadoReporte').textContent='El codigo fue copiado.';");
html.AppendLine("}");
html.AppendLine("function sigReportarError(){");
html.AppendLine(" var boton=document.getElementById('sigBtnReportar');");
html.AppendLine(" var resultado=document.getElementById('sigResultadoReporte');");
html.AppendLine(" boton.disabled=true;");
html.AppendLine(" resultado.textContent='Enviando reporte...';");
html.AppendLine(" var xhr=new XMLHttpRequest();");
html.AppendLine(" xhr.open('POST',sigUrlReporte,true);");
html.AppendLine(" xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded; charset=UTF-8');");
html.AppendLine(" xhr.onreadystatechange=function(){");
html.AppendLine(" if(xhr.readyState!==4){return;}");
html.AppendLine(" if(xhr.status>=200&&xhr.status<300){resultado.textContent='El error fue reportado correctamente.';return;}");
html.AppendLine(" boton.disabled=false;");
html.AppendLine(" resultado.textContent='No fue posible enviar el reporte. Comunica el codigo a soporte.';");
html.AppendLine(" };");
html.AppendLine(" xhr.send('codigo='+encodeURIComponent(sigCodigoError)+'&ruta='+encodeURIComponent(window.location.href));");
html.AppendLine("}");
html.AppendLine("sigRestaurarMenu();");
html.AppendLine("</script>");
html.AppendLine("</body>");
html.AppendLine("</html>");
return html.ToString();
}
public static string ObtenerDetalleTecnico(
Exception exception)
{
if (exception == null)
{
return "No se encontro informacion de la excepcion.";
}
StringBuilder texto = new StringBuilder();
Exception actual = exception;
int nivel = 1;
while (actual != null)
{
texto.AppendLine(
"EXCEPCION " + nivel);
texto.AppendLine(
"Tipo: " + actual.GetType().FullName);
texto.AppendLine(
"Mensaje: " + actual.Message);
HttpParseException parseException =
actual as HttpParseException;
if (parseException != null)
{
texto.AppendLine(
"Archivo: " + parseException.VirtualPath);
texto.AppendLine(
"Linea: " + parseException.Line);
}
if (!string.IsNullOrWhiteSpace(
actual.StackTrace))
{
texto.AppendLine();
texto.AppendLine("Stack trace:");
texto.AppendLine(actual.StackTrace);
}
texto.AppendLine();
texto.AppendLine(
"--------------------------------------------------");
texto.AppendLine();
actual = actual.InnerException;
nivel++;
}
return texto.ToString();
}
private static string ObtenerCodigoExistente(
Exception exception)
{
Exception actual = exception;
while (actual != null)
{
try
{
object valor =
actual.Data[ErrorCodeDataKey];
if (valor != null)
{
return valor.ToString();
}
}
catch
{
}
actual = actual.InnerException;
}
return string.Empty;
}
private static void AsignarCodigo(
Exception exception,
string codigo)
{
Exception actual = exception;
while (actual != null)
{
try
{
actual.Data[ErrorCodeDataKey] = codigo;
}
catch
{
}
actual = actual.InnerException;
}
}
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Configuration;
using System.IO;
using System.Text;
using System.Web;
namespace SIG.Web.AppCode.Services.Logging
{
public static class ExceptionLogger
{
private static readonly object LockObject = new object();
public static void Log(Exception exception, string source)
{
Log(exception, source, null);
}
public static void Log(Exception exception, string source, string userMessage)
{
try
{
if (exception == null)
{
return;
}
string logPath = GetLogPath();
string directory = Path.GetDirectoryName(logPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string content = BuildLogContent(exception, source, userMessage);
lock (LockObject)
{
File.AppendAllText(logPath, content, Encoding.UTF8);
}
}
catch
{
// Never throw from the logger.
// If the logger fails, the application must continue.
}
}
private static string GetLogPath()
{
string configuredPath = ConfigurationManager.AppSettings["ExceptionLogFilePath"];
if (string.IsNullOrWhiteSpace(configuredPath))
{
configuredPath = "~/log/log.txt";
}
if (HttpContext.Current != null)
{
return HttpContext.Current.Server.MapPath(configuredPath);
}
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log", "log.txt");
}
private static string BuildLogContent(Exception exception, string source, string userMessage)
{
var sb = new StringBuilder();
sb.AppendLine("============================================================");
sb.AppendLine("Date: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
sb.AppendLine("Source: " + Safe(source));
sb.AppendLine("UserMessage: " + Safe(userMessage));
HttpContext context = HttpContext.Current;
if (context != null && context.Request != null)
{
sb.AppendLine("Url: " + Safe(context.Request.Url != null ? context.Request.Url.ToString() : null));
sb.AppendLine("HttpMethod: " + Safe(context.Request.HttpMethod));
sb.AppendLine("DireccionIP: " + Safe(context.Request.UserHostAddress));
sb.AppendLine("AgenteUsuario: " + Safe(context.Request.UserAgent));
}
if (context != null && context.User != null && context.User.Identity != null)
{
sb.AppendLine("Identity: " + Safe(context.User.Identity.Name));
sb.AppendLine("IsAuthenticated: " + context.User.Identity.IsAuthenticated);
}
sb.AppendLine("ExceptionType: " + exception.GetType().FullName);
sb.AppendLine("Message: " + exception.Message);
sb.AppendLine("BaseException: " + exception.GetBaseException().Message);
sb.AppendLine("StackTrace:");
sb.AppendLine(exception.ToString());
sb.AppendLine();
return sb.ToString();
}
private static string Safe(string value)
{
return string.IsNullOrWhiteSpace(value) ? string.Empty : value;
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using SIG.Web.AppCode.Contracts.Login;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Services.Login
{
public sealed class DummyLoginService : ILoginService
{
public LoginResult Login(string nombreUsuario, string contrasena, string nombreDominio)
{
if (string.IsNullOrWhiteSpace(nombreUsuario))
{
return LoginResult.Fail("Escriba un nombre de usuario.");
}
return LoginResult.Ok(new UserSessionModel
{
CveUsuario = 15,
CveRol = 1,
CveEmpresa = 1,
NombreUsuario = nombreUsuario.Trim(),
Nombre = "Usuario Demo",
CorreoElectronico = "usuario.demo@example.invalid",
NombreRol = "Administrador",
NombreDominio = nombreDominio,
TokenSesion = Guid.NewGuid().ToString("N"),
DebeCambiarContrasena = false
});
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Services.Menu
{
public sealed class DummyMenuProvider : IMenuProvider
{
public IReadOnlyList<MenuItemModel> GetMenu(UserSessionModel user)
{
return new List<MenuItemModel>
{
new MenuItemModel { NombreModulo = "Home", Url = "~/Inicio.aspx", ClaseCssIcono = "icon-home" },
new MenuItemModel { NombreModulo = "Developer tools", Url = "#", ClaseCssIcono = "icon-puzzle", Hijos =
{
new MenuItemModel { NombreModulo = "Blank page", Url = "~/Inicio.aspx", ClaseCssIcono = "icon-doc" }
}
}
};
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Services.Database;
using SIG.Web.AppCode.Utilities;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Services.Menu
{
public sealed class MenuProvider : IMenuProvider
{
private readonly IDbExecutor _dbExecutor;
public MenuProvider(IDbExecutor dbExecutor)
{
_dbExecutor = dbExecutor;
}
public IReadOnlyList<MenuItemModel> GetMenu(UserSessionModel user)
{
if (user == null)
{
return new List<MenuItemModel>();
}
var modulesResult = GetModules(user);
if (!modulesResult.Success || modulesResult.Data == null)
{
return new List<MenuItemModel>();
}
return BuildTree(modulesResult.Data);
}
private SIG.Web.AppCode.Models.Common.DbResult<List<MenuItemModel>> GetModules(UserSessionModel user)
{
return _dbExecutor.Execute(user.NombreDominio, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_MenuList", user.CveUsuario);
StoredProcedureCommand.AddInput(command, "@CveRol", user.CveRol, DbType.Int32);
List<MenuItemModel> items;
using (IDataReader reader = command.ExecuteReader())
{
items = reader.ToList<MenuItemModel>();
}
StoredProcedureCommand.EnsureSuccess(command);
return items;
}
});
}
private static IReadOnlyList<MenuItemModel> BuildTree(List<MenuItemModel> flatItems)
{
if (flatItems == null || flatItems.Count == 0)
{
return new List<MenuItemModel>();
}
var byId = flatItems.ToDictionary(x => x.CveModulo);
var roots = new List<MenuItemModel>();
foreach (var item in flatItems.OrderBy(x => x.OrdenVisualizacion).ThenBy(x => x.NombreModulo))
{
if (item.CveModuloPadre.HasValue && byId.ContainsKey(item.CveModuloPadre.Value))
{
byId[item.CveModuloPadre.Value].Hijos.Add(item);
}
else
{
roots.Add(item);
}
}
return roots;
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using SIG.Web.AppCode.Contracts.Search;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Services.Search
{
public sealed class DummyBuscadorCatalogoDataProvider : IBuscadorCatalogoDataProvider
{
public BuscadorCatalogoCallbackRespuesta Search(BuscadorCatalogoCallbackSolicitud request, UserSessionModel user)
{
var response = new BuscadorCatalogoCallbackRespuesta { Total = 1 };
response.Rows.Add(new Dictionary<string, object>
{
["Id"] = 1,
["Descripcion"] = "Dummy result"
});
return response;
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Models.Security;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Services.Security
{
public sealed class LoginService : ILoginService
{
private const string GenericError = "No fue posible iniciar sesion. Verifique sus datos.";
private readonly IUserRepository _userRepository;
private readonly IPasswordHasher _passwordHasher;
private readonly ILoginAttemptRepository _loginAttemptRepository;
private readonly IUserSessionRepository _userSessionRepository;
public LoginService(
IUserRepository userRepository,
IPasswordHasher passwordHasher,
ILoginAttemptRepository loginAttemptRepository,
IUserSessionRepository userSessionRepository)
{
_userRepository = userRepository;
_passwordHasher = passwordHasher;
_loginAttemptRepository = loginAttemptRepository;
_userSessionRepository = userSessionRepository;
}
public LoginResult Login(LoginRequest request)
{
if (request == null || string.IsNullOrWhiteSpace(request.NombreUsuario) || string.IsNullOrWhiteSpace(request.Contrasena))
{
return LoginResult.Fail(GenericError);
}
string username = request.NombreUsuario.Trim();
var userResult = _userRepository.ObtenerPorNombreUsuario(username, request.NombreDominio);
if (!userResult.Success)
{
return LoginResult.Fail(userResult.UserMessage);
}
UserModel user = userResult.Data;
if (user == null)
{
_loginAttemptRepository.Save(null, username, false, "UserNotFound", request.DireccionIP, request.AgenteUsuario, request.NombreDominio);
return LoginResult.Fail(GenericError);
}
if (!user.CorreoVerificado)
{
_loginAttemptRepository.Save(user.CveUsuario, username, false, "EmailNotVerified", request.DireccionIP, request.AgenteUsuario, request.NombreDominio);
return LoginResult.Fail("Debes verificar tu correo electrónico antes de iniciar sesión.");
}
if (!user.Activo)
{
_loginAttemptRepository.Save(user.CveUsuario, username, false, "InactiveUser", request.DireccionIP, request.AgenteUsuario, request.NombreDominio);
return LoginResult.Fail("El usuario se encuentra inactivo.");
}
if (user.BloqueadoHasta.HasValue && user.BloqueadoHasta.Value > DateTime.Now) {
_loginAttemptRepository.Save(user.CveUsuario, username, false, "LockedUser", request.DireccionIP, request.AgenteUsuario, request.NombreDominio);
return LoginResult.Fail("El usuario se encuentra bloqueado temporalmente.");
}
if (!_passwordHasher.VerifyPassword(request.Contrasena, user.HashContrasena))
{
_userRepository.RegisterFailedLogin(user.CveUsuario, request.NombreDominio);
_loginAttemptRepository.Save(user.CveUsuario, username, false, "InvalidPassword", request.DireccionIP, request.AgenteUsuario, request.NombreDominio);
return LoginResult.Fail(GenericError);
}
string sessionToken = Guid.NewGuid().ToString("N");
var loginUpdateResult = _userRepository.RegisterSuccessfulLogin(user.CveUsuario, request.NombreDominio);
if (!loginUpdateResult.Success)
{
return LoginResult.Fail(loginUpdateResult.UserMessage);
}
var sessionResult = _userSessionRepository.StartSession(user.CveUsuario, sessionToken, request.DireccionIP, request.AgenteUsuario, request.NombreDominio);
if (!sessionResult.Success)
{
return LoginResult.Fail(sessionResult.UserMessage);
}
_loginAttemptRepository.Save(user.CveUsuario, username, true, null, request.DireccionIP, request.AgenteUsuario, request.NombreDominio);
var sessionUser = new UserSessionModel
{
CveUsuario = user.CveUsuario,
CveRol = user.CveRol,
NombreUsuario = user.NombreUsuario,
Nombre = user.NombreUsuario,
CorreoElectronico = user.CorreoElectronico,
NombreRol = user.NombreRol,
NombreDominio = request.NombreDominio,
TokenSesion = sessionToken,
DebeCambiarContrasena = user.DebeCambiarContrasena
};
return LoginResult.Ok(sessionUser);
}
public void Logout(string sessionToken, string domainName)
{
if (!string.IsNullOrWhiteSpace(sessionToken))
{
_userSessionRepository.EndSession(sessionToken, domainName);
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Security.Cryptography;
using SIG.Web.AppCode.Contracts.Security;
namespace SIG.Web.AppCode.Services.Security
{
public sealed class PasswordHasher : IPasswordHasher
{
private const int SaltSize = 16;
private const int HashSize = 32;
private const int Iterations = 100000;
public string HashPassword(string password)
{
byte[] salt = new byte[SaltSize];
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(salt);
byte[] hash = Derive(password, salt, Iterations);
return "PBKDF2-SHA256$" + Iterations + "$" + Convert.ToBase64String(salt) + "$" + Convert.ToBase64String(hash);
}
public bool VerifyPassword(string password, string storedHash)
{
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(storedHash)) return false;
string[] parts = storedHash.Split('$');
if (parts.Length != 4 || parts[0] != "PBKDF2-SHA256") return false;
int iterations; if (!int.TryParse(parts[1], out iterations)) return false;
byte[] salt = Convert.FromBase64String(parts[2]);
byte[] expected = Convert.FromBase64String(parts[3]);
byte[] actual = Derive(password, salt, iterations);
return FixedTimeEquals(actual, expected);
}
private static byte[] Derive(string password, byte[] salt, int iterations)
{
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256)) return pbkdf2.GetBytes(HashSize);
}
private static bool FixedTimeEquals(byte[] left, byte[] right)
{
if (left == null || right == null || left.Length != right.Length) return false;
int diff = 0; for (int i = 0; i < left.Length; i++) diff |= left[i] ^ right[i];
return diff == 0;
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Data;
using SIG.Web.AppCode.Contracts.Database;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Services.Database;
namespace SIG.Web.AppCode.Services.Security
{
public sealed class PermissionService : IPermissionService
{
private readonly IDbExecutor _dbExecutor;
public PermissionService(IDbExecutor dbExecutor)
{
_dbExecutor = dbExecutor;
}
public bool HasPermission(int cveUser, string permissionCode)
{
if (cveUser <= 0 || string.IsNullOrWhiteSpace(permissionCode))
{
return false;
}
var result = _dbExecutor.Execute(null, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_PermisoCheck", cveUser);
StoredProcedureCommand.AddInput(command, "@CodigoPermiso", permissionCode.Trim(), DbType.String);
object value = command.ExecuteScalar();
StoredProcedureCommand.EnsureSuccess(command);
return value != null && value != DBNull.Value && Convert.ToBoolean(value);
}
});
return result.Success && result.Data;
}
public bool CanAccessPage(int cveUser, string pageUrl)
{
if (cveUser <= 0)
{
return false;
}
string normalizedUrl = NormalizePageUrl(pageUrl);
if (IsPublicPage(normalizedUrl))
{
return true;
}
var result = _dbExecutor.Execute(null, connection =>
{
using (IDbCommand command = connection.CreateCommand())
{
StoredProcedureCommand.Configure(command, "dbo.PA_Sis_PaginaAccessCheck", cveUser);
StoredProcedureCommand.AddInput(command, "@PaginaUrl", normalizedUrl, DbType.String);
object value = command.ExecuteScalar();
StoredProcedureCommand.EnsureSuccess(command);
return value != null && value != DBNull.Value && Convert.ToBoolean(value);
}
});
return result.Success && result.Data;
}
private static string NormalizePageUrl(string pageUrl)
{
if (string.IsNullOrWhiteSpace(pageUrl))
{
return string.Empty;
}
string value = pageUrl.Trim();
if (value.StartsWith("~/", StringComparison.Ordinal))
{
value = value.Substring(2);
}
if (value.StartsWith("/", StringComparison.Ordinal))
{
value = value.Substring(1);
}
return value;
}
private static bool IsPublicPage(string normalizedUrl)
{
return normalizedUrl.Equals("Login.aspx", StringComparison.OrdinalIgnoreCase) ||
normalizedUrl.Equals("Logout.aspx", StringComparison.OrdinalIgnoreCase) ||
normalizedUrl.Equals("AccessDenied.aspx", StringComparison.OrdinalIgnoreCase) ||
normalizedUrl.Equals("Error.aspx", StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Models.Common;
using SIG.Web.AppCode.Models.Security;
namespace SIG.Web.AppCode.Services.Security
{
public sealed class PublicRegistrationService : IPublicRegistrationService
{
private readonly IUserAdministrationRepository _repository;
private readonly IPasswordHasher _passwordHasher;
private readonly IEmailSender _emailSender;
public PublicRegistrationService(IUserAdministrationRepository repository, IPasswordHasher passwordHasher, IEmailSender emailSender) { _repository = repository; _passwordHasher = passwordHasher; _emailSender = emailSender; }
public DbResult<bool> Validate(PublicRegistrationDraft draft, string domainName)
{
string error = ValidateRequired(draft);
return error == null ? _repository.ValidateRegistration(draft.NombreUsuario, draft.CorreoElectronico, draft.RFC, domainName) : DbResult<bool>.Fail(error, error, null);
}
public DbResult<List<StateOptionModel>> ListStates(string domainName) { return _repository.ListStates(domainName); }
public DbResult<bool> Register(PublicRegistrationDraft draft, string verificationBaseUrl, string domainName)
{
DbResult<bool> validation = Validate(draft, domainName); if (!validation.Success) return validation;
if (!_emailSender.IsConfigured) return DbResult<bool>.Fail("El envío de correo todavía no está configurado. Contacta a Sistemas.", "SMTP no configurado.", null);
string token = CreateToken();
DbResult<RegistrationCreatedModel> created = _repository.CreateRegistration(draft, _passwordHasher.HashPassword(draft.Contrasena), HashToken(token), domainName);
if (!created.Success) return DbResult<bool>.Fail(created.UserMessage, created.TechnicalMessage, created.Exception);
try
{
_emailSender.SendVerification(draft.CorreoElectronico, verificationBaseUrl + (verificationBaseUrl.Contains("?") ? "&" : "?") + "token=" + Uri.EscapeDataString(token));
return DbResult<bool>.Ok(true);
}
catch (Exception ex) { return DbResult<bool>.Fail("La cuenta quedó pendiente, pero no fue posible enviar el correo. Contacta a Sistemas para reenviarlo.", ex.Message, ex); }
}
public DbResult<bool> Verify(string token, string domainName)
{
return string.IsNullOrWhiteSpace(token) ? DbResult<bool>.Fail("La liga de verificación no es válida.", "Token vacío.", null) : _repository.VerifyEmail(HashToken(token), domainName);
}
private static string ValidateRequired(PublicRegistrationDraft d)
{
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.";
return null;
}
private static string CreateToken() { byte[] bytes = new byte[32]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(bytes); return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_'); }
private static string HashToken(string token) { using (SHA256 sha = SHA256.Create()) { byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(token)); var value = new StringBuilder(64); foreach (byte item in hash) value.Append(item.ToString("x2")); return value.ToString(); } }
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Configuration;
using System.Net;
using System.Net.Mail;
using SIG.Web.AppCode.Contracts.Security;
namespace SIG.Web.AppCode.Services.Security
{
public sealed class SmtpEmailSender : IEmailSender
{
public bool IsConfigured { get { return !string.IsNullOrWhiteSpace(ConfigurationManager.AppSettings["SmtpHost"]) && !string.IsNullOrWhiteSpace(ConfigurationManager.AppSettings["EmailFrom"]); } }
public void SendVerification(string recipient, string verificationUrl)
{
if (!IsConfigured) throw new InvalidOperationException("El envío de correo no se encuentra configurado.");
int port; if (!int.TryParse(ConfigurationManager.AppSettings["SmtpPort"], out port)) port = 25;
bool ssl; bool.TryParse(ConfigurationManager.AppSettings["SmtpEnableSsl"], out ssl);
using (var message = new MailMessage(ConfigurationManager.AppSettings["EmailFrom"], recipient))
using (var client = new SmtpClient(ConfigurationManager.AppSettings["SmtpHost"], port))
{
message.Subject = "Verifica tu correo - Plataforma de Desarrolladores";
message.IsBodyHtml = true;
message.Body = "<p>Para concluir la creación de tu cuenta, abre la siguiente liga:</p><p><a href=\"" + WebUtility.HtmlEncode(verificationUrl) + "\">Verificar mi correo</a></p><p>La liga vence en 24 horas.</p>";
client.EnableSsl = ssl;
string user = ConfigurationManager.AppSettings["SmtpUser"];
if (!string.IsNullOrWhiteSpace(user)) client.Credentials = new NetworkCredential(user, ConfigurationManager.AppSettings["SmtpPassword"]);
client.Send(message);
}
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Web;
using SIG.Web.AppCode.Contracts.Security;
using SIG.Web.AppCode.Session;
namespace SIG.Web.AppCode.Services.Security
{
public sealed class WebFormsUserContextProvider : Contracts.Security.IUserContextProvider
{
public bool IsAuthenticated() { return GetCurrentUser() != null; }
public UserSessionModel GetCurrentUser()
{
return HttpContext.Current == null || HttpContext.Current.Session == null ? null : HttpContext.Current.Session[SessionKeys.CurrentUser] as UserSessionModel;
}
public void SetCurrentUser(UserSessionModel user)
{
if (HttpContext.Current == null || HttpContext.Current.Session == null)
return; HttpContext.Current.Session[SessionKeys.CurrentUser] = user;
HttpContext.Current.Session[SessionKeys.TokenSesion] = user != null ? user.TokenSesion : null;
}
public void Clear()
{
if (HttpContext.Current == null || HttpContext.Current.Session == null)
return; HttpContext.Current.Session.Remove(SessionKeys.CurrentUser);
HttpContext.Current.Session.Remove(SessionKeys.TokenSesion);
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace SIG.Web.AppCode.Session
{
public interface IMenuProvider
{
IReadOnlyList<MenuItemModel> GetMenu(UserSessionModel user);
}
}

View File

@@ -0,0 +1,10 @@
namespace SIG.Web.AppCode.Session
{
public interface ISessionContext
{
T Get<T>(string key) where T : class;
void Set<T>(string key, T value) where T : class;
void Remove(string key);
void Clear();
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace SIG.Web.AppCode.Session
{
[Serializable]
public sealed class MenuItemModel
{
public int CveModulo { get; set; }
public int? CveModuloPadre { get; set; }
public string NombreModulo { get; set; }
public string Url { get; set; }
public string ClaseCssIcono { get; set; }
public string ClaseCssEstilo { get; set; }
public int OrdenVisualizacion { get; set; }
public bool Visible { get; set; } = true;
public List<MenuItemModel> Hijos { get; set; } = new List<MenuItemModel>();
}
}

View File

@@ -0,0 +1,8 @@
namespace SIG.Web.AppCode.Session
{
public static class SessionKeys
{
public const string CurrentUser = "SIG_CurrentUser";
public const string TokenSesion = "SIG_SessionToken";
}
}

View File

@@ -0,0 +1,16 @@
namespace SIG.Web.AppCode.Session
{
public sealed class UserSessionModel
{
public int CveUsuario { get; set; }
public int CveRol { get; set; }
public int CveEmpresa { get; set; }
public string NombreUsuario { get; set; }
public string Nombre { get; set; }
public string CorreoElectronico { get; set; }
public string NombreRol { get; set; }
public string NombreDominio { get; set; }
public string TokenSesion { get; set; }
public bool DebeCambiarContrasena { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using System.Web;
namespace SIG.Web.AppCode.Session
{
public sealed class WebFormsSessionContext : ISessionContext
{
public T Get<T>(string key) where T : class
{
return HttpContext.Current?.Session?[key] as T;
}
public void Set<T>(string key, T value) where T : class
{
if (HttpContext.Current?.Session == null) return;
HttpContext.Current.Session[key] = value;
}
public void Remove(string key)
{
HttpContext.Current?.Session?.Remove(key);
}
public void Clear()
{
HttpContext.Current?.Session?.Clear();
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;
using Newtonsoft.Json;
using SIG.Web.AppCode.Configuration;
using SIG.Web.AppCode.Contracts.Search;
namespace SIG.Web.AppCode.SigControls
{
[ToolboxData("<{0}:BuscadorCatalogo runat=server></{0}:BuscadorCatalogo>")]
public class BuscadorCatalogo : CompositeControl, INamingContainer
{
private TextBox _textBox;
private HiddenField _value;
private Button _button;
[Category("SIG")]
public string Catalogo
{
get { return ViewState["Catalogo"] as string ?? string.Empty; }
set { ViewState["Catalogo"] = value; }
}
[Category("SIG")]
public string Text
{
get { EnsureChildControls(); return _textBox.Text; }
set { EnsureChildControls(); _textBox.Text = value; }
}
[Category("SIG")]
public string SelectedValue
{
get { EnsureChildControls(); return _value.Value; }
set { EnsureChildControls(); _value.Value = value; }
}
protected override void CreateChildControls()
{
Controls.Clear();
_textBox = new TextBox { ID = "Txt", CssClass = "form-control sig-buscador-input" };
_value = new HiddenField { ID = "Hdn" };
_button = new Button { ID = "Btn", Text = "...", CssClass = "btn btn-default sig-buscador-button" };
_button.OnClientClick = "return false;";
Controls.Add(_value);
Controls.Add(_textBox);
Controls.Add(_button);
}
protected override void Render(HtmlTextWriter writer)
{
EnsureChildControls();
writer.AddAttribute(HtmlTextWriterAttribute.Class, "input-group sig-buscador-catalogo");
writer.AddAttribute("data-catalogo", Catalogo ?? string.Empty);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
_textBox.RenderControl(writer);
writer.AddAttribute(HtmlTextWriterAttribute.Class, "input-group-btn");
writer.RenderBeginTag(HtmlTextWriterTag.Span);
_button.RenderControl(writer);
writer.RenderEndTag();
_value.RenderControl(writer);
writer.RenderEndTag();
}
public static string SearchJson(string json)
{
var request = JsonConvert.DeserializeObject<BuscadorCatalogoCallbackSolicitud>(json ?? "{}") ?? new BuscadorCatalogoCallbackSolicitud();
var user = AppServices.UserContext.GetCurrentUser();
var response = AppServices.SearchProvider.Search(request, user);
return JsonConvert.SerializeObject(response);
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace SIG.Web.AppCode.SigControls
{
public sealed class DropdownGridColumn
{
public string DataField { get; set; }
public string HeaderText { get; set; }
public bool Visible { get; set; } = true;
}
[ToolboxData("<{0}:DropdownGrid runat=server></{0}:DropdownGrid>")]
public class DropdownGrid : CompositeControl, INamingContainer
{
private HiddenField _selectedValues;
private TextBox _text;
public List<DropdownGridColumn> Columns
{
get { return ViewState["Columns"] as List<DropdownGridColumn> ?? new List<DropdownGridColumn>(); }
set { ViewState["Columns"] = value; }
}
[Category("SIG")]
public bool Multiple
{
get { return ViewState["Multiple"] as bool? ?? false; }
set { ViewState["Multiple"] = value; }
}
[Category("SIG")]
public string SelectedValues
{
get { EnsureChildControls(); return _selectedValues.Value; }
set { EnsureChildControls(); _selectedValues.Value = value; }
}
[Category("SIG")]
public string Text
{
get { EnsureChildControls(); return _text.Text; }
set { EnsureChildControls(); _text.Text = value; }
}
protected override void CreateChildControls()
{
Controls.Clear();
_selectedValues = new HiddenField { ID = "HdnValues" };
_text = new TextBox { ID = "TxtText", CssClass = "form-control sig-dropdown-grid-text" };
Controls.Add(_selectedValues);
Controls.Add(_text);
}
protected override void Render(HtmlTextWriter writer)
{
EnsureChildControls();
writer.AddAttribute(HtmlTextWriterAttribute.Class, "sig-dropdown-grid");
writer.AddAttribute("data-multiple", Multiple ? "true" : "false");
writer.RenderBeginTag(HtmlTextWriterTag.Div);
_selectedValues.RenderControl(writer);
_text.RenderControl(writer);
writer.AddAttribute(HtmlTextWriterAttribute.Class, "sig-dropdown-grid-popup");
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.Write("<!-- grid popup rendered by JavaScript -->");
writer.RenderEndTag();
writer.RenderEndTag();
}
public IReadOnlyList<string> GetSelectedValueList()
{
return (SelectedValues ?? string.Empty)
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToList();
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace SIG.Web.AppCode.SigControls
{
[Serializable]
public sealed class FiltroActivo
{
public string Nombre { get; set; }
public string Titulo { get; set; }
public string Descripcion { get; set; }
public int Orden { get; set; }
public bool Visible { get; set; } = true;
}
}

View File

@@ -0,0 +1,9 @@
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfClientAction
{
public string Action { get; set; }
public string Message { get; set; }
public string Url { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfDocumentComposer
{
public byte[] Compose(byte[] originalPdf)
{
return originalPdf ?? new byte[0];
}
}
}

View File

@@ -0,0 +1,9 @@
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfSignatureAsset
{
public string Name { get; set; }
public byte[] Bytes { get; set; }
public string ContentType { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfSignatureComposer
{
public byte[] ApplySignatures(byte[] originalPdf, IEnumerable<PdfSignaturePlacement> placements)
{
// Placeholder. Keep signature composition behind this class.
return originalPdf ?? new byte[0];
}
}
}

View File

@@ -0,0 +1,11 @@
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfSignaturePlacement
{
public int Page { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfSignatureSavingEventArgs : EventArgs
{
public PdfSignatureAsset Signature { get; }
public bool Cancel { get; set; }
public PdfSignatureSavingEventArgs(PdfSignatureAsset signature)
{
Signature = signature;
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfSignedDocumentEventArgs : EventArgs
{
public byte[] Bytes { get; }
public string FileName { get; }
public PdfSignedDocumentEventArgs(byte[] bytes, string fileName)
{
Bytes = bytes;
FileName = fileName;
}
}
}

View File

@@ -0,0 +1,11 @@
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public enum PdfSourceMode
{
None,
Bytes,
FilePath,
Url,
TempStorage
}
}

View File

@@ -0,0 +1,9 @@
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public sealed class PdfStoredSignature
{
public int Id { get; set; }
public string Name { get; set; }
public string DataUrl { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.IO;
using System.Web;
namespace SIG.Web.AppCode.SigControls.PDFViewer
{
public static class PdfTempStorage
{
private const string Folder = "~/App_Data/PdfTemp";
public static string Save(byte[] bytes, string extension = ".pdf")
{
if (bytes == null) throw new ArgumentNullException(nameof(bytes));
var dir = HttpContext.Current.Server.MapPath(Folder);
Directory.CreateDirectory(dir);
var fileName = Guid.NewGuid().ToString("N") + extension;
var path = Path.Combine(dir, fileName);
File.WriteAllBytes(path, bytes);
return path;
}
public static byte[] Read(string path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return null;
return File.ReadAllBytes(path);
}
}
}

Some files were not shown because too many files have changed in this diff Show More