feat(terminal): add secure release updates and installer
Some checks failed
ci / verify (push) Has been cancelled
Some checks failed
ci / verify (push) Has been cancelled
This commit is contained in:
parent
d45bee960a
commit
827d321a1a
14 changed files with 694 additions and 10 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -11,3 +11,5 @@ backups/
|
||||||
restore-work/
|
restore-work/
|
||||||
apps/KronaStaffTerminal/bin/
|
apps/KronaStaffTerminal/bin/
|
||||||
apps/KronaStaffTerminal/obj/
|
apps/KronaStaffTerminal/obj/
|
||||||
|
artifacts/
|
||||||
|
.audit-build/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: Krona Staff API
|
title: Krona Staff API
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
description: Operational API for the Krona Staff foundation. HR domain endpoints are intentionally absent until owner requirements are approved.
|
description: Operational API for the Krona Staff foundation. HR domain endpoints are intentionally absent until owner requirements are approved.
|
||||||
license:
|
license:
|
||||||
name: Proprietary
|
name: Proprietary
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<Version>0.2.0</Version>
|
||||||
|
<AssemblyVersion>0.2.0.0</AssemblyVersion>
|
||||||
|
<FileVersion>0.2.0.0</FileVersion>
|
||||||
|
<InformationalVersion>0.2.0</InformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -81,8 +85,25 @@
|
||||||
|
|
||||||
<!-- Publish Properties -->
|
<!-- Publish Properties -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
<PublishReadyToRun>False</PublishReadyToRun>
|
||||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
|
||||||
<PublishTrimmed>False</PublishTrimmed>
|
<PublishTrimmed>False</PublishTrimmed>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyUnpackagedWinUiResourcesToPublish" AfterTargets="Publish">
|
||||||
|
<ItemGroup>
|
||||||
|
<_WinUiRootResource Include="$(TargetDir)*.xbf" />
|
||||||
|
<_WinUiRootResource Include="$(TargetDir)$(AssemblyName).pri" />
|
||||||
|
<_WinUiAsset Include="$(TargetDir)Assets\**\*" />
|
||||||
|
<_WinUiStyle Include="$(TargetDir)Styles\**\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Copy SourceFiles="@(_WinUiRootResource)"
|
||||||
|
DestinationFolder="$(PublishDir)"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
<Copy SourceFiles="@(_WinUiAsset)"
|
||||||
|
DestinationFiles="@(_WinUiAsset->'$(PublishDir)Assets\%(RecursiveDir)%(Filename)%(Extension)')"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
<Copy SourceFiles="@(_WinUiStyle)"
|
||||||
|
DestinationFiles="@(_WinUiStyle->'$(PublishDir)Styles\%(RecursiveDir)%(Filename)%(Extension)')"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
RowSpacing="12"
|
RowSpacing="12"
|
||||||
Background="{ThemeResource KronaAppBackgroundBrush}">
|
Background="{ThemeResource KronaAppBackgroundBrush}">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
@ -25,8 +26,21 @@
|
||||||
Title="Подключение"
|
Title="Подключение"
|
||||||
Message="Запуск терминала…"/>
|
Message="Запуск терминала…"/>
|
||||||
|
|
||||||
<TabView x:Name="MainTabs"
|
<InfoBar x:Name="UpdateBar"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
Margin="0,0,0,4"
|
||||||
|
IsOpen="False"
|
||||||
|
IsClosable="True"
|
||||||
|
Severity="Informational"
|
||||||
|
Title="Доступно обновление">
|
||||||
|
<InfoBar.ActionButton>
|
||||||
|
<Button Content="Обновить"
|
||||||
|
Click="InstallUpdate_Click"/>
|
||||||
|
</InfoBar.ActionButton>
|
||||||
|
</InfoBar>
|
||||||
|
|
||||||
|
<TabView x:Name="MainTabs"
|
||||||
|
Grid.Row="2"
|
||||||
Background="{ThemeResource KronaWorkspaceBrush}"
|
Background="{ThemeResource KronaWorkspaceBrush}"
|
||||||
IsAddTabButtonVisible="False"
|
IsAddTabButtonVisible="False"
|
||||||
SelectionChanged="MainTabs_SelectionChanged">
|
SelectionChanged="MainTabs_SelectionChanged">
|
||||||
|
|
@ -205,6 +219,26 @@
|
||||||
<ToggleSwitch x:Name="MaxEnabledSwitch" Header="MAX" OffContent="Выключен" OnContent="Включён"/>
|
<ToggleSwitch x:Name="MaxEnabledSwitch" Header="MAX" OffContent="Выключен" OnContent="Включён"/>
|
||||||
<PasswordBox x:Name="MaxTokenBox" Header="Токен MAX-бота" PasswordRevealMode="Peek"/>
|
<PasswordBox x:Name="MaxTokenBox" Header="Токен MAX-бота" PasswordRevealMode="Peek"/>
|
||||||
<TextBox x:Name="MaxChatBox" Header="Chat ID MAX"/>
|
<TextBox x:Name="MaxChatBox" Header="Chat ID MAX"/>
|
||||||
|
<Border Height="1" Background="{ThemeResource DividerStrokeColorDefaultBrush}"/>
|
||||||
|
<TextBlock Text="Обновления программы" Foreground="{ThemeResource KronaTextPrimaryBrush}" Style="{StaticResource SubtitleTextBlockStyle}"/>
|
||||||
|
<TextBlock x:Name="CurrentVersionText" Opacity="0.75"/>
|
||||||
|
<ToggleSwitch x:Name="UpdateChecksSwitch"
|
||||||
|
Header="Автоматическая проверка"
|
||||||
|
OffContent="Выключена"
|
||||||
|
OnContent="Проверять при запуске и каждые 6 часов"/>
|
||||||
|
<TextBox x:Name="UpdateFeedBox"
|
||||||
|
Header="Адрес канала обновлений"
|
||||||
|
IsReadOnly="True"
|
||||||
|
PlaceholderText="HTTPS-адрес последнего релиза"/>
|
||||||
|
<PasswordBox x:Name="UpdateTokenBox"
|
||||||
|
Header="Токен чтения приватного репозитория"
|
||||||
|
PasswordRevealMode="Peek"/>
|
||||||
|
<TextBlock Text="Токен нужен только пока обновления берутся из приватного Forgejo. Позже этот канал можно заменить Бизнес Платформой без изменения установщика."
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Opacity="0.7"/>
|
||||||
|
<Button Content="Проверить обновления"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Click="CheckForUpdates_Click"/>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
<Button Content="Сохранить настройки"
|
<Button Content="Сохранить настройки"
|
||||||
Background="{ThemeResource KronaPrimaryBrush}"
|
Background="{ThemeResource KronaPrimaryBrush}"
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,16 @@ public sealed partial class MainPage : Page
|
||||||
private readonly TerminalStore _store = new();
|
private readonly TerminalStore _store = new();
|
||||||
private readonly NotificationService _notifications = new();
|
private readonly NotificationService _notifications = new();
|
||||||
private readonly FingerprintScanner _scanner = new();
|
private readonly FingerprintScanner _scanner = new();
|
||||||
|
private readonly IUpdateSource _updateSource = new ForgejoUpdateSource();
|
||||||
private TerminalState _state = new();
|
private TerminalState _state = new();
|
||||||
private bool _loaded;
|
private bool _loaded;
|
||||||
private TaskCompletionSource<bool>? _adminAuthCompletion;
|
private TaskCompletionSource<bool>? _adminAuthCompletion;
|
||||||
private ContentDialog? _adminAuthDialog;
|
private ContentDialog? _adminAuthDialog;
|
||||||
private readonly DispatcherQueueTimer _adminSessionTimer;
|
private readonly DispatcherQueueTimer _adminSessionTimer;
|
||||||
|
private readonly DispatcherQueueTimer _updateTimer;
|
||||||
|
private readonly CancellationTokenSource _updateCancellation = new();
|
||||||
|
private AvailableUpdate? _availableUpdate;
|
||||||
|
private bool _updateCheckInProgress;
|
||||||
private bool _adminSessionActive;
|
private bool _adminSessionActive;
|
||||||
private bool _adminNavigationInProgress;
|
private bool _adminNavigationInProgress;
|
||||||
private bool _stateLoadFailed;
|
private bool _stateLoadFailed;
|
||||||
|
|
@ -31,7 +36,15 @@ public sealed partial class MainPage : Page
|
||||||
|
|
||||||
public MainPage()
|
public MainPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
try
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
CrashLogService.Write(exception);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
_scanner.StatusChanged += message => Enqueue(() => SetStatus(message));
|
_scanner.StatusChanged += message => Enqueue(() => SetStatus(message));
|
||||||
_scanner.RegistrationProgress += count => Enqueue(() => SetStatus($"Регистрация отпечатка: {count}/3"));
|
_scanner.RegistrationProgress += count => Enqueue(() => SetStatus($"Регистрация отпечатка: {count}/3"));
|
||||||
_scanner.RegistrationCompleted += (id, template) => EnqueueTask(() => CompleteRegistrationAsync(id, template));
|
_scanner.RegistrationCompleted += (id, template) => EnqueueTask(() => CompleteRegistrationAsync(id, template));
|
||||||
|
|
@ -49,6 +62,10 @@ public sealed partial class MainPage : Page
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_adminSessionTimer.Start();
|
_adminSessionTimer.Start();
|
||||||
|
_updateTimer = DispatcherQueue.CreateTimer();
|
||||||
|
_updateTimer.Interval = TimeSpan.FromHours(6);
|
||||||
|
_updateTimer.IsRepeating = true;
|
||||||
|
_updateTimer.Tick += (_, _) => EnqueueTask(() => CheckForUpdatesAsync(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Page_Loaded(object sender, RoutedEventArgs e)
|
private async void Page_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
|
@ -72,17 +89,21 @@ public sealed partial class MainPage : Page
|
||||||
ApplyStartupSetting();
|
ApplyStartupSetting();
|
||||||
RefreshLists();
|
RefreshLists();
|
||||||
ConnectScanner();
|
ConnectScanner();
|
||||||
|
_updateTimer.Start();
|
||||||
MainTabs.SelectedItem = TerminalTab;
|
MainTabs.SelectedItem = TerminalTab;
|
||||||
if (_store.RecoveryNotice is not null)
|
if (_store.RecoveryNotice is not null)
|
||||||
{
|
{
|
||||||
SetStatus(_store.RecoveryNotice, InfoBarSeverity.Warning);
|
SetStatus(_store.RecoveryNotice, InfoBarSeverity.Warning);
|
||||||
}
|
}
|
||||||
|
await CheckForUpdatesAsync(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Page_Unloaded(object sender, RoutedEventArgs e)
|
private void Page_Unloaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_loaded = false;
|
_loaded = false;
|
||||||
_adminSessionTimer.Stop();
|
_adminSessionTimer.Stop();
|
||||||
|
_updateTimer.Stop();
|
||||||
|
_updateCancellation.Cancel();
|
||||||
_scanner.Dispose();
|
_scanner.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -857,6 +878,117 @@ public sealed partial class MainPage : Page
|
||||||
SetStatus("Настройки сохранены.", InfoBarSeverity.Success);
|
SetStatus("Настройки сохранены.", InfoBarSeverity.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void CheckForUpdates_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!await EnsureAdminSessionAsync("проверка обновлений")) return;
|
||||||
|
CopyUpdateSettingsFromControls();
|
||||||
|
await _store.SaveAsync(_state);
|
||||||
|
await CheckForUpdatesAsync(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckForUpdatesAsync(bool reportUpToDate)
|
||||||
|
{
|
||||||
|
if (_updateCheckInProgress ||
|
||||||
|
(!_state.Settings.UpdateChecksEnabled && !reportUpToDate) ||
|
||||||
|
_updateCancellation.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateCheckInProgress = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var update = await _updateSource.FindUpdateAsync(
|
||||||
|
_state.Settings,
|
||||||
|
_updateCancellation.Token);
|
||||||
|
_state.Settings.LastUpdateCheckUtc = DateTimeOffset.UtcNow;
|
||||||
|
if (update is null)
|
||||||
|
{
|
||||||
|
_availableUpdate = null;
|
||||||
|
UpdateBar.IsOpen = false;
|
||||||
|
if (reportUpToDate)
|
||||||
|
{
|
||||||
|
SetStatus(
|
||||||
|
$"Установлена последняя версия {ForgejoUpdateSource.CurrentVersion.ToString(3)}.",
|
||||||
|
InfoBarSeverity.Success);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_availableUpdate = update;
|
||||||
|
UpdateBar.Message = $"{update.ReleaseName}. Установка доступна администратору.";
|
||||||
|
UpdateBar.IsOpen = true;
|
||||||
|
if (reportUpToDate)
|
||||||
|
{
|
||||||
|
SetStatus($"Доступна версия {update.DisplayVersion}.", InfoBarSeverity.Informational);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (_updateCancellation.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
CrashLogService.Write(exception);
|
||||||
|
if (reportUpToDate)
|
||||||
|
{
|
||||||
|
SetStatus($"Не удалось проверить обновления: {exception.Message}", InfoBarSeverity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_updateCheckInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void InstallUpdate_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_availableUpdate is null) return;
|
||||||
|
if (!await EnsureAdminSessionAsync("установка обновления")) return;
|
||||||
|
|
||||||
|
var update = _availableUpdate;
|
||||||
|
var notes = string.IsNullOrWhiteSpace(update.ReleaseNotes)
|
||||||
|
? "После загрузки программа закроется и откроется установщик."
|
||||||
|
: update.ReleaseNotes;
|
||||||
|
var dialog = new ContentDialog
|
||||||
|
{
|
||||||
|
XamlRoot = XamlRoot,
|
||||||
|
Title = $"Установить Krona Staff {update.DisplayVersion}?",
|
||||||
|
Content = new TextBlock
|
||||||
|
{
|
||||||
|
Text = notes,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
MaxWidth = 520
|
||||||
|
},
|
||||||
|
PrimaryButtonText = "Скачать и установить",
|
||||||
|
CloseButtonText = "Позже",
|
||||||
|
DefaultButton = ContentDialogButton.Primary
|
||||||
|
};
|
||||||
|
if (await dialog.ShowAsync() != ContentDialogResult.Primary) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateBar.IsClosable = false;
|
||||||
|
UpdateBar.Message = $"Загрузка версии {update.DisplayVersion}: 0%";
|
||||||
|
var progress = new Progress<double>(value =>
|
||||||
|
UpdateBar.Message = $"Загрузка версии {update.DisplayVersion}: {value:P0}");
|
||||||
|
var installerPath = await _updateSource.DownloadAndVerifyAsync(
|
||||||
|
update,
|
||||||
|
_state.Settings,
|
||||||
|
progress,
|
||||||
|
_updateCancellation.Token);
|
||||||
|
AppendSecurityAudit("application.update", "started", $"version={update.DisplayVersion}");
|
||||||
|
await _store.SaveAsync(_state, createRecoveryPoint: true);
|
||||||
|
ForgejoUpdateSource.LaunchInstaller(installerPath);
|
||||||
|
((App)Application.Current).CompleteExit();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
CrashLogService.Write(exception);
|
||||||
|
UpdateBar.IsClosable = true;
|
||||||
|
SetStatus($"Обновление не установлено: {exception.Message}", InfoBarSeverity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void TestNotification_Click(object sender, RoutedEventArgs e)
|
private async void TestNotification_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!await EnsureAdminSessionAsync("проверка уведомлений")) return;
|
if (!await EnsureAdminSessionAsync("проверка уведомлений")) return;
|
||||||
|
|
@ -878,6 +1010,14 @@ public sealed partial class MainPage : Page
|
||||||
_state.Settings.MaxEnabled = MaxEnabledSwitch.IsOn;
|
_state.Settings.MaxEnabled = MaxEnabledSwitch.IsOn;
|
||||||
_state.Settings.MaxBotToken = MaxTokenBox.Password.Trim();
|
_state.Settings.MaxBotToken = MaxTokenBox.Password.Trim();
|
||||||
_state.Settings.MaxChatId = MaxChatBox.Text.Trim();
|
_state.Settings.MaxChatId = MaxChatBox.Text.Trim();
|
||||||
|
CopyUpdateSettingsFromControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyUpdateSettingsFromControls()
|
||||||
|
{
|
||||||
|
_state.Settings.UpdateChecksEnabled = UpdateChecksSwitch.IsOn;
|
||||||
|
_state.Settings.UpdateFeedUrl = UpdateFeedBox.Text.Trim();
|
||||||
|
_state.Settings.UpdateAccessToken = UpdateTokenBox.Password.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadSettings()
|
private void LoadSettings()
|
||||||
|
|
@ -892,6 +1032,10 @@ public sealed partial class MainPage : Page
|
||||||
MaxEnabledSwitch.IsOn = _state.Settings.MaxEnabled;
|
MaxEnabledSwitch.IsOn = _state.Settings.MaxEnabled;
|
||||||
MaxTokenBox.Password = _state.Settings.MaxBotToken;
|
MaxTokenBox.Password = _state.Settings.MaxBotToken;
|
||||||
MaxChatBox.Text = _state.Settings.MaxChatId;
|
MaxChatBox.Text = _state.Settings.MaxChatId;
|
||||||
|
UpdateChecksSwitch.IsOn = _state.Settings.UpdateChecksEnabled;
|
||||||
|
UpdateFeedBox.Text = _state.Settings.UpdateFeedUrl;
|
||||||
|
UpdateTokenBox.Password = _state.Settings.UpdateAccessToken;
|
||||||
|
CurrentVersionText.Text = $"Текущая версия: {ForgejoUpdateSource.CurrentVersion.ToString(3)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyStartupSetting()
|
private void ApplyStartupSetting()
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,10 @@ public sealed class TerminalSettings
|
||||||
public bool MaxEnabled { get; set; }
|
public bool MaxEnabled { get; set; }
|
||||||
public string MaxBotToken { get; set; } = "";
|
public string MaxBotToken { get; set; } = "";
|
||||||
public string MaxChatId { get; set; } = "";
|
public string MaxChatId { get; set; } = "";
|
||||||
|
public bool UpdateChecksEnabled { get; set; } = true;
|
||||||
|
public string UpdateFeedUrl { get; set; } = "https://git.ochenstarik.ru/api/v1/repos/ochenstarik/krona-staff/releases/latest";
|
||||||
|
public string UpdateAccessToken { get; set; } = "";
|
||||||
|
public DateTimeOffset LastUpdateCheckUtc { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TerminalState
|
public sealed class TerminalState
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
- закрытый административный раздел с входом по PIN или отпечатку администратора;
|
- закрытый административный раздел с входом по PIN или отпечатку администратора;
|
||||||
- автоматическая блокировка административного раздела через 5 минут и при сворачивании;
|
- автоматическая блокировка административного раздела через 5 минут и при сворачивании;
|
||||||
- шифрование сотрудников, токенов и шаблонов отпечатков средствами Windows DPAPI.
|
- шифрование сотрудников, токенов и шаблонов отпечатков средствами Windows DPAPI.
|
||||||
|
- проверка новых релизов Forgejo при запуске и каждые шесть часов;
|
||||||
|
- загрузка обновления только после входа администратора и обязательной проверки SHA-256.
|
||||||
|
|
||||||
Исходное изображение пальца не сохраняется. Шаблон отпечатка хранится только локально и может быть расшифрован текущей учётной записью Windows.
|
Исходное изображение пальца не сохраняется. Шаблон отпечатка хранится только локально и может быть расшифрован текущей учётной записью Windows.
|
||||||
|
|
||||||
|
|
@ -29,6 +31,16 @@
|
||||||
dotnet run --project apps/KronaStaffTerminal/KronaStaffTerminal.csproj -c Debug -p:Platform=x64 --launch-profile "KronaStaffTerminal (Unpackaged)"
|
dotnet run --project apps/KronaStaffTerminal/KronaStaffTerminal.csproj -c Debug -p:Platform=x64 --launch-profile "KronaStaffTerminal (Unpackaged)"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Для обычной установки скачайте из раздела Releases файл
|
||||||
|
`KronaStaffTerminal-Setup-<версия>.exe`. В конце установки можно открыть
|
||||||
|
[официальную страницу ZKFinger SDK для Windows](https://www.zkteco.com/en/Biometrics_Module_SDK/ZKFinger-SDK-for-Windows),
|
||||||
|
где находится драйвер совместимого сканера SLK20R.
|
||||||
|
|
||||||
|
Драйвер не включается внутрь платного продукта: EULA из полученного SDK разрешает
|
||||||
|
распространение в составе только бесплатных или некоммерческих пакетов. Для
|
||||||
|
коммерческого комплекта требуется отдельное письменное право распространения от
|
||||||
|
ZKTeco. Уже установленный драйвер продолжает работать как обычно.
|
||||||
|
|
||||||
## Первый сотрудник
|
## Первый сотрудник
|
||||||
|
|
||||||
1. Откройте вкладку «Сотрудники» и при первом входе создайте PIN длиной не менее шести символов.
|
1. Откройте вкладку «Сотрудники» и при первом входе создайте PIN длиной не менее шести символов.
|
||||||
|
|
@ -47,6 +59,29 @@ dotnet run --project apps/KronaStaffTerminal/KronaStaffTerminal.csproj -c Debug
|
||||||
|
|
||||||
После первичной настройки добавление сотрудников, регистрация и удаление отпечатков, изменение прав, уведомлений и параметров сканера доступны только в административном сеансе. Крестик скрывает окно в системный трей, сканер продолжает работать. Двойной щелчок по значку возвращает окно. Автозагрузка включается администратором в настройках.
|
После первичной настройки добавление сотрудников, регистрация и удаление отпечатков, изменение прав, уведомлений и параметров сканера доступны только в административном сеансе. Крестик скрывает окно в системный трей, сканер продолжает работать. Двойной щелчок по значку возвращает окно. Автозагрузка включается администратором в настройках.
|
||||||
|
|
||||||
|
При блокировке Windows (`Win+L`) терминал продолжает работать. При полном выходе
|
||||||
|
из учётной записи Windows система завершает все пользовательские программы, поэтому
|
||||||
|
сканирование останавливается до следующего входа. Для работы вообще без открытого
|
||||||
|
сеанса потребуется отдельная Windows-служба сканера.
|
||||||
|
|
||||||
## Уведомления
|
## Уведомления
|
||||||
|
|
||||||
На вкладке «Уведомления и сканер» включите нужный канал, укажите токен бота и идентификатор чата, сохраните настройки и нажмите «Отправить тест». При успешном распознавании сообщение содержит имя сотрудника и местное время прихода.
|
На вкладке «Уведомления и сканер» включите нужный канал, укажите токен бота и идентификатор чата, сохраните настройки и нажмите «Отправить тест». При успешном распознавании сообщение содержит имя сотрудника и местное время прихода.
|
||||||
|
|
||||||
|
## Обновления
|
||||||
|
|
||||||
|
Пока репозиторий приватный, администратор терминала указывает в настройках токен
|
||||||
|
Forgejo только с правом чтения репозитория. Токен хранится вместе с остальными
|
||||||
|
настройками в зашифрованном Windows DPAPI-файле текущего пользователя и не
|
||||||
|
встраивается в программу. Источник обновлений реализован отдельным адаптером:
|
||||||
|
впоследствии Forgejo можно заменить публичным портом Бизнес Платформы.
|
||||||
|
|
||||||
|
Для сборки установщика и двух файлов релиза:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\build-terminal-release.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт публикует x64-приложение, создаёт установщик NSIS и соседний файл
|
||||||
|
`.sha256`. В релиз нужно загружать оба файла; без контрольной суммы встроенное
|
||||||
|
обновление откажется запускать установщик.
|
||||||
|
|
|
||||||
261
apps/KronaStaffTerminal/Services/UpdateService.cs
Normal file
261
apps/KronaStaffTerminal/Services/UpdateService.cs
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using KronaStaffTerminal.Models;
|
||||||
|
|
||||||
|
namespace KronaStaffTerminal.Services;
|
||||||
|
|
||||||
|
public sealed record AvailableUpdate(
|
||||||
|
Version Version,
|
||||||
|
string DisplayVersion,
|
||||||
|
string ReleaseName,
|
||||||
|
string ReleaseNotes,
|
||||||
|
Uri ReleasePage,
|
||||||
|
Uri InstallerUrl,
|
||||||
|
Uri ChecksumUrl,
|
||||||
|
string InstallerFileName);
|
||||||
|
|
||||||
|
public interface IUpdateSource
|
||||||
|
{
|
||||||
|
Task<AvailableUpdate?> FindUpdateAsync(TerminalSettings settings, CancellationToken cancellationToken);
|
||||||
|
Task<string> DownloadAndVerifyAsync(
|
||||||
|
AvailableUpdate update,
|
||||||
|
TerminalSettings settings,
|
||||||
|
IProgress<double>? progress,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ForgejoUpdateSource : IUpdateSource
|
||||||
|
{
|
||||||
|
private const string InstallerPrefix = "KronaStaffTerminal-Setup-";
|
||||||
|
private static readonly HttpClient HttpClient = CreateHttpClient();
|
||||||
|
|
||||||
|
public async Task<AvailableUpdate?> FindUpdateAsync(
|
||||||
|
TerminalSettings settings,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(settings.UpdateFeedUrl, UriKind.Absolute, out var feedUri) ||
|
||||||
|
feedUri.Scheme != Uri.UriSchemeHttps)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Адрес обновлений должен быть защищённым HTTPS-адресом Forgejo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var request = CreateRequest(HttpMethod.Get, feedUri, settings.UpdateAccessToken);
|
||||||
|
using var response = await HttpClient.SendAsync(
|
||||||
|
request,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead,
|
||||||
|
cancellationToken);
|
||||||
|
if (response.StatusCode is HttpStatusCode.Unauthorized or
|
||||||
|
HttpStatusCode.Forbidden or
|
||||||
|
HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Нет доступа к обновлениям. Укажите токен чтения приватного репозитория в настройках.");
|
||||||
|
}
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
var release = await JsonSerializer.DeserializeAsync<ForgejoRelease>(
|
||||||
|
stream,
|
||||||
|
cancellationToken: cancellationToken)
|
||||||
|
?? throw new InvalidDataException("Forgejo вернул пустое описание релиза.");
|
||||||
|
if (!TryParseVersion(release.TagName, out var releaseVersion) ||
|
||||||
|
releaseVersion <= CurrentVersion)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var installer = release.Assets.FirstOrDefault(asset =>
|
||||||
|
asset.Name.StartsWith(InstallerPrefix, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
asset.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var checksum = release.Assets.FirstOrDefault(asset =>
|
||||||
|
string.Equals(asset.Name, installer?.Name + ".sha256", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (installer is null ||
|
||||||
|
checksum is null ||
|
||||||
|
!Uri.TryCreate(installer.BrowserDownloadUrl, UriKind.Absolute, out var installerUri) ||
|
||||||
|
!Uri.TryCreate(checksum.BrowserDownloadUrl, UriKind.Absolute, out var checksumUri) ||
|
||||||
|
!Uri.TryCreate(release.HtmlUrl, UriKind.Absolute, out var releasePage))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
"В релизе нет установщика Windows или файла контрольной суммы.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AvailableUpdate(
|
||||||
|
releaseVersion,
|
||||||
|
releaseVersion.ToString(3),
|
||||||
|
string.IsNullOrWhiteSpace(release.Name)
|
||||||
|
? $"Krona Staff {releaseVersion.ToString(3)}"
|
||||||
|
: release.Name,
|
||||||
|
release.Body ?? "",
|
||||||
|
releasePage,
|
||||||
|
installerUri,
|
||||||
|
checksumUri,
|
||||||
|
installer.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> DownloadAndVerifyAsync(
|
||||||
|
AvailableUpdate update,
|
||||||
|
TerminalSettings settings,
|
||||||
|
IProgress<double>? progress,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var updateDirectory = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Krona",
|
||||||
|
"StaffTerminal",
|
||||||
|
"updates",
|
||||||
|
update.DisplayVersion);
|
||||||
|
Directory.CreateDirectory(updateDirectory);
|
||||||
|
var installerPath = Path.Combine(
|
||||||
|
updateDirectory,
|
||||||
|
Path.GetFileName(update.InstallerFileName));
|
||||||
|
var temporaryPath = installerPath + ".download";
|
||||||
|
|
||||||
|
using var checksumRequest = CreateRequest(
|
||||||
|
HttpMethod.Get,
|
||||||
|
update.ChecksumUrl,
|
||||||
|
settings.UpdateAccessToken);
|
||||||
|
using var checksumResponse = await HttpClient.SendAsync(
|
||||||
|
checksumRequest,
|
||||||
|
cancellationToken);
|
||||||
|
checksumResponse.EnsureSuccessStatusCode();
|
||||||
|
var checksumText = await checksumResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var expectedHash = checksumText
|
||||||
|
.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (expectedHash is null ||
|
||||||
|
expectedHash.Length != 64 ||
|
||||||
|
expectedHash.Any(character => !Uri.IsHexDigit(character)))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
"Файл контрольной суммы обновления повреждён.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var installerRequest = CreateRequest(
|
||||||
|
HttpMethod.Get,
|
||||||
|
update.InstallerUrl,
|
||||||
|
settings.UpdateAccessToken);
|
||||||
|
using var installerResponse = await HttpClient.SendAsync(
|
||||||
|
installerRequest,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead,
|
||||||
|
cancellationToken);
|
||||||
|
installerResponse.EnsureSuccessStatusCode();
|
||||||
|
var totalBytes = installerResponse.Content.Headers.ContentLength;
|
||||||
|
await using (var source = await installerResponse.Content.ReadAsStreamAsync(cancellationToken))
|
||||||
|
await using (var destination = new FileStream(
|
||||||
|
temporaryPath,
|
||||||
|
FileMode.Create,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.None,
|
||||||
|
81920,
|
||||||
|
FileOptions.Asynchronous))
|
||||||
|
{
|
||||||
|
var buffer = new byte[81920];
|
||||||
|
long received = 0;
|
||||||
|
int count;
|
||||||
|
while ((count = await source.ReadAsync(buffer, cancellationToken)) > 0)
|
||||||
|
{
|
||||||
|
await destination.WriteAsync(buffer.AsMemory(0, count), cancellationToken);
|
||||||
|
received += count;
|
||||||
|
if (totalBytes is > 0)
|
||||||
|
{
|
||||||
|
progress?.Report((double)received / totalBytes.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await destination.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var downloaded = File.OpenRead(temporaryPath);
|
||||||
|
var actualHash = Convert.ToHexString(
|
||||||
|
await SHA256.HashDataAsync(downloaded, cancellationToken));
|
||||||
|
if (!CryptographicOperations.FixedTimeEquals(
|
||||||
|
Convert.FromHexString(expectedHash),
|
||||||
|
Convert.FromHexString(actualHash)))
|
||||||
|
{
|
||||||
|
File.Delete(temporaryPath);
|
||||||
|
throw new InvalidDataException(
|
||||||
|
"Контрольная сумма обновления не совпала. Установка отменена.");
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(temporaryPath, installerPath, true);
|
||||||
|
return installerPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Version CurrentVersion =>
|
||||||
|
Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0);
|
||||||
|
|
||||||
|
public static void LaunchInstaller(string installerPath) =>
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = installerPath,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
|
||||||
|
private static HttpRequestMessage CreateRequest(
|
||||||
|
HttpMethod method,
|
||||||
|
Uri uri,
|
||||||
|
string token)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(method, uri);
|
||||||
|
if (!string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
request.Headers.Authorization =
|
||||||
|
new AuthenticationHeaderValue("token", token.Trim());
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateHttpClient()
|
||||||
|
{
|
||||||
|
var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd("KronaStaffTerminal/0.2");
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseVersion(string? value, out Version version)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? "")
|
||||||
|
.Trim()
|
||||||
|
.TrimStart('v', 'V')
|
||||||
|
.Split('-', '+')[0];
|
||||||
|
if (Version.TryParse(normalized, out var parsed))
|
||||||
|
{
|
||||||
|
version = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
version = new Version(0, 0, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ForgejoRelease
|
||||||
|
{
|
||||||
|
[JsonPropertyName("tag_name")]
|
||||||
|
public string TagName { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("body")]
|
||||||
|
public string? Body { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("html_url")]
|
||||||
|
public string HtmlUrl { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("assets")]
|
||||||
|
public List<ForgejoAsset> Assets { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ForgejoAsset
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("browser_download_url")]
|
||||||
|
public string BrowserDownloadUrl { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
101
installer/KronaStaffTerminal.nsi
Normal file
101
installer/KronaStaffTerminal.nsi
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
Unicode True
|
||||||
|
SetCompressor /SOLID lzma
|
||||||
|
|
||||||
|
!include "MUI2.nsh"
|
||||||
|
|
||||||
|
!ifndef APP_VERSION
|
||||||
|
!define APP_VERSION "0.2.0"
|
||||||
|
!endif
|
||||||
|
!ifndef PUBLISH_DIR
|
||||||
|
!error "PUBLISH_DIR is required"
|
||||||
|
!endif
|
||||||
|
!ifndef OUTPUT_DIR
|
||||||
|
!error "OUTPUT_DIR is required"
|
||||||
|
!endif
|
||||||
|
!ifndef PROJECT_ROOT
|
||||||
|
!error "PROJECT_ROOT is required"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!define APP_NAME "Krona Staff Terminal"
|
||||||
|
!define APP_EXE "KronaStaffTerminal.exe"
|
||||||
|
!define APP_REGISTRY_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\KronaStaffTerminal"
|
||||||
|
!define DRIVER_URL "https://www.zkteco.com/en/Biometrics_Module_SDK/ZKFinger-SDK-for-Windows"
|
||||||
|
|
||||||
|
Name "${APP_NAME}"
|
||||||
|
OutFile "${OUTPUT_DIR}\KronaStaffTerminal-Setup-${APP_VERSION}.exe"
|
||||||
|
InstallDir "$LOCALAPPDATA\Programs\Krona Staff Terminal"
|
||||||
|
InstallDirRegKey HKCU "${APP_REGISTRY_KEY}" "InstallLocation"
|
||||||
|
RequestExecutionLevel user
|
||||||
|
Icon "${PROJECT_ROOT}\apps\KronaStaffTerminal\Assets\AppIcon.ico"
|
||||||
|
BrandingText "Krona"
|
||||||
|
ShowInstDetails show
|
||||||
|
ShowUninstDetails show
|
||||||
|
|
||||||
|
VIProductVersion "${APP_VERSION}.0"
|
||||||
|
VIAddVersionKey /LANG=1049 "ProductName" "${APP_NAME}"
|
||||||
|
VIAddVersionKey /LANG=1049 "CompanyName" "Krona"
|
||||||
|
VIAddVersionKey /LANG=1049 "LegalCopyright" "Copyright © 2026 Krona"
|
||||||
|
VIAddVersionKey /LANG=1049 "FileDescription" "Установщик ${APP_NAME}"
|
||||||
|
VIAddVersionKey /LANG=1049 "FileVersion" "${APP_VERSION}"
|
||||||
|
VIAddVersionKey /LANG=1049 "ProductVersion" "${APP_VERSION}"
|
||||||
|
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
!define MUI_ICON "${PROJECT_ROOT}\apps\KronaStaffTerminal\Assets\AppIcon.ico"
|
||||||
|
!define MUI_UNICON "${PROJECT_ROOT}\apps\KronaStaffTerminal\Assets\AppIcon.ico"
|
||||||
|
!define MUI_FINISHPAGE_RUN "$INSTDIR\${APP_EXE}"
|
||||||
|
!define MUI_FINISHPAGE_RUN_TEXT "Запустить Krona Staff Terminal"
|
||||||
|
!define MUI_FINISHPAGE_LINK "Скачать официальный драйвер ZKTeco/BioLink SLK20R"
|
||||||
|
!define MUI_FINISHPAGE_LINK_LOCATION "${DRIVER_URL}"
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
!insertmacro MUI_PAGE_COMPONENTS
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "Russian"
|
||||||
|
|
||||||
|
Section "Krona Staff Terminal" SecMain
|
||||||
|
SectionIn RO
|
||||||
|
SetShellVarContext current
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
SetOverwrite try
|
||||||
|
File /r "${PUBLISH_DIR}\*"
|
||||||
|
|
||||||
|
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||||
|
CreateDirectory "$SMPROGRAMS\Krona"
|
||||||
|
CreateShortcut "$SMPROGRAMS\Krona\Krona Staff Terminal.lnk" "$INSTDIR\${APP_EXE}" "" "$INSTDIR\${APP_EXE}" 0
|
||||||
|
|
||||||
|
WriteRegStr HKCU "${APP_REGISTRY_KEY}" "DisplayName" "${APP_NAME}"
|
||||||
|
WriteRegStr HKCU "${APP_REGISTRY_KEY}" "DisplayVersion" "${APP_VERSION}"
|
||||||
|
WriteRegStr HKCU "${APP_REGISTRY_KEY}" "Publisher" "Krona"
|
||||||
|
WriteRegStr HKCU "${APP_REGISTRY_KEY}" "InstallLocation" "$INSTDIR"
|
||||||
|
WriteRegStr HKCU "${APP_REGISTRY_KEY}" "DisplayIcon" "$INSTDIR\${APP_EXE}"
|
||||||
|
WriteRegStr HKCU "${APP_REGISTRY_KEY}" "UninstallString" "$\"$INSTDIR\Uninstall.exe$\""
|
||||||
|
WriteRegDWORD HKCU "${APP_REGISTRY_KEY}" "NoModify" 1
|
||||||
|
WriteRegDWORD HKCU "${APP_REGISTRY_KEY}" "NoRepair" 1
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Значок на рабочем столе" SecDesktop
|
||||||
|
SetShellVarContext current
|
||||||
|
CreateShortcut "$DESKTOP\Krona Staff Terminal.lnk" "$INSTDIR\${APP_EXE}" "" "$INSTDIR\${APP_EXE}" 0
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
LangString DESC_SecMain ${LANG_RUSSIAN} "Основная программа учёта прихода и ухода."
|
||||||
|
LangString DESC_SecDesktop ${LANG_RUSSIAN} "Создать значок программы на рабочем столе."
|
||||||
|
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
||||||
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecMain} $(DESC_SecMain)
|
||||||
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} $(DESC_SecDesktop)
|
||||||
|
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
||||||
|
|
||||||
|
Section "Uninstall"
|
||||||
|
SetShellVarContext current
|
||||||
|
Delete "$DESKTOP\Krona Staff Terminal.lnk"
|
||||||
|
Delete "$SMPROGRAMS\Krona\Krona Staff Terminal.lnk"
|
||||||
|
RMDir "$SMPROGRAMS\Krona"
|
||||||
|
DeleteRegKey HKCU "${APP_REGISTRY_KEY}"
|
||||||
|
RMDir /r "$INSTDIR"
|
||||||
|
SectionEnd
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"$schema": "./schemas/module.schema.json",
|
"$schema": "./schemas/module.schema.json",
|
||||||
"id": "ru.krona.staff",
|
"id": "ru.krona.staff",
|
||||||
"name": "Krona Staff",
|
"name": "Krona Staff",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"min_platform_version": "3.2.0",
|
"min_platform_version": "3.2.0",
|
||||||
"dependencies": ["ru.krona.core"],
|
"dependencies": ["ru.krona.core"],
|
||||||
"optional_dependencies": [],
|
"optional_dependencies": [],
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@krona/staff",
|
"name": "@krona/staff",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@krona/staff",
|
"name": "@krona/staff",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/helmet": "^13.0.2",
|
"@fastify/helmet": "^13.0.2",
|
||||||
"@fastify/sensible": "^6.0.3",
|
"@fastify/sensible": "^6.0.3",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@krona/staff",
|
"name": "@krona/staff",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
82
scripts/build-terminal-release.ps1
Normal file
82
scripts/build-terminal-release.ps1
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||||
|
$projectPath = Join-Path $repositoryRoot 'apps\KronaStaffTerminal\KronaStaffTerminal.csproj'
|
||||||
|
$installerScript = Join-Path $repositoryRoot 'installer\KronaStaffTerminal.nsi'
|
||||||
|
$stagingRoot = Join-Path $repositoryRoot 'artifacts\terminal-release'
|
||||||
|
$publishDirectory = Join-Path $stagingRoot 'publish'
|
||||||
|
$outputDirectory = Join-Path $repositoryRoot 'artifacts'
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
[xml]$project = Get-Content -LiteralPath $projectPath -Raw
|
||||||
|
$Version = [string]$project.Project.PropertyGroup.Version
|
||||||
|
}
|
||||||
|
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
|
||||||
|
throw "Version must use MAJOR.MINOR.PATCH format."
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedStagingPrefix = $repositoryRoot.TrimEnd('\') + '\artifacts\'
|
||||||
|
$resolvedStaging = [System.IO.Path]::GetFullPath($stagingRoot)
|
||||||
|
if (-not $resolvedStaging.StartsWith($expectedStagingPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw "Refusing to clean staging outside repository artifacts."
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $stagingRoot) {
|
||||||
|
Remove-Item -LiteralPath $stagingRoot -Recurse -Force
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $publishDirectory -Force | Out-Null
|
||||||
|
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
|
||||||
|
|
||||||
|
$publishArguments = @(
|
||||||
|
'publish',
|
||||||
|
$projectPath,
|
||||||
|
'-c', 'Release',
|
||||||
|
'-r', 'win-x64',
|
||||||
|
'--self-contained', 'true',
|
||||||
|
'-p:Platform=x64',
|
||||||
|
"-p:Version=$Version",
|
||||||
|
"-p:BaseOutputPath=$(Join-Path $stagingRoot 'bin')\",
|
||||||
|
'-o', $publishDirectory
|
||||||
|
)
|
||||||
|
& dotnet @publishArguments
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Terminal publish failed."
|
||||||
|
}
|
||||||
|
|
||||||
|
$nsisCandidates = @(
|
||||||
|
(Join-Path ${env:ProgramFiles(x86)} 'NSIS\makensis.exe'),
|
||||||
|
(Join-Path $env:ProgramFiles 'NSIS\makensis.exe'),
|
||||||
|
(Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Programs\NSIS\makensis.exe')
|
||||||
|
)
|
||||||
|
$makensis = $nsisCandidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
|
||||||
|
if ($null -eq $makensis) {
|
||||||
|
throw "NSIS 3 is not installed."
|
||||||
|
}
|
||||||
|
|
||||||
|
$nsisArguments = @(
|
||||||
|
'/INPUTCHARSET', 'UTF8',
|
||||||
|
"/DAPP_VERSION=$Version",
|
||||||
|
"/DPUBLISH_DIR=$publishDirectory",
|
||||||
|
"/DOUTPUT_DIR=$outputDirectory",
|
||||||
|
"/DPROJECT_ROOT=$repositoryRoot",
|
||||||
|
$installerScript
|
||||||
|
)
|
||||||
|
& $makensis @nsisArguments
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Installer compilation failed."
|
||||||
|
}
|
||||||
|
|
||||||
|
$installerPath = Join-Path $outputDirectory "KronaStaffTerminal-Setup-$Version.exe"
|
||||||
|
$hash = (Get-FileHash -LiteralPath $installerPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
$checksumPath = "$installerPath.sha256"
|
||||||
|
$checksumLine = "$hash $([System.IO.Path]::GetFileName($installerPath))" + [Environment]::NewLine
|
||||||
|
[System.IO.File]::WriteAllText(
|
||||||
|
$checksumPath,
|
||||||
|
$checksumLine,
|
||||||
|
[System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
Write-Output $installerPath
|
||||||
|
Write-Output $checksumPath
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export const SERVICE_NAME = 'krona-staff';
|
export const SERVICE_NAME = 'krona-staff';
|
||||||
export const SERVICE_VERSION = '0.1.0';
|
export const SERVICE_VERSION = '0.2.0';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue