Enhance role management by adding pricing fields and updating related logic
CI / Backend (build + test) (push) Successful in 1m22s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s

- Updated `CreateRoleCommand` and `UpdateRoleCommand` to include optional pricing fields: `PricePerConfigPerQuarter` and `PricePerConfigPerYear`.
- Modified `RoleEndpoints` to handle the new pricing parameters during role creation and updates.
- Enhanced validation logic in `CreateRoleCommandValidator` and `UpdateRoleCommandValidator` to ensure pricing fields are non-negative when provided.
- Updated `RoleDto` and `SelectableRoleDto` to include pricing information, ensuring proper data handling in API responses.
- Adjusted frontend components to support new pricing fields in role forms and display total costs based on configurations.
- Updated API documentation to reflect changes in role management endpoints and pricing structure.
This commit is contained in:
Leonid Pershin
2026-07-18 19:02:12 +03:00
parent 68781ef183
commit 285d8180c8
29 changed files with 2638 additions and 30 deletions
@@ -53,7 +53,13 @@ public static class RoleEndpoints
)
{
var result = await sender.Send(
new UpdateRoleCommand(id, body.MaxConfigs, body.MaxIpLimit),
new UpdateRoleCommand(
id,
body.MaxConfigs,
body.MaxIpLimit,
body.PricePerConfigPerQuarter,
body.PricePerConfigPerYear
),
cancellationToken
);
return result.ToHttpResult();
@@ -84,6 +90,11 @@ public static class RoleEndpoints
}
}
public sealed record UpdateRoleBody(int MaxConfigs, int MaxIpLimit);
public sealed record UpdateRoleBody(
int MaxConfigs,
int MaxIpLimit,
int? PricePerConfigPerQuarter,
int? PricePerConfigPerYear
);
public sealed record ChangeUserRoleBody(Guid RoleId);
@@ -22,7 +22,7 @@ public static class SupportEndpoints
{
var group = app.MapGroup("/api/support").WithTags("Support").RequireAuthorization();
group.MapGet("/roles", ListSelectableRoles).Produces<IReadOnlyList<RoleDto>>();
group.MapGet("/roles", ListSelectableRoles).Produces<IReadOnlyList<SelectableRoleDto>>();
group
.MapPost("/tickets/bug-reports", CreateBugReport)
.DisableAntiforgery()
@@ -4,5 +4,10 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Admin.Roles;
public sealed record CreateRoleCommand(string Name, int MaxConfigs, int MaxIpLimit)
: ICommand<Result<RoleDto>>;
public sealed record CreateRoleCommand(
string Name,
int MaxConfigs,
int MaxIpLimit,
int? PricePerConfigPerQuarter,
int? PricePerConfigPerYear
) : ICommand<Result<RoleDto>>;
@@ -15,6 +15,8 @@ public sealed class CreateRoleCommandHandler(IRoleService roleService)
command.Name,
command.MaxConfigs,
command.MaxIpLimit,
command.PricePerConfigPerQuarter,
command.PricePerConfigPerYear,
cancellationToken
);
}
@@ -10,5 +10,12 @@ public sealed class CreateRoleCommandValidator : AbstractValidator<CreateRoleCom
RuleFor(x => x.MaxConfigs).GreaterThanOrEqualTo(-1);
RuleFor(x => x.MaxIpLimit).GreaterThanOrEqualTo(-1);
RuleFor(x => x.PricePerConfigPerQuarter!.Value)
.GreaterThanOrEqualTo(0)
.When(x => x.PricePerConfigPerQuarter.HasValue);
RuleFor(x => x.PricePerConfigPerYear!.Value)
.GreaterThanOrEqualTo(0)
.When(x => x.PricePerConfigPerYear.HasValue);
}
}
@@ -4,5 +4,10 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Admin.Roles;
public sealed record UpdateRoleCommand(Guid RoleId, int MaxConfigs, int MaxIpLimit)
: ICommand<Result<RoleDto>>;
public sealed record UpdateRoleCommand(
Guid RoleId,
int MaxConfigs,
int MaxIpLimit,
int? PricePerConfigPerQuarter,
int? PricePerConfigPerYear
) : ICommand<Result<RoleDto>>;
@@ -15,6 +15,8 @@ public sealed class UpdateRoleCommandHandler(IRoleService roleService)
command.RoleId,
command.MaxConfigs,
command.MaxIpLimit,
command.PricePerConfigPerQuarter,
command.PricePerConfigPerYear,
cancellationToken
);
}
@@ -8,5 +8,12 @@ public sealed class UpdateRoleCommandValidator : AbstractValidator<UpdateRoleCom
{
RuleFor(x => x.MaxConfigs).GreaterThanOrEqualTo(-1);
RuleFor(x => x.MaxIpLimit).GreaterThanOrEqualTo(-1);
RuleFor(x => x.PricePerConfigPerQuarter!.Value)
.GreaterThanOrEqualTo(0)
.When(x => x.PricePerConfigPerQuarter.HasValue);
RuleFor(x => x.PricePerConfigPerYear!.Value)
.GreaterThanOrEqualTo(0)
.When(x => x.PricePerConfigPerYear.HasValue);
}
}
@@ -49,6 +49,8 @@ public sealed class ApproveRoleRequestCommandHandler(
ticket.ProposedRoleName!,
ticket.ProposedMaxConfigs!.Value,
ticket.ProposedMaxIpLimit!.Value,
pricePerConfigPerQuarter: null,
pricePerConfigPerYear: null,
cancellationToken
);
if (!createResult.IsSuccess)
@@ -2,7 +2,15 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Common.Interfaces;
public sealed record RoleDto(Guid Id, string Name, int MaxConfigs, int MaxIpLimit, bool IsSystem);
public sealed record RoleDto(
Guid Id,
string Name,
int MaxConfigs,
int MaxIpLimit,
bool IsSystem,
int? PricePerConfigPerQuarter = null,
int? PricePerConfigPerYear = null
);
public interface IRoleService
{
@@ -10,6 +18,8 @@ public interface IRoleService
string name,
int maxConfigs,
int maxIpLimit,
int? pricePerConfigPerQuarter,
int? pricePerConfigPerYear,
CancellationToken cancellationToken
);
@@ -17,6 +27,8 @@ public interface IRoleService
Guid roleId,
int maxConfigs,
int maxIpLimit,
int? pricePerConfigPerQuarter,
int? pricePerConfigPerYear,
CancellationToken cancellationToken
);
@@ -1,4 +1,3 @@
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Application.Common.Messaging;
using PnvPanel.Application.Common.Models;
@@ -7,5 +6,5 @@ namespace PnvPanel.Application.Support.ListSelectableRoles;
/// <summary>Список ролей для выбора в заявке на роль (без admin) — в отличие от ListRolesQuery
/// (Admin/Roles), доступен любому активированному пользователю.</summary>
public sealed record ListSelectableRolesQuery
: IQuery<Result<IReadOnlyList<RoleDto>>>,
: IQuery<Result<IReadOnlyList<SelectableRoleDto>>>,
IRequiresActivation;
@@ -9,29 +9,31 @@ public sealed class ListSelectableRolesQueryHandler(
IRoleService roleService,
IIdentityService identityService,
ICurrentUser currentUser
) : IQueryHandler<ListSelectableRolesQuery, Result<IReadOnlyList<RoleDto>>>
) : IQueryHandler<ListSelectableRolesQuery, Result<IReadOnlyList<SelectableRoleDto>>>
{
// Совпадает со значением Infrastructure.Identity.RoleNames.Admin — см. пояснение в
// CreateRoleRequestTicketCommandHandler (Application не может ссылаться на Infrastructure).
private const string AdminRoleName = "admin";
public async Task<Result<IReadOnlyList<RoleDto>>> Handle(
public async Task<Result<IReadOnlyList<SelectableRoleDto>>> Handle(
ListSelectableRolesQuery query,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<IReadOnlyList<RoleDto>>(AuthErrors.Unauthorized);
return Result.Failure<IReadOnlyList<SelectableRoleDto>>(AuthErrors.Unauthorized);
var profile = await identityService.GetProfileAsync(userId, cancellationToken);
var roles = await roleService.ListRolesAsync(cancellationToken);
// Без admin (нельзя запросить) и без текущей роли пользователя (уже есть — нечего запрашивать).
// Цена (RoleDto.PricePerConfigPer*) намеренно не пробрасывается — видна только админу.
var selectable = roles
.Where(r => !r.Name.Equals(AdminRoleName, StringComparison.OrdinalIgnoreCase))
.Where(r => profile is null || r.Id != profile.RoleId)
.Select(r => new SelectableRoleDto(r.Id, r.Name, r.MaxConfigs, r.MaxIpLimit))
.ToList();
return Result.Success<IReadOnlyList<RoleDto>>(selectable);
return Result.Success<IReadOnlyList<SelectableRoleDto>>(selectable);
}
}
@@ -0,0 +1,5 @@
namespace PnvPanel.Application.Support.ListSelectableRoles;
/// <summary>Роль для выбора в заявке на роль. Без справочной цены (RoleDto.PricePerConfigPer*) —
/// она видна только админу, этот эндпоинт доступен любому активированному пользователю.</summary>
public sealed record SelectableRoleDto(Guid Id, string Name, int MaxConfigs, int MaxIpLimit);
@@ -18,6 +18,14 @@ public class AppRole : IdentityRole<Guid>
public bool IsSystem { get; set; }
/// <summary>Справочная цена за конфиг за минимальный период оплаты (3 месяца), в рублях. Только для
/// отображения админу — без биллинга/статусов оплаты. У роли `admin` всегда null.</summary>
public int? PricePerConfigPerQuarter { get; set; }
/// <summary>Справочная цена за конфиг за год, в рублях. Задаётся независимо от квартальной цены
/// (не производная — позволяет админу задать скидку за годовую оплату). У роли `admin` всегда null.</summary>
public int? PricePerConfigPerYear { get; set; }
public AppRole() { }
public AppRole(string name)
@@ -16,6 +16,8 @@ internal sealed class RoleService(
string name,
int maxConfigs,
int maxIpLimit,
int? pricePerConfigPerQuarter,
int? pricePerConfigPerYear,
CancellationToken cancellationToken
)
{
@@ -27,6 +29,8 @@ internal sealed class RoleService(
MaxConfigs = maxConfigs,
MaxIpLimit = maxIpLimit,
IsSystem = false,
PricePerConfigPerQuarter = pricePerConfigPerQuarter,
PricePerConfigPerYear = pricePerConfigPerYear,
};
var result = await roleManager.CreateAsync(role);
if (!result.Succeeded)
@@ -46,6 +50,8 @@ internal sealed class RoleService(
Guid roleId,
int maxConfigs,
int maxIpLimit,
int? pricePerConfigPerQuarter,
int? pricePerConfigPerYear,
CancellationToken cancellationToken
)
{
@@ -53,8 +59,13 @@ internal sealed class RoleService(
if (role is null)
return Result.Failure<RoleDto>(RoleErrors.NotFound);
var isAdmin = role.Name!.Equals(RoleNames.Admin, StringComparison.OrdinalIgnoreCase);
role.MaxConfigs = maxConfigs;
role.MaxIpLimit = maxIpLimit;
// Роль admin оплаты не имеет — цена не задаётся, даже если пришла в запросе.
role.PricePerConfigPerQuarter = isAdmin ? null : pricePerConfigPerQuarter;
role.PricePerConfigPerYear = isAdmin ? null : pricePerConfigPerYear;
await roleManager.UpdateAsync(role);
return Result.Success(ToDto(role));
@@ -81,7 +92,15 @@ internal sealed class RoleService(
{
return await roleManager
.Roles.OrderBy(r => r.Name)
.Select(r => new RoleDto(r.Id, r.Name!, r.MaxConfigs, r.MaxIpLimit, r.IsSystem))
.Select(r => new RoleDto(
r.Id,
r.Name!,
r.MaxConfigs,
r.MaxIpLimit,
r.IsSystem,
r.PricePerConfigPerQuarter,
r.PricePerConfigPerYear
))
.ToListAsync(cancellationToken);
}
@@ -118,5 +137,13 @@ internal sealed class RoleService(
}
private static RoleDto ToDto(AppRole role) =>
new(role.Id, role.Name!, role.MaxConfigs, role.MaxIpLimit, role.IsSystem);
new(
role.Id,
role.Name!,
role.MaxConfigs,
role.MaxIpLimit,
role.IsSystem,
role.PricePerConfigPerQuarter,
role.PricePerConfigPerYear
);
}
@@ -0,0 +1,944 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using PnvPanel.Infrastructure.Persistence;
#nullable disable
namespace PnvPanel.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260718154538_AddRolePricing")]
partial class AddRolePricing
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Activation.ActivationRequest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Comment")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("DecidedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("DecidedBy")
.HasColumnType("uuid");
b.Property<string>("RejectionReason")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "Status");
b.ToTable("ActivationRequests", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Apps.ClientApp", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("DownloadUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("IconUrl")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<bool>("IsRecommended")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("OperatingSystem")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<int>("SortOrder")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("ClientApps", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Audit.AuditLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<Guid?>("ActorId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("TargetId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TargetType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.ToTable("AuditLogs", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Configs.TrafficSample", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<Guid>("ConfigId")
.HasColumnType("uuid");
b.Property<long>("DownBytes")
.HasColumnType("bigint");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<long>("UpBytes")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ConfigId", "Timestamp");
b.ToTable("TrafficSamples", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Configs.VpnConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ClientEmail")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientExternalId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("InboundId")
.HasColumnType("uuid");
b.Property<string>("Label")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTimeOffset?>("LastSyncAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Protocol")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("SubscriptionToken")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<long>("UsedDownBytes")
.HasColumnType("bigint");
b.Property<long>("UsedUpBytes")
.HasColumnType("bigint");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("InboundId");
b.HasIndex("SubscriptionToken")
.IsUnique();
b.HasIndex("UserId", "Status");
b.ToTable("VpnConfigs", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Inbounds.Inbound", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.PrimitiveCollection<Guid[]>("AllowedRoleIds")
.IsRequired()
.HasColumnType("uuid[]");
b.Property<string>("DisplayName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<bool>("IsPublished")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LastSyncAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("NodeId")
.HasColumnType("uuid");
b.Property<int>("Port")
.HasColumnType("integer");
b.Property<string>("Protocol")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Remark")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("RemoteInboundId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Id");
b.HasIndex("NodeId", "RemoteInboundId")
.IsUnique();
b.ToTable("Inbounds", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Instructions.InstructionIntro", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(20000)
.HasColumnType("character varying(20000)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("InstructionIntros", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Instructions.InstructionTab", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(20000)
.HasColumnType("character varying(20000)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("SortOrder")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("SortOrder");
b.ToTable("InstructionTabs", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.News.NewsPost", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(20000)
.HasColumnType("character varying(20000)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.ToTable("NewsPosts", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Nodes.Node", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("BaseAddress")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LastSyncAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Location")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.HasKey("Id");
b.ToTable("Nodes", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Support.SupportTicket", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("ProposedMaxConfigs")
.HasColumnType("integer");
b.Property<int?>("ProposedMaxIpLimit")
.HasColumnType("integer");
b.Property<string>("ProposedRoleName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<Guid?>("RequestedRoleId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("Type", "Status");
b.HasIndex("UserId", "Status");
b.ToTable("SupportTickets", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Support.TicketAttachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("CommentId")
.HasColumnType("uuid");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<long>("SizeBytes")
.HasColumnType("bigint");
b.Property<string>("StoredFileName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("Id");
b.HasIndex("CommentId");
b.HasIndex("StoredFileName")
.IsUnique();
b.ToTable("TicketAttachments", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Support.TicketComment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AuthorId")
.HasColumnType("uuid");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TicketId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("TicketId", "CreatedAt");
b.ToTable("TicketComments", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Telegram.TelegramLinkToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("ConsumedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.ToTable("TelegramLinkTokens", (string)null);
});
modelBuilder.Entity("PnvPanel.Domain.Telegram.TelegramLoginRequest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Context")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("TelegramLoginRequests", (string)null);
});
modelBuilder.Entity("PnvPanel.Infrastructure.Identity.AppRole", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<int>("MaxConfigs")
.HasColumnType("integer");
b.Property<int>("MaxIpLimit")
.HasColumnType("integer");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("PricePerConfigPerQuarter")
.HasColumnType("integer");
b.Property<int?>("PricePerConfigPerYear")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("PnvPanel.Infrastructure.Identity.AppUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<DateTimeOffset?>("ActivatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("ActivatedBy")
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsActivated")
.HasColumnType("boolean");
b.Property<bool>("IsBlocked")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<string>("SubscriptionToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset?>("TelegramLinkedAt")
.HasColumnType("timestamp with time zone");
b.Property<long?>("TelegramUserId")
.HasColumnType("bigint");
b.Property<string>("TelegramUsername")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("SubscriptionToken")
.IsUnique();
b.HasIndex("TelegramUserId")
.IsUnique();
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("PnvPanel.Infrastructure.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReplacedByTokenHash")
.HasColumnType("text");
b.Property<DateTimeOffset?>("RevokedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("TokenHash")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("PnvPanel.Infrastructure.Identity.AppRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("PnvPanel.Infrastructure.Identity.AppRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("PnvPanel.Domain.Nodes.Node", b =>
{
b.OwnsOne("PnvPanel.Domain.Nodes.NodeCredentials", "Credentials", b1 =>
{
b1.Property<Guid>("NodeId")
.HasColumnType("uuid");
b1.Property<string>("ProtectedPassword")
.IsRequired()
.HasColumnType("text")
.HasColumnName("CredentialsProtectedPassword");
b1.Property<string>("Username")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("CredentialsUsername");
b1.HasKey("NodeId");
b1.ToTable("Nodes");
b1.WithOwner()
.HasForeignKey("NodeId");
});
b.Navigation("Credentials")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PnvPanel.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddRolePricing : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PricePerConfigPerQuarter",
table: "AspNetRoles",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PricePerConfigPerYear",
table: "AspNetRoles",
type: "integer",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PricePerConfigPerQuarter",
table: "AspNetRoles");
migrationBuilder.DropColumn(
name: "PricePerConfigPerYear",
table: "AspNetRoles");
}
}
}
@@ -708,6 +708,12 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("PricePerConfigPerQuarter")
.HasColumnType("integer");
b.Property<int?>("PricePerConfigPerYear")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NormalizedName")