Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CodexaCP/DG_APK/llms.txt

Use this file to discover all available pages before exploring further.

SessionService is the single authoritative source of authentication state for Dragon Guard Handheld. It owns all reading, writing, and clearing of session data; no other part of the application touches the token or company context directly. Every protected HTTP request is decorated by AuthenticatedHttpMessageHandler, which reads from SessionService at call-time — there is no separate in-memory cache to synchronise. This design means that a sign-out or a token invalidation is immediately effective for all subsequent requests, regardless of which screen the operator is on.

Session storage keys

SessionService defines a fixed set of string constants for the underlying storage keys. These names are the canonical identifiers used in SecureStorage and in the Preferences fallback.
private const string TokenKey         = "auth_token";
private const string EmailKey         = "auth_email";
private const string FullNameKey      = "auth_full_name";
private const string CompaniesKey     = "auth_companies";
private const string ActiveCompanyIdKey = "auth_active_company_id";
Session data is written to SecureStorage first. On devices where SecureStorage is unavailable (older Android versions without a compatible hardware-backed keystore, or certain emulators), the service automatically falls back to Preferences. Preferences data is not encrypted at the OS level, so production deployments should always use hardware that supports Android Keystore.
private static async Task<string?> ReadAsync(string key)
{
    try   { return await SecureStorage.Default.GetAsync(key); }
    catch { return Preferences.Default.Get<string?>(key, null); }
}

Stored fields

FieldStorage keyTypeDescription
Bearer tokenauth_tokenstringJWT issued by the server on successful login
Emailauth_emailstringOperator’s email address
Full nameauth_full_namestringOperator’s display name
Companiesauth_companiesJSON stringSerialised List<AuthCompanyAccessDto>
Active company IDauth_active_company_idstring (GUID)The single operational company GUID for this session

State properties

Three computed properties expose the session validity to the rest of the application without requiring callers to inspect raw storage values.
public bool IsAuthenticated =>
    !string.IsNullOrWhiteSpace(Token);

public bool HasAmbiguousCompanyContext =>
    Companies.Count > 1;

public bool HasValidOperationalContext =>
    IsAuthenticated && ActiveCompanyId.HasValue && !HasAmbiguousCompanyContext;
PropertyReturns true when
IsAuthenticatedToken is a non-empty string
HasAmbiguousCompanyContextThe session contains more than one company entry
HasValidOperationalContextToken is present, an active company is set, and there is no ambiguity
Never rely on visual filtering alone to enforce company separation. A session that happens to contain multiple companies must be blocked at sign-in — HasAmbiguousCompanyContext must never be true in a running session. The SessionService.SignInAsync method enforces this at write-time, but any code that checks HasValidOperationalContext before performing a warehouse operation provides a secondary defence layer against silent cross-company data access.

InitializeAsync — boot-time session restore

Called once during app startup, InitializeAsync reads every storage key and reconstructs the in-memory session state. If the token is missing, the company list is empty, or the session contains more than one company, all fields are cleared from both SecureStorage and Preferences to prevent a stale ambiguous session from surviving a restart.
public async Task InitializeAsync()
{
    Token    = await ReadAsync(TokenKey)    ?? string.Empty;
    Email    = await ReadAsync(EmailKey)    ?? string.Empty;
    FullName = await ReadAsync(FullNameKey) ?? string.Empty;

    var companiesJson = await ReadAsync(CompaniesKey);
    // ... deserialise companies list ...

    var activeCompanyId = await ReadAsync(ActiveCompanyIdKey);
    ActiveCompanyId = Guid.TryParse(activeCompanyId, out var companyId)
        ? companyId
        : Companies.FirstOrDefault()?.CompanyId;

    if (string.IsNullOrWhiteSpace(Token) || Companies.Count == 0 || HasAmbiguousCompanyContext)
    {
        // Clear all fields and storage — session is not usable
        Token           = string.Empty;
        Email           = string.Empty;
        FullName        = string.Empty;
        Companies       = Array.Empty<AuthCompanyAccessDto>();
        ActiveCompanyId = null;

        Remove(TokenKey);
        Remove(EmailKey);
        Remove(FullNameKey);
        Remove(CompaniesKey);
        Remove(ActiveCompanyIdKey);
    }

    SessionChanged?.Invoke();
}
After InitializeAsync completes, IsAuthenticated and HasValidOperationalContext reflect the actual state and the app router can decide whether to show the login screen or navigate directly to the home screen.

SignInAsync — writing a new session

SessionService.SignInAsync accepts the AuthSessionResponseDto returned by AuthService and enforces the single-company rule before writing anything to storage. It filters out entries where CompanyId is Guid.Empty, then checks the count.
public async Task SignInAsync(AuthSessionResponseDto session)
{
    var companies = session.Companies
        .Where(x => x.CompanyId != Guid.Empty)
        .ToList();

    if (companies.Count == 0)
        throw new InvalidOperationException(
            "El usuario no tiene una empresa operativa asignada para handheld.");

    if (companies.Count > 1)
        throw new InvalidOperationException(
            "Este usuario tiene acceso a varias empresas. " +
            "Para Dragon Guard Handheld se requiere una sola empresa operativa por usuario.");

    Token           = session.Token;
    Email           = session.Email;
    FullName        = session.FullName;
    Companies       = companies;
    ActiveCompanyId = Companies[0].CompanyId;

    await WriteAsync(TokenKey,    Token);
    await WriteAsync(EmailKey,    Email);
    await WriteAsync(FullNameKey, FullName);
    await WriteAsync(CompaniesKey, JsonSerializer.Serialize(Companies, _jsonOptions));

    if (ActiveCompanyId.HasValue)
        await WriteAsync(ActiveCompanyIdKey, ActiveCompanyId.Value.ToString());
    else
        Remove(ActiveCompanyIdKey);

    SessionChanged?.Invoke();
}
If either exception is thrown, none of the WriteAsync calls have been reached, so no partial session is persisted.

SignOutAsync — full session teardown

SignOutAsync zeroes every in-memory field and removes all five storage keys. It fires SessionChanged after clearing so that any UI components observing the event can react immediately.
public async Task SignOutAsync()
{
    Token           = string.Empty;
    Email           = string.Empty;
    FullName        = string.Empty;
    Companies       = Array.Empty<AuthCompanyAccessDto>();
    ActiveCompanyId = null;

    Remove(TokenKey);
    Remove(EmailKey);
    Remove(FullNameKey);
    Remove(CompaniesKey);
    Remove(ActiveCompanyIdKey);

    await Task.CompletedTask;
    SessionChanged?.Invoke();
}
SignOutAsync removes keys from both SecureStorage and Preferences via the shared Remove helper, ensuring no token fragment survives in either storage layer after sign-out regardless of which layer was used during sign-in.

AuthenticatedHttpMessageHandler — automatic header injection

AuthenticatedHttpMessageHandler is a DelegatingHandler registered in the DI container. Every HTTP client that needs authentication routes its requests through this handler without any call-site changes.

What the handler does

1

Guard: check IsAuthenticated

Before forwarding the request, the handler checks SessionService.IsAuthenticated. If the token is missing, it calls SignOutAsync and navigates to the login page, then throws to abort the original request.
if (!_sessionService.IsAuthenticated)
{
    await RedirectToLoginAsync();
    throw new InvalidOperationException("La sesion no es valida. Inicia sesion nuevamente.");
}
2

Inject Authorization header

The handler sets the Authorization header to Bearer <token> from the current in-memory token value.
request.Headers.Authorization =
    new AuthenticationHeaderValue("Bearer", _sessionService.Token);
3

Inject X-Company-Id header

If an active company GUID is set and the request does not already carry the header (to allow callers to override), the handler appends it.
if (_sessionService.ActiveCompanyId.HasValue &&
    !request.Headers.Contains("X-Company-Id"))
{
    request.Headers.Add("X-Company-Id",
        _sessionService.ActiveCompanyId.Value.ToString());
}
The X-Company-Id value is always the active company’s GUID from SessionService. It is never hardcoded and never derived from a local cache — any call to SetActiveCompanyAsync or SignOutAsync immediately affects the next outgoing request.
4

Handle 401 / 403 responses

After the inner handler returns, the handler inspects the status code. A 401 Unauthorized or 403 Forbidden response triggers the same redirect-to-login flow as a missing token: SignOutAsync is called and the app navigates to the login screen.
if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{
    await RedirectToLoginAsync();
}

Request headers produced

Every protected API call carries these two headers:
Authorization: Bearer eyJhbGci...
X-Company-Id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
The server uses X-Company-Id to scope all data access, audit logging, and permission checks to the operator’s single operational tenant. Requests that omit this header or carry an incorrect GUID will be rejected by the backend, which is an additional safeguard on top of the JWT-level tenant claim.

Protected navigation

NavigationService checks SessionService.IsAuthenticated before pushing any protected page onto the navigation stack. If the token is absent at navigation-time — for example, because SignOutAsync was called mid-session by the HTTP handler — the push is blocked and the app redirects to login instead. After a sign-out triggered by a 401/403 API response, AuthenticatedHttpMessageHandler calls app.ShowLoginAsync on the main thread. This replaces the navigation root with the login page, which means Android’s hardware back button cannot navigate back into protected screens — the back stack no longer contains them.
private async Task RedirectToLoginAsync()
{
    await _sessionService.SignOutAsync();

    if (Application.Current is App app)
    {
        await MainThread.InvokeOnMainThreadAsync(app.ShowLoginAsync);
    }
}

SessionChanged event

Every mutating operation (InitializeAsync, SignInAsync, SignOutAsync, SetActiveCompanyAsync) fires the SessionChanged event after completing. ViewModels and services that need to react to auth state changes subscribe to this event rather than polling properties.
public event Action? SessionChanged;
Subscribers are responsible for unsubscribing when they are disposed to avoid memory leaks, particularly in long-lived singleton services.

Build docs developers (and LLMs) love