mirror of
https://github.com/KeyKeeperApp/KeyKeeper.git
synced 2026-05-06 06:46:36 +03:00
merge branch 'feature/session-timeout'
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Threading;
|
||||||
using KeyKeeper.PasswordStore;
|
using KeyKeeper.PasswordStore;
|
||||||
using static KeyKeeper.PasswordStore.FileFormatConstants;
|
using static KeyKeeper.PasswordStore.FileFormatConstants;
|
||||||
|
|
||||||
@@ -7,8 +8,13 @@ namespace KeyKeeper.ViewModels;
|
|||||||
|
|
||||||
public partial class RepositoryWindowViewModel : ViewModelBase
|
public partial class RepositoryWindowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan LockTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
private object currentPage;
|
private object currentPage;
|
||||||
private IPassStore passStore;
|
private IPassStore passStore;
|
||||||
|
private DispatcherTimer? _lockTimer;
|
||||||
|
private DateTime _timerStart;
|
||||||
|
private string _lockTimerDisplay = string.Empty;
|
||||||
|
|
||||||
public Func<string, Task> ShowErrorPopup;
|
public Func<string, Task> ShowErrorPopup;
|
||||||
|
|
||||||
@@ -18,6 +24,12 @@ public partial class RepositoryWindowViewModel : ViewModelBase
|
|||||||
set { currentPage = value; OnPropertyChanged(nameof(CurrentPage)); }
|
set { currentPage = value; OnPropertyChanged(nameof(CurrentPage)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string LockTimerDisplay
|
||||||
|
{
|
||||||
|
get => _lockTimerDisplay;
|
||||||
|
private set { _lockTimerDisplay = value; OnPropertyChanged(nameof(LockTimerDisplay)); }
|
||||||
|
}
|
||||||
|
|
||||||
public RepositoryWindowViewModel(IPassStore store)
|
public RepositoryWindowViewModel(IPassStore store)
|
||||||
{
|
{
|
||||||
passStore = store;
|
passStore = store;
|
||||||
@@ -32,15 +44,72 @@ public partial class RepositoryWindowViewModel : ViewModelBase
|
|||||||
SwitchToLocked();
|
SwitchToLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сбрасывает таймер блокировки (вызывается при любой активности пользователя).
|
||||||
|
/// </summary>
|
||||||
|
public void ResetLockTimer()
|
||||||
|
{
|
||||||
|
if (_lockTimer != null && _lockTimer.IsEnabled)
|
||||||
|
_timerStart = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
private void SwitchToUnlocked()
|
private void SwitchToUnlocked()
|
||||||
{
|
{
|
||||||
var directory = passStore.GetGroupByType(GROUP_TYPE_DEFAULT)
|
var directory = passStore.GetGroupByType(GROUP_TYPE_DEFAULT)
|
||||||
?? passStore.GetRootDirectory();
|
?? passStore.GetRootDirectory();
|
||||||
CurrentPage = new UnlockedRepositoryViewModel(passStore, directory);
|
CurrentPage = new UnlockedRepositoryViewModel(passStore, directory);
|
||||||
|
StartLockTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SwitchToLocked()
|
private void SwitchToLocked()
|
||||||
{
|
{
|
||||||
|
StopLockTimer();
|
||||||
CurrentPage = new LockedRepositoryViewModel(passStore, this);
|
CurrentPage = new LockedRepositoryViewModel(passStore, this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public void StartLockTimer()
|
||||||
|
{
|
||||||
|
StopLockTimer();
|
||||||
|
_timerStart = DateTime.UtcNow;
|
||||||
|
_lockTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_lockTimer.Tick += OnLockTimerTick;
|
||||||
|
_lockTimer.Start();
|
||||||
|
UpdateTimerDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopLockTimer()
|
||||||
|
{
|
||||||
|
if (_lockTimer != null)
|
||||||
|
{
|
||||||
|
_lockTimer.Tick -= OnLockTimerTick;
|
||||||
|
_lockTimer.Stop();
|
||||||
|
_lockTimer = null;
|
||||||
|
}
|
||||||
|
LockTimerDisplay = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLockTimerTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var elapsed = DateTime.UtcNow - _timerStart;
|
||||||
|
var remaining = LockTimeout - elapsed;
|
||||||
|
|
||||||
|
if (remaining <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
StopLockTimer();
|
||||||
|
passStore.Lock();
|
||||||
|
UpdateLockStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTimerDisplay(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTimerDisplay(TimeSpan? remaining = null)
|
||||||
|
{
|
||||||
|
var r = remaining ?? LockTimeout;
|
||||||
|
LockTimerDisplay = $"{r:mm\\:ss}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:i="using:Avalonia.Interactivity"
|
xmlns:i="using:Avalonia.Interactivity"
|
||||||
xmlns:kkp="using:KeyKeeper.Views"
|
xmlns:kkp="using:KeyKeeper.Views"
|
||||||
x:Class="KeyKeeper.Views.RepositoryWindow"
|
x:Class="KeyKeeper.Views.RepositoryWindow"
|
||||||
Closing="RepositoryWindow_Closing"
|
Closing="RepositoryWindow_Closing"
|
||||||
Title="KeyKeeper - Password store"
|
Title="KeyKeeper - Password store"
|
||||||
CanResize="False"
|
CanResize="False"
|
||||||
Width="800"
|
Width="800"
|
||||||
@@ -12,36 +12,59 @@
|
|||||||
Background="White"
|
Background="White"
|
||||||
x:DataType="vm:RepositoryWindowViewModel">
|
x:DataType="vm:RepositoryWindowViewModel">
|
||||||
|
|
||||||
<Window.DataTemplates>
|
<Window.DataTemplates>
|
||||||
<DataTemplate DataType="{x:Type vm:UnlockedRepositoryViewModel}">
|
<DataTemplate DataType="{x:Type vm:UnlockedRepositoryViewModel}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<!-- Синий левый край -->
|
<!-- Синий левый край -->
|
||||||
<Border Width="200"
|
<Border Width="200"
|
||||||
Background="#2328C4"
|
Background="#2328C4"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Stretch"/>
|
VerticalAlignment="Stretch"/>
|
||||||
<StackPanel Margin="20" HorizontalAlignment="Left">
|
<StackPanel Margin="20" HorizontalAlignment="Left">
|
||||||
<!-- Надпись KeyKeeper -->
|
<!-- Надпись KeyKeeper -->
|
||||||
<TextBlock Text="KeyKeeper"
|
<TextBlock Text="KeyKeeper"
|
||||||
FontSize="32"
|
FontSize="32"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Margin="0,0,0,20"/>
|
Margin="0,0,0,20"/>
|
||||||
|
|
||||||
|
<!-- Таймер блокировки под надписью KeyKeeper -->
|
||||||
|
<Border Background="#CC000000"
|
||||||
|
CornerRadius="6"
|
||||||
|
Padding="8,4"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
IsVisible="{Binding $parent[Window].DataContext.LockTimerDisplay,
|
||||||
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||||
|
<!-- Иконка замка (Material Design lock outline) -->
|
||||||
|
<Path Fill="White"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Width="13" Height="13"
|
||||||
|
Stretch="Uniform"
|
||||||
|
Data="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||||
|
<TextBlock Text="{Binding $parent[Window].DataContext.LockTimerDisplay}"
|
||||||
|
Foreground="White"
|
||||||
|
FontSize="13"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Рамочка -->
|
<!-- Рамочка -->
|
||||||
<!-- <Border BorderBrush="Gray"
|
<!-- <Border BorderBrush="Gray"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="5"
|
CornerRadius="5"
|
||||||
Padding="20"
|
Padding="20"
|
||||||
Background="#F5F5F5"
|
Background="#F5F5F5"
|
||||||
HorizontalAlignment="Left">
|
HorizontalAlignment="Left">
|
||||||
|
|
||||||
<StackPanel HorizontalAlignment="Left">
|
<StackPanel HorizontalAlignment="Left">
|
||||||
<Button Content="All Passwords"
|
<Button Content="All Passwords"
|
||||||
Width="120"
|
Width="120"
|
||||||
Height="30"
|
Height="30"
|
||||||
HorizontalAlignment="Left"/>
|
HorizontalAlignment="Left"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</Border> -->
|
</Border> -->
|
||||||
<!-- Save Passwords -->
|
<!-- Save Passwords -->
|
||||||
@@ -52,93 +75,93 @@
|
|||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Margin="0,20,0,0"/>
|
Margin="0,20,0,0"/>
|
||||||
|
|
||||||
<!-- New Entry -->
|
<!-- New Entry -->
|
||||||
<Button Content="New Entry"
|
<Button Content="New Entry"
|
||||||
Classes="accentSidebarButton"
|
Classes="accentSidebarButton"
|
||||||
Click="AddEntryButton_Click"
|
Click="AddEntryButton_Click"
|
||||||
Height="30"
|
Height="30"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Margin="0,20,0,0"/>
|
Margin="0,20,0,0"/>
|
||||||
|
|
||||||
<!-- Edit Selected Entry -->
|
<!-- Edit Selected Entry -->
|
||||||
<Button Content="Edit Selected Entry"
|
<Button Content="Edit Selected Entry"
|
||||||
Classes="accentSidebarButton"
|
Classes="accentSidebarButton"
|
||||||
Click="EditEntryButton_Click"
|
Click="EditEntryButton_Click"
|
||||||
Height="30"
|
Height="30"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Margin="0,20,0,0"/>
|
Margin="0,20,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- ListBox с паролями -->
|
<!-- ListBox с паролями -->
|
||||||
<ListBox x:Name="PasswordsListBox"
|
<ListBox x:Name="PasswordsListBox"
|
||||||
Width="580"
|
Width="580"
|
||||||
Margin="210 10 10 10"
|
Margin="210 10 10 10"
|
||||||
ItemsSource="{Binding Passwords}"
|
ItemsSource="{Binding Passwords}"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
SelectionMode="Single">
|
SelectionMode="Single">
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<WrapPanel Orientation="Horizontal" />
|
<WrapPanel Orientation="Horizontal" />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
|
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border Background="Transparent" DoubleTapped="Entry_DoubleTapped">
|
<Border Background="Transparent" DoubleTapped="Entry_DoubleTapped">
|
||||||
<StackPanel Width="100"
|
<StackPanel Width="100"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
HorizontalAlignment="Center">
|
HorizontalAlignment="Center">
|
||||||
<Svg Path="{Binding IconPath}" Width="48" Height="48"/>
|
<Svg Path="{Binding IconPath}" Width="48" Height="48"/>
|
||||||
<TextBlock Text="{Binding Name}"
|
<TextBlock Text="{Binding Name}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Foreground="Black" />
|
Foreground="Black" />
|
||||||
<TextBlock Text="{Binding Username.Value}"
|
<TextBlock Text="{Binding Username.Value}"
|
||||||
Foreground="#666"
|
Foreground="#666"
|
||||||
HorizontalAlignment="Center" />
|
HorizontalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Border.ContextMenu>
|
<Border.ContextMenu>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<MenuItem Name="entryCtxMenuCopyUsername" Header="Copy username" Click="EntryContextMenuItem_Click"/>
|
<MenuItem Name="entryCtxMenuCopyUsername" Header="Copy username" Click="EntryContextMenuItem_Click"/>
|
||||||
<MenuItem Name="entryCtxMenuCopyPassword" Header="Copy password" Click="EntryContextMenuItem_Click"/>
|
<MenuItem Name="entryCtxMenuCopyPassword" Header="Copy password" Click="EntryContextMenuItem_Click"/>
|
||||||
<!-- Новый пункт меню "Edit" -->
|
<!-- Новый пункт меню "Edit" -->
|
||||||
<MenuItem Name="entryCtxMenuEdit" Header="Edit" Click="EntryContextMenuItem_Click"/>
|
<MenuItem Name="entryCtxMenuEdit" Header="Edit" Click="EntryContextMenuItem_Click"/>
|
||||||
<MenuItem Name="entryCtxMenuDelete" Header="Delete" Click="EntryContextMenuItem_Click"/>
|
<MenuItem Name="entryCtxMenuDelete" Header="Delete" Click="EntryContextMenuItem_Click"/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Border.ContextMenu>
|
</Border.ContextMenu>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<kkp:ToastNotificationHost x:Name="NotificationHost"
|
<kkp:ToastNotificationHost x:Name="NotificationHost"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="20"
|
Margin="20"
|
||||||
Duration="0:0:2" />
|
Duration="0:0:2" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
|
<DataTemplate DataType="{x:Type vm:LockedRepositoryViewModel}">
|
||||||
<StackPanel Margin="20"
|
<StackPanel Margin="20"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Spacing="10">
|
Spacing="10">
|
||||||
<TextBlock Text="Enter credentials to unlock"
|
<TextBlock Text="Enter credentials to unlock"
|
||||||
Foreground="#2328C4"
|
Foreground="#2328C4"
|
||||||
FontSize="32" />
|
FontSize="32" />
|
||||||
|
|
||||||
<TextBox x:Name="UnlockPasswordEdit"
|
<TextBox x:Name="UnlockPasswordEdit"
|
||||||
Text="{Binding UnlockPassword, Mode=TwoWay}"
|
Text="{Binding UnlockPassword, Mode=TwoWay}"
|
||||||
PasswordChar="*"
|
PasswordChar="*"
|
||||||
Width="450" />
|
Width="450" />
|
||||||
|
|
||||||
<Button x:Name="UnlockButton"
|
<Button x:Name="UnlockButton"
|
||||||
Command="{Binding TryUnlock}"
|
Command="{Binding TryUnlock}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Content="Unlock!" />
|
Content="Unlock!" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</Window.DataTemplates>
|
</Window.DataTemplates>
|
||||||
|
|
||||||
<ContentControl Content="{Binding CurrentPage}"/>
|
<ContentControl Content="{Binding CurrentPage}"/>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ public partial class RepositoryWindow : Window
|
|||||||
protected override void OnOpened(EventArgs e)
|
protected override void OnOpened(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnOpened(e);
|
base.OnOpened(e);
|
||||||
|
AddHandler(PointerMovedEvent, OnUserActivity, RoutingStrategies.Tunnel);
|
||||||
|
AddHandler(PointerPressedEvent, OnUserActivity, RoutingStrategies.Tunnel);
|
||||||
|
AddHandler(KeyDownEvent, OnUserActivity, RoutingStrategies.Tunnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserActivity(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is RepositoryWindowViewModel vm)
|
||||||
|
vm.ResetLockTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void RepositoryWindow_Closing(object? sender, WindowClosingEventArgs e)
|
private async void RepositoryWindow_Closing(object? sender, WindowClosingEventArgs e)
|
||||||
@@ -77,8 +86,13 @@ public partial class RepositoryWindow : Window
|
|||||||
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
|
if (DataContext is RepositoryWindowViewModel vm_ && vm_.CurrentPage is UnlockedRepositoryViewModel vm)
|
||||||
{
|
{
|
||||||
EntryEditWindow dialog = new();
|
EntryEditWindow dialog = new();
|
||||||
|
|
||||||
|
vm_.StopLockTimer();
|
||||||
|
|
||||||
await dialog.ShowDialog(this);
|
await dialog.ShowDialog(this);
|
||||||
|
|
||||||
|
vm_.StartLockTimer();
|
||||||
|
|
||||||
if (dialog.EditedEntry != null)
|
if (dialog.EditedEntry != null)
|
||||||
vm.AddEntry(dialog.EditedEntry);
|
vm.AddEntry(dialog.EditedEntry);
|
||||||
}
|
}
|
||||||
@@ -104,8 +118,13 @@ public partial class RepositoryWindow : Window
|
|||||||
|
|
||||||
EntryEditWindow dialog = new();
|
EntryEditWindow dialog = new();
|
||||||
dialog.SetEntry(selectedEntry);
|
dialog.SetEntry(selectedEntry);
|
||||||
|
|
||||||
|
vm_.StopLockTimer();
|
||||||
|
|
||||||
await dialog.ShowDialog(this);
|
await dialog.ShowDialog(this);
|
||||||
|
|
||||||
|
vm_.StartLockTimer();
|
||||||
|
|
||||||
if (dialog.EditedEntry != null)
|
if (dialog.EditedEntry != null)
|
||||||
{
|
{
|
||||||
vm.UpdateEntry(dialog.EditedEntry);
|
vm.UpdateEntry(dialog.EditedEntry);
|
||||||
@@ -151,7 +170,9 @@ public partial class RepositoryWindow : Window
|
|||||||
{
|
{
|
||||||
EntryEditWindow dialog = new();
|
EntryEditWindow dialog = new();
|
||||||
dialog.SetEntry(pwd);
|
dialog.SetEntry(pwd);
|
||||||
|
vm.StopLockTimer();
|
||||||
await dialog.ShowDialog(this);
|
await dialog.ShowDialog(this);
|
||||||
|
vm.StartLockTimer();
|
||||||
if (dialog.EditedEntry != null)
|
if (dialog.EditedEntry != null)
|
||||||
{
|
{
|
||||||
pageVm.UpdateEntry(dialog.EditedEntry);
|
pageVm.UpdateEntry(dialog.EditedEntry);
|
||||||
@@ -202,4 +223,4 @@ public static class VisualTreeExtensions
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user