1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
3afafd4e91 feat: Adjust features and fix some issue 2025-05-21 00:44:48 +08:00
b21dae1192 fix: Some timing issue has been fixed 2025-05-20 16:07:07 +08:00
14 changed files with 135 additions and 112 deletions

View File

@ -18,6 +18,12 @@ namespace Flawless.Client.Remote
[System.CodeDom.Compiler.GeneratedCode("Refitter", "1.5.5.0")]
public partial interface IFlawlessServer
{
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")]
[Post("/api/admin/add_user")]
Task AddUser([Body] RegisterRequest body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/superuser/{username}")]
@ -243,7 +249,7 @@ namespace Flawless.Client.Remote
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/fetch_depot")]
Task<ApiResponse<Stream>> FetchDepot(string userName, string repositoryName, [Query] string depotId);
Task<ApiResponse<Stream>> FetchDepot(string userName, string repositoryName, [Query] string depotId, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>

View File

@ -275,11 +275,11 @@ public partial class RepositoryViewModel : RoutableViewModelBase
private async Task StartupTasksAsync()
{
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
await RefreshRepositoryIssuesAsyncCommand.Execute();
await RefreshStatisticDataCommand.Execute();
await RefreshWebhooksCommand.Execute();
await DetectLocalChangesAsyncCommand.Execute();
await RefreshRepositoryRoleInfoAsync();
await RefreshRepositoryIssuesAsync();
await RefreshStatisticDataAsync();
await RefreshWebhooksAsync();
await DetectLocalChangesAsync();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
@ -659,6 +659,8 @@ public partial class RepositoryViewModel : RoutableViewModelBase
[ReactiveCommand]
private async ValueTask RefreshRepositoryIssuesAsync()
{
if (!IsReporterRole) return;
using var l = UIHelper.MakeLoading("Refreshing issues...");
await RepositoryService.C.UpdateIssuesListFromServerAsync(Repository);
}
@ -756,6 +758,8 @@ public partial class RepositoryViewModel : RoutableViewModelBase
[ReactiveCommand]
private async ValueTask DetectLocalChangesAsync()
{
if (!IsDeveloperRole) return;
using var l = UIHelper.MakeLoading("Refreshing local changes...");
var ns = await Task.Run(async () =>
{
@ -773,7 +777,16 @@ public partial class RepositoryViewModel : RoutableViewModelBase
foreach (var n in LocalChangeSetRaw)
n.Included = true;
}
[ReactiveCommand]
private void OpenFolder()
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() {
FileName = PathUtility.GetWorkspacePath(Api.C.Username.Value!, Repository.OwnerName, Repository.Name),
UseShellExecute = true,
Verb = "open"
});
}
[ReactiveCommand]
private void DeselectAllChanges()
@ -783,8 +796,10 @@ public partial class RepositoryViewModel : RoutableViewModelBase
}
[ReactiveCommand]
private async Task RefreshStatisticData()
private async Task RefreshStatisticDataAsync()
{
if (!IsOwnerRole) return;
try
{
var api = Api.C;
@ -800,13 +815,12 @@ public partial class RepositoryViewModel : RoutableViewModelBase
foreach (var dp in rsp.Depots)
DepotsStats.Add(new DepotStatsInfo{ Id = dp.DepotName, Size = dp.DepotSize});
ByDay = new[]
{
ByDay = [
new ColumnSeries<DateTimePoint>
{
Values = rsp.CommitByDay.Select(k => new DateTimePoint(k.Day.LocalDateTime, k.Count)).ToList(),
}
};
];
}
catch (Exception e)
{

View File

@ -2,18 +2,14 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Threading;
using DynamicData;
using Flawless.Client.Models;
using Flawless.Client.Remote;
using Flawless.Client.Service;
using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox;
using LiveChartsCore;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Refit;
@ -49,9 +45,9 @@ public partial class SettingViewModel : RoutableViewModelBase
[Reactive] private string _bio;
[Reactive] private DateTime?
_logSearchFrom = null,
_logSearchTo = null;
[Reactive] private DateTime
_logSearchFrom = DateTime.Now.AddDays(-1),
_logSearchTo = DateTime.Now;
[Reactive] private int _page = 1;
@ -72,10 +68,10 @@ public partial class SettingViewModel : RoutableViewModelBase
{
using (UIHelper.MakeLoading("Fetch server data..."))
{
await RefreshUsersCommand.Execute();
await RefreshUsersAsync();
var sb = new StringBuilder();
ServerBlacklist = sb.AppendJoin(",\n", await Api.C.Gateway.IpWhitelistGet()).ToString();
ServerWhitelist = sb.AppendJoin(",\n", await Api.C.Gateway.IpWhitelistGet()).ToString();
ServerBlacklist = sb.Clear().AppendJoin(",\n", await Api.C.Gateway.IpBlacklistGet()).ToString();
}
}
@ -244,7 +240,8 @@ public partial class SettingViewModel : RoutableViewModelBase
Username = user.Username,
Email = user.Email,
IsActive = user.IsActive,
IsAdmin = user.IsAdmin ?? false
IsAdmin = user.IsAdmin ?? false,
CanEdit = user.Username != Api.C.Username.Value!
});
}
}
@ -257,9 +254,12 @@ public partial class SettingViewModel : RoutableViewModelBase
[ReactiveCommand]
private async Task CreateUserAsync()
{
var result = new UserCreateDialogViewModel();
result.Password = GenerateRandomPassword();
var opt = UIHelper.DefaultOverlayDialogOptionsYesNo();
var result = new UserCreateDialogViewModel
{
Password = GenerateRandomPassword()
};
while (true)
{
var r = await OverlayDialog.ShowModal<UserCreateDialogView, UserCreateDialogViewModel>(result, AppDefaultValues.HostId, opt);
@ -271,14 +271,14 @@ public partial class SettingViewModel : RoutableViewModelBase
try
{
await Api.C.Gateway.Register(new RegisterRequest
await Api.C.Gateway.AddUser(new RegisterRequest
{
Username = result.Username,
Password = result.Password,
Email = result.Email
});
Users.Add(UserService.C.GetUserInfoAsync(result.Username)!);
await this.RefreshUsersAsync();
UIHelper.NotifySuccess($"User {result.Username} create successfully");
}
catch (ApiException ex)
@ -311,11 +311,12 @@ public partial class SettingViewModel : RoutableViewModelBase
var newPassword = GenerateRandomPassword();
try
{
await Api.C.Gateway.RenewPassword(new ResetPasswordRequest
await Api.C.Gateway.ResetPassword(new ResetPasswordRequest
{
Identity = username,
NewPassword = newPassword
});
await UIHelper.SimpleAlert($"Password has been reset to {newPassword}");
}
catch (Exception ex)
@ -386,7 +387,7 @@ public partial class SettingViewModel : RoutableViewModelBase
{
await Api.C.Gateway.SuperuserPost(username, active);
Users.First(x => x.Username == username).IsAdmin = active;
UIHelper.NotifySuccess($"{username} has already {(active ? "enabled" : "disabled")}.");
UIHelper.NotifySuccess($"{username} has already set to {(active ? "superuser" : "nornmal user")}.");
}
catch (Exception ex)
{

View File

@ -12,7 +12,7 @@
<DockPanel Margin="50">
<Grid RowDefinitions="Auto, 18, Auto" ColumnDefinitions="*, Auto" DockPanel.Dock="Top">
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<Label Content="{Binding ServerFriendlyName, StringFormat='Server {0}', FallbackValue='Server LocalTest'}" FontSize="18" FontWeight="400"></Label>
<Label Content="{Binding ServerFriendlyName, FallbackValue='Unknown Server'}" FontSize="18" FontWeight="400"></Label>
<Label Content="Repositories" FontSize="32" FontWeight="600"></Label>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">

View File

@ -6,7 +6,7 @@
xmlns:vm="clr-namespace:Flawless.Client.ViewModels.ModalBox"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:IssueEditDialogViewModel"
MinWidth="400"
MinWidth="500"
x:Class="Flawless.Client.Views.ModalBox.IssueDetailEditView">
<u:Form HorizontalAlignment="Stretch">

View File

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
x:Class="Flawless.Client.Views.ModalBox.UserCreateDialogView"
MinWidth="400"
x:DataType="vm:UserCreateDialogViewModel">
<Grid Margin="10" HorizontalAlignment="Stretch" RowDefinitions="Auto,Auto,Auto">

View File

@ -7,10 +7,10 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoCommitPageView">
<Grid ColumnDefinitions="2*, *">
<TreeDataGrid Grid.Column="0" Source="{Binding Commits}">
<TreeDataGrid Grid.Column="0" Source="{Binding Commits, Mode=TwoWay}">
<TreeDataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="View File Tree"/>
<!-- <MenuItem Header="View File Tree"/> -->
<MenuItem Header="Reset To">
<MenuItem Header="Keep" Command="{Binding RevertFileTreeToSelectedCommitKeepCommand}"/>
<MenuItem Header="Soft" Command="{Binding RevertFileTreeToSelectedCommitSoftCommand}"/>

View File

@ -13,8 +13,10 @@
<u:IconButton
Icon="{StaticResource SemiIconDownload}" Content="Pull" HorizontalAlignment="Stretch"
Command="{Binding PullLatestRepositoryCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconExternalOpen}" Content="Open Folder"
Command="{Binding OpenFolderCommand}"/>
</StackPanel>
<TreeDataGrid Grid.Row="1" Source="{Binding FileTree}">
<TreeDataGrid Grid.Row="1" Source="{Binding FileTree, Mode=TwoWay}">
</TreeDataGrid>
</Grid>
</UserControl>

View File

@ -97,11 +97,11 @@
</ScrollViewer>
</TabItem>
<TabItem Header="Statics" IsVisible="{Binding IsOwnerRole}">
<ScrollViewer Width="600" HorizontalAlignment="Stretch">
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
<StackPanel Spacing="16" HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="10">
<u:IconButton Content="Refresh" Icon="{StaticResource SemiIconRefresh}"
Command="{Binding RefreshWebhooksCommand}"/>
Command="{Binding RefreshStatisticDataCommand}"/>
</StackPanel>
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
<Label>Every Day Commits</Label>

View File

@ -15,8 +15,8 @@
Command="{Binding SelectAllChangesCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconList}" Content="None"
Command="{Binding DeselectAllChangesCommand}"/>
<!-- <u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Open Folder" -->
<!-- Command="{Binding OpenFolderInSystemDefaultExplorerCommand}"/> -->
<u:IconButton Icon="{StaticResource SemiIconExternalOpen}" Content="Open Folder"
Command="{Binding OpenFolderCommand}"/>
</StackPanel>
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>
<Border Grid.Row="1" Grid.Column="2" Classes="Shadow" Theme="{StaticResource CardBorder}">

View File

@ -39,19 +39,10 @@
<TabItem Header="Preference">
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
<u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<u:FormItem Label="Default Storage Location">
<u:PathPicker/>
</u:FormItem>
<!-- <u:FormGroup Header="Workspace Refresh"> -->
<!-- <StackPanel Spacing="4"> -->
<!-- <CheckBox Theme="{StaticResource CardCheckBox}" Content="Open Workspace" -->
<!-- IsChecked="{Binding SettingModel.RefreshWorkspaceOnOpen}"/> -->
<!-- <CheckBox Theme="{StaticResource CardCheckBox}" Content="Filesystem Changed" -->
<!-- IsChecked="{Binding SettingModel.RefreshWorkspaceOnFilesystemChanges}"/> -->
<!-- </StackPanel> -->
<!-- </u:FormGroup> -->
<!-- <u:FormItem Label="Default Storage Location"> -->
<!-- <u:PathPicker/> -->
<!-- </u:FormItem> -->
<u:FormGroup Header="Extern Tools">
<!-- <u:PathPicker u:FormItem.Label="Open Folder"/> -->
<u:PathPicker u:FormItem.Label="Diff Tool"/>
</u:FormGroup>
<StackPanel Orientation="Horizontal" Spacing="4">
@ -92,36 +83,28 @@
Command="{Binding CreateUserCommand}"/>
</StackPanel>
<DataGrid Grid.Row="1" ItemsSource="{Binding Users, Mode=TwoWay}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Username" Binding="{Binding Username}" Width="*"/>
<DataGridTextColumn Header="CreateAt" Binding="{Binding JoinDate}" Width="*"/>
<DataGridCheckBoxColumn Header="Active" Binding="{Binding IsActive}" Width="Auto"/>
<DataGridCheckBoxColumn Header="Admin" Binding="{Binding IsAdmin}" Width="Auto"/>
<DataGridTemplateColumn Header="Operations" Width="2*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="4">
<ListBox Grid.Row="1" ItemsSource="{Binding Users, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Spacing="12" Margin="6">
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<PathIcon Data="{StaticResource SemiIconCrown}" Height="12" Width="12" IsVisible="{Binding IsAdmin}"/>
<PathIcon Data="{StaticResource SemiIconMinusCircle}" Height="12" Width="12" IsVisible="{Binding !IsActive}"/>
<Label Content="{Binding Username}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center" IsEnabled="{Binding CanEdit}">
<Button Content="Enable"
Command="{Binding $parent[views:SettingView].((vm:SettingViewModel)DataContext).InactivateUserCommand}"
CommandParameter="{Binding Username}"
IsEnabled="{Binding !CanEdit}"
IsVisible="{Binding IsActive}"/>
<Button Content="Disable"
Command="{Binding $parent[views:SettingView].((vm:SettingViewModel)DataContext).ActivateUserCommand}"
CommandParameter="{Binding Username}"
IsEnabled="{Binding !CanEdit}"
IsVisible="{Binding !IsActive}"/>
<Button Content="Disable"
Command="{Binding $parent[views:SettingView].((vm:SettingViewModel)DataContext).InactivateUserCommand}"
CommandParameter="{Binding Username}"
IsVisible="{Binding IsActive}"/>
<Button Content="Promote"
Command="{Binding $parent[views:SettingView].((vm:SettingViewModel)DataContext).PromoteUserCommand}"
CommandParameter="{Binding Username}"
IsEnabled="{Binding !CanEdit}"
IsVisible="{Binding !IsAdmin}"
Classes="Danger"/>
@ -129,39 +112,35 @@
Command="{Binding $parent[views:SettingView].((vm:SettingViewModel)DataContext).DemoteUserCommand}"
CommandParameter="{Binding Username}"
IsVisible="{Binding IsAdmin}"
IsEnabled="{Binding !CanEdit}"
Classes="Danger"/>
<Button Content="Delete"
Command="{Binding $parent[views:SettingView].((vm:SettingViewModel)DataContext).DeleteUserCommand}"
CommandParameter="{Binding Username}"
IsEnabled="{Binding !CanEdit}"
Classes="Danger"/>
<Button Content="Reset Password"
Command="{Binding $parent[views:SettingView].((vm:SettingViewModel)DataContext).ForceUpdateUserPasswordCommand}"
CommandParameter="{Binding Username}"
IsEnabled="{Binding !CanEdit}"/>
CommandParameter="{Binding Username}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</TabItem>
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Logfile">
<StackPanel Spacing="8" Orientation="Vertical" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="4">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4">
<DatePicker SelectedDate="{Binding LogSearchFrom}"/>
<Label Content=" → "/>
<DatePicker SelectedDate="{Binding LogSearchTo}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<u:EnumSelector SelectedValue="{Binding Loglevel}"/>
<NumericUpDown Value="{Binding PageSize}" Minimum="10" Maximum="100"/>
<NumericUpDown Value="{Binding Page}" Minimum="1"/>
<u:EnumSelector SelectedValue="{Binding Loglevel, Mode=TwoWay}"/>
<NumericUpDown Value="{Binding PageSize, Mode=TwoWay}" Minimum="10" Maximum="100"/>
<NumericUpDown Value="{Binding Page, Mode=TwoWay}" Minimum="1"/>
<u:IconButton Icon="{StaticResource SemiIconSearch}"
Command="{Binding DownloadServerLogCommand}"/>
</StackPanel>

View File

@ -13,6 +13,7 @@ namespace Flawless.Server.Controllers;
public class AdminController(
UserManager<AppUser> userManager,
AccessControlService accessControlService,
ILogger<AdminController> logger,
AppDbContext dbContext) : ControllerBase
{
@ -23,6 +24,29 @@ public class AdminController(
return null;
}
[HttpPost("add_user")]
public async Task<IActionResult> AddUserAsync(RegisterRequest request)
{
var user = new AppUser
{
UserName = request.Username,
Email = request.Email,
EmailConfirmed = true,
CreatedOn = DateTime.UtcNow,
};
user.RenewSecurityStamp();
var result = await userManager.CreateAsync(user, request.Password);
if (result.Succeeded)
{
logger.LogInformation("User '{0}' created (SUPERUSER REGISTER)", user.UserName);
return Ok();
}
logger.LogInformation("User '{0}' NOT created (SUPERUSER REGISTER) : {1}", user.UserName, result.Errors);
return BadRequest(new FailedResponse(result.Errors));
}
[HttpPost("superuser/{username}")]
public async Task<IActionResult> SetSuperuserAsync(string username, bool toSuper)
{
@ -59,7 +83,7 @@ public class AdminController(
var t = await TestIfValid();
if (t != null) return t;
return await userManager.Users.Select(x => new UserInfoResponse
var r = await userManager.Users.Select(x => new UserInfoResponse
{
Authorized = true,
Username = x.UserName,
@ -71,8 +95,10 @@ public class AdminController(
Email = x.Email,
Phone = x.PhoneNumber,
IsAdmin = x.Admin,
IsActive = x.LockoutEnabled
IsActive = !x.LockoutEnabled
}).ToArrayAsync();
return r;
}
[HttpPost("user/delete/{username}")]
@ -93,6 +119,9 @@ public class AdminController(
[HttpPost("user/enable/{username}")]
public async Task<IActionResult> EnableUserAsync(string username)
{
var t = await TestIfValid();
if (t != null) return t;
var user = await userManager.FindByNameAsync(username);
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
@ -124,11 +153,10 @@ public class AdminController(
if (t != null) return t;
if (r.Identity == null) return BadRequest(new FailedResponse("Identity (User Id) is not set!"));
var user = await userManager.FindByIdAsync(r.Identity);
var user = await userManager.FindByNameAsync(r.Identity);
if (user == null) return BadRequest(new FailedResponse("Identity (User Id) does not exist!"));
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
var result = await userManager.ResetPasswordAsync(user, resetToken, r.NewPassword);
var result = await userManager.ResetPasswordAsync(user, "", r.NewPassword);
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
return Ok();
@ -174,26 +202,18 @@ public class AdminController(
[HttpGet("logs")]
public async Task<ActionResult<IEnumerable<LogEntryResponse>>> GetSystemLogsAsync(
[FromQuery] DateTime? startTime = null,
[FromQuery] DateTime? endTime = null,
[FromQuery] LogLevel? level = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50)
[FromQuery] DateTime startTime,
[FromQuery] DateTime endTime,
[FromQuery] LogLevel level,
[FromQuery] int page,
[FromQuery] int pageSize)
{
var t = await TestIfValid();
if (t != null) return t;
var query = dbContext.SystemLogs.AsQueryable();
// 时间过滤
if (startTime.HasValue)
query = query.Where(l => l.Timestamp >= startTime);
if (endTime.HasValue)
query = query.Where(l => l.Timestamp <= endTime);
// 日志级别过滤
if (level.HasValue && level.Value != LogLevel.None)
query = query.Where(l => l.LogLevel == level.Value);
var query = dbContext.SystemLogs.Where(x =>
x.Timestamp >= startTime && x.Timestamp <= endTime && x.LogLevel >= level
).AsQueryable();
// 分页处理
var totalCount = await query.CountAsync();

View File

@ -10,9 +10,9 @@
"CoreDb": "Server=localhost;Port=5432;User Id=postgres;Database=flawless"
},
"LocalStoragePath": "/Users/cardidi/flawless-data",
"User": {
"PublicRegister": true
},
"UseWebHook": true,
"AllowPublicRegistration": true,
"ServerName": "Cardidi Private Area",
"Jwt": {
"SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",
"Issuer": "test",

View File

@ -10,9 +10,9 @@
"CoreDb": "Server=localhost;Port=5432;User Id=postgres;Database=flawless"
},
"LocalStoragePath": "/Users/cardidi/flawless-data",
"User": {
"PublicRegister": true
},
"UseWebHook": true,
"AllowPublicRegistration": true,
"ServerName": "Cardidi Private Area",
"Jwt": {
"SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",
"Issuer": "test",