Enhance VpnConfigDto to include ClientEmail and update related components
CI / Backend (build + test) (push) Successful in 1m17s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s

- Added ClientEmail property to VpnConfigDto for better client identification.
- Updated ConfigCard to display the ClientEmail in the UI for improved user information.
- Modified API schema and types to accommodate the new ClientEmail field.
- Enhanced localization for displaying the ClientEmail in both Russian and English.
This commit is contained in:
Leonid Pershin
2026-07-13 15:59:09 +03:00
parent 24d9ea1099
commit 7fce5ef181
5 changed files with 12 additions and 3 deletions
@@ -5,13 +5,15 @@ namespace PnvPanel.Application.Configs;
/// <summary> /// <summary>
/// Пользователю показываем только DisplayName + протокол инбаунда — адрес/хост ноды и прочие /// Пользователю показываем только DisplayName + протокол инбаунда — адрес/хост ноды и прочие
/// детали 3x-ui в этот DTO не попадают (см. domain-model.md). /// детали 3x-ui в этот DTO не попадают (см. domain-model.md). Исключение — ClientEmail: это не
/// секрет и не адрес ноды, а просто ключ клиента в 3x-ui, полезный владельцу для диагностики
/// (сверка конфига с записью в панели у админа).
/// </summary> /// </summary>
public sealed record VpnConfigDto( public sealed record VpnConfigDto(
Guid Id, string? Label, VpnProtocol Protocol, string Location, Guid Id, string? Label, string ClientEmail, VpnProtocol Protocol, string Location,
long UsedUpBytes, long UsedDownBytes, DateTimeOffset? ExpiresAt, ConfigStatus Status, DateTimeOffset CreatedAt) long UsedUpBytes, long UsedDownBytes, DateTimeOffset? ExpiresAt, ConfigStatus Status, DateTimeOffset CreatedAt)
{ {
public static VpnConfigDto FromDomain(VpnConfig config, Inbound inbound) => new( public static VpnConfigDto FromDomain(VpnConfig config, Inbound inbound) => new(
config.Id, config.Label, config.Protocol, inbound.DisplayName ?? inbound.Remark, config.Id, config.Label, config.ClientEmail, config.Protocol, inbound.DisplayName ?? inbound.Remark,
config.UsedUpBytes, config.UsedDownBytes, config.ExpiresAt, config.Status, config.CreatedAt); config.UsedUpBytes, config.UsedDownBytes, config.ExpiresAt, config.Status, config.CreatedAt);
} }
@@ -80,6 +80,9 @@ export function ConfigCard({ config }: { config: VpnConfigDto }) {
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{formatBytes(config.usedUpBytes)} {formatBytes(config.usedDownBytes)} {formatBytes(config.usedUpBytes)} {formatBytes(config.usedDownBytes)}
</div> </div>
<p className="truncate font-mono text-xs text-muted-foreground/60">
{t('configs.panelEmail', { email: config.clientEmail })}
</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={() => setDetailsOpen(true)}> <Button size="sm" variant="outline" onClick={() => setDetailsOpen(true)}>
<QrCode className="h-4 w-4" /> <QrCode className="h-4 w-4" />
+1
View File
@@ -2071,6 +2071,7 @@ export interface components {
/** Format: uuid */ /** Format: uuid */
id: string; id: string;
label: null | string; label: null | string;
clientEmail: string;
protocol: components["schemas"]["VpnProtocol"]; protocol: components["schemas"]["VpnProtocol"];
location: string; location: string;
/** Format: int64 */ /** Format: int64 */
+1
View File
@@ -49,6 +49,7 @@ export type ActivationStatusDto = {
export type VpnConfigDto = { export type VpnConfigDto = {
id: string id: string
label: string | null label: string | null
clientEmail: string
protocol: VpnProtocol protocol: VpnProtocol
location: string location: string
usedUpBytes: number usedUpBytes: number
+2
View File
@@ -87,6 +87,7 @@ const resources = {
copyLink: 'Скопировать ссылку', copyLink: 'Скопировать ссылку',
loadingLink: 'Загрузка ссылки…', loadingLink: 'Загрузка ссылки…',
subscriptionLink: 'Ссылка-подписка (для клиента):', subscriptionLink: 'Ссылка-подписка (для клиента):',
panelEmail: 'Email в панели: {{email}}',
aggregatedSubscription: 'Общая подписка', aggregatedSubscription: 'Общая подписка',
aggregatedSubscriptionHint: 'Одна ссылка/QR со всеми активными конфигами — удобно добавить один раз в клиент.', aggregatedSubscriptionHint: 'Одна ссылка/QR со всеми активными конфигами — удобно добавить один раз в клиент.',
status: { status: {
@@ -379,6 +380,7 @@ const resources = {
copyLink: 'Copy link', copyLink: 'Copy link',
loadingLink: 'Loading link…', loadingLink: 'Loading link…',
subscriptionLink: 'Subscription link (for the client app):', subscriptionLink: 'Subscription link (for the client app):',
panelEmail: 'Panel email: {{email}}',
aggregatedSubscription: 'Aggregated subscription', aggregatedSubscription: 'Aggregated subscription',
aggregatedSubscriptionHint: 'One link/QR with all active configs — add it once to your client.', aggregatedSubscriptionHint: 'One link/QR with all active configs — add it once to your client.',
status: { status: {