Add factory reset functionality and update identity service
- Introduced a new DELETE endpoint `/api/admin/maintenance/factory-reset` for a complete reset of the admin panel, removing all users except the current admin and clearing various data. - Implemented the `FactoryReset` method in `AdminMaintenanceEndpoints` to handle the reset logic. - Added a new method `ListAllUserIdsExceptAsync` in `IIdentityService` to retrieve user IDs excluding a specified user, aiding in the factory reset process. - Updated the frontend to include a confirmation dialog for the factory reset action, enhancing user experience and safety. - Enhanced localization support for the new factory reset feature in both Russian and English, ensuring clarity for all users.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Domain.Apps;
|
||||
using PnvPanel.Infrastructure.Persistence;
|
||||
|
||||
namespace PnvPanel.Infrastructure.Apps;
|
||||
|
||||
internal sealed class ClientAppCatalogSeeder(
|
||||
AppDbContext dbContext,
|
||||
ILogger<ClientAppCatalogSeeder> logger
|
||||
) : IClientAppCatalogSeeder
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
public async Task SeedIfEmptyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (await dbContext.ClientApps.AnyAsync(cancellationToken))
|
||||
return;
|
||||
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "seed", "client-apps.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
logger.LogWarning("Client app catalog seed file not found: {Path}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(path, cancellationToken);
|
||||
var entries = JsonSerializer.Deserialize<List<ClientAppSeedEntry>>(json, JsonOptions) ?? [];
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!Enum.TryParse<OsPlatform>(entry.OperatingSystem, ignoreCase: true, out var os))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Unknown OS '{Os}' in client app catalog seed — skipped",
|
||||
entry.OperatingSystem
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var app = ClientApp.Create(
|
||||
entry.Name,
|
||||
new Uri(entry.DownloadUrl, UriKind.Absolute),
|
||||
os,
|
||||
entry.Description,
|
||||
iconUrl: null,
|
||||
entry.SortOrder
|
||||
);
|
||||
dbContext.ClientApps.Add(app);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Seeded client app catalog: {Count} entries", entries.Count);
|
||||
}
|
||||
|
||||
private sealed record ClientAppSeedEntry(
|
||||
string Name,
|
||||
string OperatingSystem,
|
||||
string DownloadUrl,
|
||||
string? Description,
|
||||
int SortOrder,
|
||||
bool IsEnabled
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Infrastructure.Apps;
|
||||
using PnvPanel.Infrastructure.BackgroundJobs;
|
||||
using PnvPanel.Infrastructure.Identity;
|
||||
using PnvPanel.Infrastructure.Persistence;
|
||||
@@ -135,6 +136,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IJwtTokenService, JwtTokenService>();
|
||||
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
||||
services.AddScoped<IRoleService, RoleService>();
|
||||
services.AddScoped<IClientAppCatalogSeeder, ClientAppCatalogSeeder>();
|
||||
// Один и тот же экземпляр CurrentUser на scope — и как ICurrentUser (чтение), и как
|
||||
// ICurrentUserSetter (запись, только для Telegram-бота, см. TelegramBotHostedService).
|
||||
services.AddScoped<CurrentUser>();
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PnvPanel.Domain.Apps;
|
||||
using PnvPanel.Infrastructure.Persistence;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
|
||||
namespace PnvPanel.Infrastructure.Identity;
|
||||
|
||||
@@ -13,17 +10,12 @@ namespace PnvPanel.Infrastructure.Identity;
|
||||
public sealed class DbInitializer(
|
||||
RoleManager<AppRole> roleManager,
|
||||
UserManager<AppUser> userManager,
|
||||
AppDbContext dbContext,
|
||||
IClientAppCatalogSeeder clientAppCatalogSeeder,
|
||||
IOptions<AdminSeedOptions> adminSeedOptions,
|
||||
IOptions<RolesOptions> rolesOptions,
|
||||
ILogger<DbInitializer> logger
|
||||
)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
public async Task SeedAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureRoleAsync(
|
||||
@@ -39,7 +31,7 @@ public sealed class DbInitializer(
|
||||
isSystem: true
|
||||
);
|
||||
await SeedAdminAsync();
|
||||
await SeedClientAppsAsync(cancellationToken);
|
||||
await clientAppCatalogSeeder.SeedIfEmptyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task EnsureRoleAsync(string name, int maxConfigs, int maxIpLimit, bool isSystem)
|
||||
@@ -100,54 +92,4 @@ public sealed class DbInitializer(
|
||||
await userManager.AddToRoleAsync(admin, RoleNames.Admin);
|
||||
logger.LogInformation("Created admin account {Username}", options.Username);
|
||||
}
|
||||
|
||||
private async Task SeedClientAppsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (await dbContext.ClientApps.AnyAsync(cancellationToken))
|
||||
return;
|
||||
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "seed", "client-apps.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
logger.LogWarning("Client app catalog seed file not found: {Path}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(path, cancellationToken);
|
||||
var entries = JsonSerializer.Deserialize<List<ClientAppSeedEntry>>(json, JsonOptions) ?? [];
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!Enum.TryParse<OsPlatform>(entry.OperatingSystem, ignoreCase: true, out var os))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Unknown OS '{Os}' in client app catalog seed — skipped",
|
||||
entry.OperatingSystem
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var app = ClientApp.Create(
|
||||
entry.Name,
|
||||
new Uri(entry.DownloadUrl, UriKind.Absolute),
|
||||
os,
|
||||
entry.Description,
|
||||
iconUrl: null,
|
||||
entry.SortOrder
|
||||
);
|
||||
dbContext.ClientApps.Add(app);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Seeded client app catalog: {Count} entries", entries.Count);
|
||||
}
|
||||
|
||||
private sealed record ClientAppSeedEntry(
|
||||
string Name,
|
||||
string OperatingSystem,
|
||||
string DownloadUrl,
|
||||
string? Description,
|
||||
int SortOrder,
|
||||
bool IsEnabled
|
||||
);
|
||||
}
|
||||
|
||||
@@ -364,6 +364,18 @@ internal sealed class IdentityService(
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Guid>> ListAllUserIdsExceptAsync(
|
||||
Guid exceptUserId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
return await userManager
|
||||
.Users.AsNoTracking()
|
||||
.Where(u => u.Id != exceptUserId)
|
||||
.Select(u => u.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string> GetPrimaryRoleNameAsync(AppUser user)
|
||||
{
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
|
||||
Reference in New Issue
Block a user