Add admin access and landing dashboard
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
internal static class AccessPasswordSettingsWriter
|
||||
{
|
||||
private static readonly object FileLock = new();
|
||||
|
||||
public static string HashPassword(string password)
|
||||
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password)));
|
||||
|
||||
public static void SavePasswordHash(string contentRootPath, string sectionName, string passwordHash)
|
||||
{
|
||||
var path = Path.Combine(contentRootPath, "appsettings.json");
|
||||
|
||||
lock (FileLock)
|
||||
{
|
||||
var json = File.Exists(path)
|
||||
? File.ReadAllText(path, Encoding.UTF8)
|
||||
: "{}";
|
||||
|
||||
var root = JsonNode.Parse(json)?.AsObject() ?? new JsonObject();
|
||||
var section = root[sectionName] as JsonObject;
|
||||
if (section is null)
|
||||
{
|
||||
section = new JsonObject();
|
||||
root[sectionName] = section;
|
||||
}
|
||||
|
||||
section["PasswordHash"] = passwordHash;
|
||||
section["Password"] = string.Empty;
|
||||
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
File.WriteAllText(path, root.ToJsonString(options), new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IAccessSessionTracker
|
||||
{
|
||||
IReadOnlyList<AccessSessionSnapshot> GetActiveSessions();
|
||||
void Register(string sessionId, string area, string username, string? remoteAddress);
|
||||
void Touch(string sessionId);
|
||||
void Unregister(string sessionId);
|
||||
}
|
||||
|
||||
public sealed class AccessSessionTracker : IAccessSessionTracker
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, AccessSessionSnapshot> _sessions = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IReadOnlyList<AccessSessionSnapshot> GetActiveSessions()
|
||||
=> _sessions.Values
|
||||
.OrderByDescending(session => session.LastSeenAt)
|
||||
.ToList();
|
||||
|
||||
public void Register(string sessionId, string area, string username, string? remoteAddress)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
_sessions[sessionId] = new AccessSessionSnapshot(
|
||||
sessionId,
|
||||
area,
|
||||
username,
|
||||
string.IsNullOrWhiteSpace(remoteAddress) ? "unbekannt" : remoteAddress,
|
||||
now,
|
||||
now);
|
||||
}
|
||||
|
||||
public void Touch(string sessionId)
|
||||
{
|
||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||
return;
|
||||
|
||||
_sessions[sessionId] = session with { LastSeenAt = DateTimeOffset.Now };
|
||||
}
|
||||
|
||||
public void Unregister(string sessionId)
|
||||
{
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record AccessSessionSnapshot(
|
||||
string SessionId,
|
||||
string Area,
|
||||
string Username,
|
||||
string RemoteAddress,
|
||||
DateTimeOffset StartedAt,
|
||||
DateTimeOffset LastSeenAt);
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TrafagSalesExporter.Security;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IAdminAccessService
|
||||
{
|
||||
bool IsEnabled { get; }
|
||||
bool IsConfigured { get; }
|
||||
bool IsUnlocked { get; }
|
||||
bool TryUnlock(string username, string password);
|
||||
bool TryChangePassword(string username, string currentPassword, string newPassword);
|
||||
void Lock();
|
||||
}
|
||||
|
||||
public sealed class AdminAccessService : IAdminAccessService
|
||||
{
|
||||
private readonly AdminAccessOptions _options;
|
||||
private readonly IHostEnvironment _environment;
|
||||
|
||||
public AdminAccessService(IOptions<AdminAccessOptions> options, IHostEnvironment environment)
|
||||
{
|
||||
_options = options.Value;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _options.Enabled;
|
||||
|
||||
public bool IsConfigured =>
|
||||
!IsEnabled ||
|
||||
!string.IsNullOrWhiteSpace(_options.Username) &&
|
||||
(!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password));
|
||||
|
||||
public bool IsUnlocked { get; private set; }
|
||||
|
||||
public bool TryUnlock(string username, string password)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
IsUnlocked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsConfigured ||
|
||||
string.IsNullOrWhiteSpace(username) ||
|
||||
string.IsNullOrEmpty(password) ||
|
||||
!FixedEquals(username.Trim(), _options.Username.Trim()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var valid = !string.IsNullOrWhiteSpace(_options.PasswordHash)
|
||||
? VerifyPasswordHash(password, _options.PasswordHash)
|
||||
: FixedEquals(password, _options.Password);
|
||||
|
||||
IsUnlocked = valid;
|
||||
return valid;
|
||||
}
|
||||
|
||||
public bool TryChangePassword(string username, string currentPassword, string newPassword)
|
||||
{
|
||||
if (!IsEnabled ||
|
||||
!IsConfigured ||
|
||||
string.IsNullOrWhiteSpace(newPassword) ||
|
||||
newPassword.Length < 8 ||
|
||||
!TryUnlock(username, currentPassword))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var passwordHash = AccessPasswordSettingsWriter.HashPassword(newPassword);
|
||||
AccessPasswordSettingsWriter.SavePasswordHash(_environment.ContentRootPath, AdminAccessOptions.SectionName, passwordHash);
|
||||
_options.PasswordHash = passwordHash;
|
||||
_options.Password = string.Empty;
|
||||
IsUnlocked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Lock() => IsUnlocked = false;
|
||||
|
||||
private static bool VerifyPasswordHash(string password, string configuredHash)
|
||||
{
|
||||
var passwordHash = AccessPasswordSettingsWriter.HashPassword(password);
|
||||
return FixedEquals(passwordHash, configuredHash.Trim());
|
||||
}
|
||||
|
||||
private static bool FixedEquals(string left, string right)
|
||||
{
|
||||
var leftBytes = Encoding.UTF8.GetBytes(left);
|
||||
var rightBytes = Encoding.UTF8.GetBytes(right);
|
||||
return leftBytes.Length == rightBytes.Length &&
|
||||
CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes);
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,28 @@ public interface IFinanceCockpitAccessService
|
||||
bool IsConfigured { get; }
|
||||
bool IsUnlocked { get; }
|
||||
bool TryUnlock(string username, string password);
|
||||
bool TryChangePassword(string username, string currentPassword, string newPassword);
|
||||
void Lock();
|
||||
}
|
||||
|
||||
public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService
|
||||
public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService, IDisposable
|
||||
{
|
||||
private readonly FinanceCockpitAccessOptions _options;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IAccessSessionTracker _sessionTracker;
|
||||
private readonly string _sessionId = Guid.NewGuid().ToString("N");
|
||||
|
||||
public FinanceCockpitAccessService(IOptions<FinanceCockpitAccessOptions> options)
|
||||
public FinanceCockpitAccessService(
|
||||
IOptions<FinanceCockpitAccessOptions> options,
|
||||
IHostEnvironment environment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IAccessSessionTracker sessionTracker)
|
||||
{
|
||||
_options = options.Value;
|
||||
_environment = environment;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_sessionTracker = sessionTracker;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _options.Enabled;
|
||||
@@ -53,14 +65,48 @@ public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService
|
||||
: FixedEquals(password, _options.Password);
|
||||
|
||||
IsUnlocked = valid;
|
||||
if (valid)
|
||||
_sessionTracker.Register(_sessionId, "Finance Cockpit", username.Trim(), GetRemoteAddress());
|
||||
return valid;
|
||||
}
|
||||
|
||||
public void Lock() => IsUnlocked = false;
|
||||
public void Lock()
|
||||
{
|
||||
IsUnlocked = false;
|
||||
_sessionTracker.Unregister(_sessionId);
|
||||
}
|
||||
|
||||
public bool TryChangePassword(string username, string currentPassword, string newPassword)
|
||||
{
|
||||
if (!IsEnabled ||
|
||||
!IsConfigured ||
|
||||
string.IsNullOrWhiteSpace(newPassword) ||
|
||||
newPassword.Length < 8 ||
|
||||
!TryUnlock(username, currentPassword))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var passwordHash = AccessPasswordSettingsWriter.HashPassword(newPassword);
|
||||
AccessPasswordSettingsWriter.SavePasswordHash(_environment.ContentRootPath, FinanceCockpitAccessOptions.SectionName, passwordHash);
|
||||
_options.PasswordHash = passwordHash;
|
||||
_options.Password = string.Empty;
|
||||
IsUnlocked = true;
|
||||
_sessionTracker.Register(_sessionId, "Finance Cockpit", username.Trim(), GetRemoteAddress());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sessionTracker.Unregister(_sessionId);
|
||||
}
|
||||
|
||||
private string? GetRemoteAddress()
|
||||
=> _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
private static bool VerifyPasswordHash(string password, string configuredHash)
|
||||
{
|
||||
var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password)));
|
||||
var passwordHash = AccessPasswordSettingsWriter.HashPassword(password);
|
||||
return FixedEquals(passwordHash, configuredHash.Trim());
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,28 @@ public interface IHrKpiAccessService
|
||||
bool IsConfigured { get; }
|
||||
bool IsUnlocked { get; }
|
||||
bool TryUnlock(string username, string password);
|
||||
bool TryChangePassword(string username, string currentPassword, string newPassword);
|
||||
void Lock();
|
||||
}
|
||||
|
||||
public sealed class HrKpiAccessService : IHrKpiAccessService
|
||||
public sealed class HrKpiAccessService : IHrKpiAccessService, IDisposable
|
||||
{
|
||||
private readonly HrKpiAccessOptions _options;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IAccessSessionTracker _sessionTracker;
|
||||
private readonly string _sessionId = Guid.NewGuid().ToString("N");
|
||||
|
||||
public HrKpiAccessService(IOptions<HrKpiAccessOptions> options)
|
||||
public HrKpiAccessService(
|
||||
IOptions<HrKpiAccessOptions> options,
|
||||
IHostEnvironment environment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IAccessSessionTracker sessionTracker)
|
||||
{
|
||||
_options = options.Value;
|
||||
_environment = environment;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_sessionTracker = sessionTracker;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _options.Enabled;
|
||||
@@ -53,14 +65,48 @@ public sealed class HrKpiAccessService : IHrKpiAccessService
|
||||
: FixedEquals(password, _options.Password);
|
||||
|
||||
IsUnlocked = valid;
|
||||
if (valid)
|
||||
_sessionTracker.Register(_sessionId, "HR KPI", username.Trim(), GetRemoteAddress());
|
||||
return valid;
|
||||
}
|
||||
|
||||
public void Lock() => IsUnlocked = false;
|
||||
public void Lock()
|
||||
{
|
||||
IsUnlocked = false;
|
||||
_sessionTracker.Unregister(_sessionId);
|
||||
}
|
||||
|
||||
public bool TryChangePassword(string username, string currentPassword, string newPassword)
|
||||
{
|
||||
if (!IsEnabled ||
|
||||
!IsConfigured ||
|
||||
string.IsNullOrWhiteSpace(newPassword) ||
|
||||
newPassword.Length < 8 ||
|
||||
!TryUnlock(username, currentPassword))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var passwordHash = AccessPasswordSettingsWriter.HashPassword(newPassword);
|
||||
AccessPasswordSettingsWriter.SavePasswordHash(_environment.ContentRootPath, HrKpiAccessOptions.SectionName, passwordHash);
|
||||
_options.PasswordHash = passwordHash;
|
||||
_options.Password = string.Empty;
|
||||
IsUnlocked = true;
|
||||
_sessionTracker.Register(_sessionId, "HR KPI", username.Trim(), GetRemoteAddress());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sessionTracker.Unregister(_sessionId);
|
||||
}
|
||||
|
||||
private string? GetRemoteAddress()
|
||||
=> _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
private static bool VerifyPasswordHash(string password, string configuredHash)
|
||||
{
|
||||
var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password)));
|
||||
var passwordHash = AccessPasswordSettingsWriter.HashPassword(password);
|
||||
return FixedEquals(passwordHash, configuredHash.Trim());
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ public sealed class UiTextService : IUiTextService
|
||||
["es"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Trafag Finance/Sales Management Cockpit"] = "Trafag Cockpit de finanzas y ventas",
|
||||
["Willkommen im Trafag Analyse Dashboard"] = "Bienvenido al panel analítico de Trafag",
|
||||
["Finance Cockpit"] = "Cockpit financiero",
|
||||
["Finance Cockpit ist geschuetzt. Bitte separat anmelden."] = "El cockpit financiero está protegido. Inicie sesión por separado.",
|
||||
["Finance-Cockpit-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in FinanceCockpitAccess konfigurieren."] = "El acceso al cockpit financiero aún no está configurado. Configure Username y PasswordHash en FinanceCockpitAccess.",
|
||||
@@ -40,6 +41,13 @@ public sealed class UiTextService : IUiTextService
|
||||
["Finance Regeln"] = "Reglas financieras",
|
||||
["Settings"] = "Configuración",
|
||||
["Logs"] = "Registros",
|
||||
["Aktive Logins"] = "Inicios de sesión activos",
|
||||
["Admin Bereich"] = "Área de administración",
|
||||
["Adminbereich ist geschützt. Bitte anmelden."] = "El área de administración está protegida. Inicie sesión.",
|
||||
["Admin-Zugang ist noch nicht konfiguriert."] = "El acceso de administrador aún no está configurado.",
|
||||
["Admin entsperren"] = "Desbloquear administración",
|
||||
["Admin-Anmeldung fehlgeschlagen."] = "Error al iniciar sesión como administrador.",
|
||||
["Admin sperren"] = "Bloquear administración",
|
||||
["Finance sperren"] = "Bloquear finanzas",
|
||||
["HR KPI (Login)"] = "KPI RR. HH. (login)",
|
||||
["HR Dashboard"] = "Panel HR",
|
||||
@@ -50,6 +58,24 @@ public sealed class UiTextService : IUiTextService
|
||||
["HR-KPI-Anmeldung fehlgeschlagen."] = "Error al iniciar sesión en HR KPI.",
|
||||
["Name"] = "Nombre",
|
||||
["Passwort"] = "Contraseña",
|
||||
["Passwort ändern"] = "Cambiar contraseña",
|
||||
["Aktuelles Passwort"] = "Contraseña actual",
|
||||
["Neues Passwort"] = "Nueva contraseña",
|
||||
["Mindestens 8 Zeichen."] = "Al menos 8 caracteres.",
|
||||
["Neues Passwort wiederholen"] = "Repetir nueva contraseña",
|
||||
["Passwort speichern"] = "Guardar contraseña",
|
||||
["Das neue Passwort muss mindestens 8 Zeichen lang sein."] = "La nueva contraseña debe tener al menos 8 caracteres.",
|
||||
["Die neuen Passwörter stimmen nicht überein."] = "Las nuevas contraseñas no coinciden.",
|
||||
["Passwort konnte nicht geändert werden. Name oder aktuelles Passwort prüfen."] = "No se pudo cambiar la contraseña. Compruebe el nombre o la contraseña actual.",
|
||||
["Passwort wurde geändert."] = "La contraseña se ha cambiado.",
|
||||
["HR-/Finance-Cockpit Sessions"] = "Sesiones de cockpit HR/Finance",
|
||||
["Gezählt werden App-interne Entsperrungen seit dem letzten App-Start."] = "Se cuentan los desbloqueos internos de la app desde el último inicio.",
|
||||
["Bereich"] = "Área",
|
||||
["IP-Adresse"] = "Dirección IP",
|
||||
["Entsperrt seit"] = "Desbloqueado desde",
|
||||
["Zuletzt gesehen"] = "Visto por última vez",
|
||||
["Keine aktiven HR-/Finance-Logins erfasst."] = "No hay inicios de sesión HR/Finance activos registrados.",
|
||||
["Hinweis: HR und Finance verwenden gemeinsame App-Logins. Diese Seite zeigt daher den verwendeten Login-Namen und die Session, nicht zwingend die echte Person."] = "Nota: HR y Finance usan logins compartidos de la app. Esta página muestra el nombre de login usado y la sesión, no necesariamente la persona real.",
|
||||
["Finance Cockpit entsperren"] = "Desbloquear cockpit financiero",
|
||||
["Finance-Jahr"] = "Año financiero",
|
||||
["Finance Summary laden"] = "Cargar resumen financiero",
|
||||
@@ -230,6 +256,7 @@ public sealed class UiTextService : IUiTextService
|
||||
["it"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Trafag Finance/Sales Management Cockpit"] = "Cockpit Trafag finanza e vendite",
|
||||
["Willkommen im Trafag Analyse Dashboard"] = "Benvenuto nel dashboard analitico Trafag",
|
||||
["Finance Cockpit"] = "Cockpit finance",
|
||||
["Finance Cockpit ist geschuetzt. Bitte separat anmelden."] = "Il cockpit finance è protetto. Effettuare un accesso separato.",
|
||||
["Finance-Cockpit-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in FinanceCockpitAccess konfigurieren."] = "L'accesso al cockpit finance non è ancora configurato. Configurare Username e PasswordHash in FinanceCockpitAccess.",
|
||||
@@ -244,6 +271,13 @@ public sealed class UiTextService : IUiTextService
|
||||
["Finance Regeln"] = "Regole finance",
|
||||
["Settings"] = "Impostazioni",
|
||||
["Logs"] = "Log",
|
||||
["Aktive Logins"] = "Login attivi",
|
||||
["Admin Bereich"] = "Area admin",
|
||||
["Adminbereich ist geschützt. Bitte anmelden."] = "L'area admin è protetta. Effettuare l'accesso.",
|
||||
["Admin-Zugang ist noch nicht konfiguriert."] = "L'accesso admin non è ancora configurato.",
|
||||
["Admin entsperren"] = "Sblocca admin",
|
||||
["Admin-Anmeldung fehlgeschlagen."] = "Accesso admin non riuscito.",
|
||||
["Admin sperren"] = "Blocca admin",
|
||||
["Finance sperren"] = "Blocca finance",
|
||||
["HR KPI (Login)"] = "KPI HR (login)",
|
||||
["HR Dashboard"] = "Dashboard HR",
|
||||
@@ -254,6 +288,24 @@ public sealed class UiTextService : IUiTextService
|
||||
["HR-KPI-Anmeldung fehlgeschlagen."] = "Accesso a HR KPI non riuscito.",
|
||||
["Name"] = "Nome",
|
||||
["Passwort"] = "Password",
|
||||
["Passwort ändern"] = "Cambia password",
|
||||
["Aktuelles Passwort"] = "Password attuale",
|
||||
["Neues Passwort"] = "Nuova password",
|
||||
["Mindestens 8 Zeichen."] = "Almeno 8 caratteri.",
|
||||
["Neues Passwort wiederholen"] = "Ripeti nuova password",
|
||||
["Passwort speichern"] = "Salva password",
|
||||
["Das neue Passwort muss mindestens 8 Zeichen lang sein."] = "La nuova password deve contenere almeno 8 caratteri.",
|
||||
["Die neuen Passwörter stimmen nicht überein."] = "Le nuove password non corrispondono.",
|
||||
["Passwort konnte nicht geändert werden. Name oder aktuelles Passwort prüfen."] = "Impossibile modificare la password. Controllare nome o password attuale.",
|
||||
["Passwort wurde geändert."] = "La password è stata modificata.",
|
||||
["HR-/Finance-Cockpit Sessions"] = "Sessioni cockpit HR/Finance",
|
||||
["Gezählt werden App-interne Entsperrungen seit dem letzten App-Start."] = "Vengono contati gli sblocchi interni dell'app dall'ultimo avvio.",
|
||||
["Bereich"] = "Area",
|
||||
["IP-Adresse"] = "Indirizzo IP",
|
||||
["Entsperrt seit"] = "Sbloccato da",
|
||||
["Zuletzt gesehen"] = "Ultima attività",
|
||||
["Keine aktiven HR-/Finance-Logins erfasst."] = "Nessun login HR/Finance attivo registrato.",
|
||||
["Hinweis: HR und Finance verwenden gemeinsame App-Logins. Diese Seite zeigt daher den verwendeten Login-Namen und die Session, nicht zwingend die echte Person."] = "Nota: HR e Finance usano login condivisi dell'app. Questa pagina mostra quindi il nome login usato e la sessione, non necessariamente la persona reale.",
|
||||
["Finance Cockpit entsperren"] = "Sblocca cockpit finance",
|
||||
["Finance-Jahr"] = "Anno finance",
|
||||
["Finance Summary laden"] = "Carica riepilogo finance",
|
||||
@@ -434,6 +486,7 @@ public sealed class UiTextService : IUiTextService
|
||||
["hi"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Trafag Finance/Sales Management Cockpit"] = "Trafag वित्त और बिक्री प्रबंधन कॉकपिट",
|
||||
["Willkommen im Trafag Analyse Dashboard"] = "Trafag विश्लेषण डैशबोर्ड में आपका स्वागत है",
|
||||
["Finance Cockpit"] = "वित्त कॉकपिट",
|
||||
["Finance Cockpit ist geschuetzt. Bitte separat anmelden."] = "वित्त कॉकपिट सुरक्षित है. कृपया अलग से साइन इन करें.",
|
||||
["Finance-Cockpit-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in FinanceCockpitAccess konfigurieren."] = "वित्त कॉकपिट एक्सेस अभी कॉन्फ़िगर नहीं है. कृपया FinanceCockpitAccess में Username और PasswordHash सेट करें.",
|
||||
@@ -448,6 +501,13 @@ public sealed class UiTextService : IUiTextService
|
||||
["Finance Regeln"] = "वित्त नियम",
|
||||
["Settings"] = "सेटिंग्स",
|
||||
["Logs"] = "लॉग",
|
||||
["Aktive Logins"] = "सक्रिय लॉगिन",
|
||||
["Admin Bereich"] = "Admin क्षेत्र",
|
||||
["Adminbereich ist geschützt. Bitte anmelden."] = "Admin क्षेत्र सुरक्षित है. कृपया साइन इन करें.",
|
||||
["Admin-Zugang ist noch nicht konfiguriert."] = "Admin एक्सेस अभी कॉन्फ़िगर नहीं है.",
|
||||
["Admin entsperren"] = "Admin अनलॉक करें",
|
||||
["Admin-Anmeldung fehlgeschlagen."] = "Admin साइन-इन विफल.",
|
||||
["Admin sperren"] = "Admin लॉक करें",
|
||||
["Finance sperren"] = "वित्त लॉक करें",
|
||||
["HR KPI (Login)"] = "HR KPI (लॉगिन)",
|
||||
["HR Dashboard"] = "HR डैशबोर्ड",
|
||||
@@ -458,6 +518,24 @@ public sealed class UiTextService : IUiTextService
|
||||
["HR-KPI-Anmeldung fehlgeschlagen."] = "HR KPI साइन-इन विफल.",
|
||||
["Name"] = "नाम",
|
||||
["Passwort"] = "पासवर्ड",
|
||||
["Passwort ändern"] = "पासवर्ड बदलें",
|
||||
["Aktuelles Passwort"] = "वर्तमान पासवर्ड",
|
||||
["Neues Passwort"] = "नया पासवर्ड",
|
||||
["Mindestens 8 Zeichen."] = "कम से कम 8 अक्षर.",
|
||||
["Neues Passwort wiederholen"] = "नया पासवर्ड दोहराएं",
|
||||
["Passwort speichern"] = "पासवर्ड सहेजें",
|
||||
["Das neue Passwort muss mindestens 8 Zeichen lang sein."] = "नया पासवर्ड कम से कम 8 अक्षरों का होना चाहिए.",
|
||||
["Die neuen Passwörter stimmen nicht überein."] = "नए पासवर्ड मेल नहीं खाते.",
|
||||
["Passwort konnte nicht geändert werden. Name oder aktuelles Passwort prüfen."] = "पासवर्ड बदला नहीं जा सका. नाम या वर्तमान पासवर्ड जांचें.",
|
||||
["Passwort wurde geändert."] = "पासवर्ड बदल दिया गया.",
|
||||
["HR-/Finance-Cockpit Sessions"] = "HR/Finance cockpit sessions",
|
||||
["Gezählt werden App-interne Entsperrungen seit dem letzten App-Start."] = "ऐप के पिछले प्रारंभ के बाद से आंतरिक अनलॉक गिने जाते हैं.",
|
||||
["Bereich"] = "क्षेत्र",
|
||||
["IP-Adresse"] = "IP पता",
|
||||
["Entsperrt seit"] = "अनलॉक समय",
|
||||
["Zuletzt gesehen"] = "अंतिम बार देखा गया",
|
||||
["Keine aktiven HR-/Finance-Logins erfasst."] = "कोई सक्रिय HR/Finance लॉगिन दर्ज नहीं है.",
|
||||
["Hinweis: HR und Finance verwenden gemeinsame App-Logins. Diese Seite zeigt daher den verwendeten Login-Namen und die Session, nicht zwingend die echte Person."] = "नोट: HR और Finance साझा ऐप लॉगिन का उपयोग करते हैं. इसलिए यह पेज उपयोग किया गया लॉगिन नाम और सत्र दिखाता है, जरूरी नहीं कि वास्तविक व्यक्ति.",
|
||||
["Finance Cockpit entsperren"] = "वित्त कॉकपिट अनलॉक करें",
|
||||
["Finance-Jahr"] = "वित्त वर्ष",
|
||||
["Finance Summary laden"] = "वित्त सारांश लोड करें",
|
||||
|
||||
Reference in New Issue
Block a user