- Session: сохранение/восстановление сессии между запусками (JWT exp-проверка) - LoadPrice: страница 20000 вместо 500 — прайс грузится одним запросом - ApiClient: повтор запроса на транзиентных сбоях TLS (поиск/загрузка) - download-страница: реальный размер, macOS помечен «скоро» Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.5 KiB
C#
71 lines
2.5 KiB
C#
using System;
|
||
using System.Text.Json;
|
||
|
||
namespace Elfisa.Core;
|
||
|
||
/// <summary>Текущая сессия пользователя (токен, роль, логин). Сохраняется между запусками.</summary>
|
||
public sealed class Session
|
||
{
|
||
public static Session Current { get; } = new();
|
||
|
||
public string? Token { get; set; }
|
||
public string? Role { get; set; }
|
||
public string? Username { get; set; }
|
||
|
||
public bool IsAuthenticated => !string.IsNullOrWhiteSpace(Token);
|
||
|
||
/// <summary>Сохранить сессию в настройки (чтобы не логиниться каждый раз).</summary>
|
||
public void Save()
|
||
{
|
||
SettingsStore.Current.Token = Token;
|
||
SettingsStore.Current.Role = Role;
|
||
SettingsStore.Current.Username = Username;
|
||
SettingsStore.Save();
|
||
}
|
||
|
||
/// <summary>Восстановить сессию из настроек, если токен есть и не истёк.</summary>
|
||
public static bool TryRestore()
|
||
{
|
||
var s = SettingsStore.Current;
|
||
if (string.IsNullOrWhiteSpace(s.Token) || IsJwtExpired(s.Token))
|
||
return false;
|
||
Current.Token = s.Token;
|
||
Current.Role = s.Role;
|
||
Current.Username = s.Username;
|
||
return true;
|
||
}
|
||
|
||
public void Clear()
|
||
{
|
||
Token = null;
|
||
Role = null;
|
||
Username = null;
|
||
SettingsStore.Current.Token = null;
|
||
SettingsStore.Current.Role = null;
|
||
SettingsStore.Current.Username = null;
|
||
SettingsStore.Save();
|
||
}
|
||
|
||
/// <summary>Проверяет, истёк ли JWT (по claim exp), с запасом в 1 минуту.</summary>
|
||
private static bool IsJwtExpired(string token)
|
||
{
|
||
try
|
||
{
|
||
var parts = token.Split('.');
|
||
if (parts.Length != 3) return true;
|
||
var payload = parts[1].Replace('-', '+').Replace('_', '/');
|
||
switch (payload.Length % 4)
|
||
{
|
||
case 2: payload += "=="; break;
|
||
case 3: payload += "="; break;
|
||
}
|
||
var json = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payload));
|
||
using var doc = JsonDocument.Parse(json);
|
||
if (doc.RootElement.TryGetProperty("exp", out var exp) && exp.TryGetInt64(out var expUnix))
|
||
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() >= expUnix - 60;
|
||
return false; // нет exp — считаем валидным
|
||
}
|
||
catch { return true; }
|
||
}
|
||
}
|