Apps multi-tenant
Una app tuya para muchas empresas: inicio de sesión con DinaupAuthClient, un MyAppClient por usuario vinculado a su empresa y el almacén clave-valor MyAppKVClient.
Una app multi-tenant es una sola aplicación tuya que da servicio a muchas empresas, cada una con sus usuarios y sus datos. Tú escribes la interfaz y la lógica; Dinaup pone el inicio de sesión, el aislamiento entre empresas y la base de datos.
Antes de empezar
- Paquete
Dinaupinstalado y tu biblioteca MyDinaup para trabajar tipado. - El
AppId(un GUID) y elAppTokende tu aplicación, registrados en Dinaup. Guárdalos en el Vault con las clavesDINAUP_APPIDyDINAUP_APPTOKEN.
Las tres piezas
| Pieza | Qué es | Ciclo de vida |
|---|---|---|
DinaupExternalAppSettings | La identidad de tu aplicación: AppId + AppToken. | Singleton |
DinaupAuthClient | Autentica usuarios por correo y contraseña. Devuelve a qué empresa pertenecen. | Singleton |
MyAppClient | El cliente de datos de un usuario concreto, vinculado a su empresa. | Uno por sesión (Scoped) |
MyAppClient hereda de DinaupClientC: informes, escritura, archivos y anotaciones funcionan igual que en el Cliente Dinaup. La diferencia es a qué datos llega: cada instancia opera solo sobre la empresa del usuario autenticado.
Montar la app
Carga la identidad de la app
var vault = new Dinaup.Vault.VaultData("VAULT_URL", "VAULT_PASSWORD");
vault.Initialize();
var appConfig = new Dinaup.Models.DinaupExternalAppSettings(vault);El constructor con VaultData lee DINAUP_APPID y DINAUP_APPTOKEN. Para pasarlas directamente: new DinaupExternalAppSettings(appId, appToken), con appId como Guid.
Registra los servicios
// Compartidos por todos los usuarios
builder.Services.AddSingleton<Dinaup.Models.DinaupExternalAppSettings>(appConfig);
builder.Services.AddSingleton<Dinaup.Auth.DinaupAuthClient>(new Dinaup.Auth.DinaupAuthClient());
builder.Services.AddSingleton<Dinaup.MyAppKVClient>();
// Uno por usuario/circuito
builder.Services.AddScoped<SessionUserContext>();
builder.Services.AddHealthChecks().AddCheck<Dinaup.MyAppKVClient>("MyAppKVClient");MyAppKVClient se construye con el DinaupExternalAppSettings registrado.
El MyAppClient y cualquier servicio que lo use van en Scoped. Un singleton compartiría la sesión de una empresa con los usuarios de otra.
Autentica al usuario
LoginAsync(email, password) valida las credenciales contra auth.dinaup.com y devuelve un AuthResponse, o null si fallan. TenantConnectionKeyword identifica la empresa del usuario; con él construyes su MyAppClient.
var authResponse = await _authClient.LoginAsync(email, password);
if (authResponse == null) throw new Exception("Credenciales inválidas");
DinaupClient = new Dinaup.MyAppClient(_appSettings, authResponse);El AuthResponse trae además UserEmail, UserId, LicId, LicSerie, ApiEndpoint, Version, Revision y TwoFactor.
Para que la sesión sobreviva a recargas, el navegador guarda solo una cookie HttpOnly con un id de sesión. El estado (clave de conexión y correo) se guarda en el almacén clave-valor bajo ese id, y al volver se recupera y se reconstruye el cliente con el tercer constructor, new MyAppClient(settings, kwCon, userEmail):
// Al hacer login: estado al KV, solo el id a la cookie
var sessionId = Guid.NewGuid().ToString();
await _kvClient.SetKVAsync($"session:{sessionId}",
$"{authResponse.TenantConnectionKeyword}|{authResponse.UserEmail}");
httpContext.Response.Cookies.Append("dinaup_sessionid", sessionId, cookieOptions);
// Al volver: recuperar el estado y reconstruir
var sessionId = httpContext.Request.Cookies["dinaup_sessionid"];
if (string.IsNullOrEmpty(sessionId) == false)
{
var estado = await _kvClient.GetKVAsync($"session:{sessionId}");
if (string.IsNullOrEmpty(estado) == false)
{
var partes = estado.Split('|');
DinaupClient = new Dinaup.MyAppClient(_appSettings, partes[0], partes[1]);
}
}Con el estado de sesión fuera del proceso, la app no guarda estado propio: el mismo contenedor se ejecuta en varios nodos detrás de un balanceador y la cookie vale en cualquiera.
Opera con los datos de su empresa
El cliente ya está vinculado a la empresa. Un servicio de dominio recibe el contexto de sesión y usa su cliente:
public class PaisesService
{
private readonly SessionUserContext _session;
public PaisesService(SessionUserContext session) => _session = session;
public async Task<List<APIPaisesC.APIPaises_RowC>> GetAllAsync()
{
var rpt = new APIPaisesC();
await rpt.ExecuteQueryAsync(_session.DinaupClient, 1, 1000);
return rpt.Rows;
}
}Informes, WriteOperation, archivos y anotaciones: todo lo del Cliente Dinaup aplica sin cambios.
Qué añade MyAppClient
| Miembro | Qué hace |
|---|---|
ExecuteReport(ReportRequestParameters, filter) | Ejecuta un informe por parámetros y devuelve un ReportResponse. |
ExecuteReport(id, QuerySearch, AdvancedFilter, ResultsPerPage, order, adminmode) | Ejecuta un informe por id con búsqueda, filtros (List<FilterCondition>), orden y modo administrador. Hay sobrecargas con variables (Dictionary<string, string>) y con LoadDataReportC. |
Email, Nombre, IsLogged | Datos de la sesión. MyAppClient implementa IUserSession y se asigna como DefaultSession del cliente. |
LicenseId, UserId | Devuelven Guid.Empty. Para atribuir escrituras a un usuario concreto, usa DinaupContext.WithUser. |
Cuentas de usuario
MyAppClient hereda las operaciones de cuenta de DinaupClientC. Tu app da de alta usuarios, los activa y gestiona contraseñas sin pantallas de Dinaup. Todas reciben el userAgent y la IP del usuario final.
| Método | Qué hace |
|---|---|
Session_RegisterAccountAsync(model, userAgent, userIP) | Crea la cuenta temporal y genera el código de activación. model es un AccountCreationModel. |
Session_ConfirmAccountRegistrationAsync(model, userAgent, userIP) | Activa la cuenta usando el código recibido. model es un AccountActivationModel. |
Session_SignInAsync(email, password, userAgent, userIP, verifyPassword = true) | Inicia sesión y devuelve un SessionSignInResponse (correcta, 2FA pendiente, rechazada). |
Session_CreateLoginCodeAsync(email, userAgent, userIP) y Session_SignInWithCodeAsync(email, code, userAgent, userIP) | Acceso sin contraseña: envía un código y entra con él. |
Session_CheckTwoFactor(sessionId, code, userAgent, userIP) | Verifica el código 2FA de una sesión pendiente. sessionId es Guid. |
Session_GetDetailsAsync(sessionId, userAgent, userIP) | Detalles de una sesión activa. |
Session_SignOutAsync(sessionId, userAgent, userIP) | Cierra la sesión. |
Session_RecoverPasswordAsync(email, userAgent, userIP) | Inicia la recuperación de contraseña por correo. |
Session_CreatePasswordRecoveryCodeAsync(model, userAgent, userIP) | Genera un código de recuperación. model es un PasswordRecoveryModel. |
Session_ChangePasswordWithCodeAsync(userEmail, code, newPassword, userAgent, userIP) | Cambia la contraseña con el código de recuperación. |
Para cambiar la contraseña de un usuario, genera el código con Session_CreatePasswordRecoveryCodeAsync y cámbiala con Session_ChangePasswordWithCodeAsync.
Los modelos de entrada (AccountCreationModel, AccountActivationModel, PasswordRecoveryModel, ChangePasswordModel, SignInModel, en Dinaup.Models) validan sus campos antes de llamar al servidor.
Almacenamiento clave-valor de la app
MyAppKVClient guarda pares clave-valor propios de tu aplicación: configuración, indicadores y las sesiones de tus usuarios (el patrón del paso 3). Es el estado compartido entre nodos: con él, la app no guarda estado en el proceso.
bool guardado = await _kvClient.SetKVAsync("clave", "valor");
string valor = await _kvClient.GetKVAsync("clave");- El valor es texto plano, hasta 10 MB por clave; por encima,
SetKVAsynclanzaInvalidOperationException. - El espacio de claves es único por aplicación, no por empresa. Si un valor pertenece a una empresa o a una sesión, prefíjalo:
session:{id},config:{empresa}. GetKVAsyncdevuelve cadena vacía si la clave no existe. Un error HTTP lanzaHttpRequestException.
Registrado como comprobación de estado (AddCheck<MyAppKVClient>), escribe la clave myappkvclient.health en cada sonda y responde Unhealthy si falla. Cómo exponer el endpoint /HealthCheck está en ASP.NET Core.
El KV de una empresa es otro almacén: KV_GetAsync(space, key) y KV_SetAsync(space, key, value) de DinaupClientC guardan pares en su sección Dinaup KV, no en tu app. Ver Cliente Dinaup.
Ejecutar como un usuario concreto
En procesos sin sesión interactiva (importaciones, tareas programadas), envuelve la operación con DinaupContext.WithUser(userId, ip = "", userAgent = "") para atribuirla a un usuario: afecta al autor del alta, al histórico y a las anotaciones.
using (DinaupContext.WithUser(userId, ip, userAgent))
{
await client.RunWriteOperationAsync(sectionId, op, false);
}Para el detalle de informes, filtros y escritura, ver el Cliente Dinaup.