From cc3344ed6cfcc6ba7ee00bdb3f2e36d06face354 Mon Sep 17 00:00:00 2001 From: kastov Date: Sat, 28 Mar 2026 06:01:52 +0300 Subject: [PATCH 1/4] ci: fix env variable --- .github/workflows/release-to-panel.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release-to-panel.yml b/.github/workflows/release-to-panel.yml index 29cc3d0c..73829b60 100644 --- a/.github/workflows/release-to-panel.yml +++ b/.github/workflows/release-to-panel.yml @@ -59,9 +59,8 @@ jobs: - name: Create Release uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.TOKEN_GH_DEPLOY }} with: + token: ${{ secrets.TOKEN_GH_DEPLOY }} repository: remnawave/panel tag_name: ${{ github.ref_name }} target_commitish: ${{ steps.commit.outputs.sha }} From 5daad91eaac4451b2ced5ea1d35461f1581cf273 Mon Sep 17 00:00:00 2001 From: Yury Kastov Date: Sun, 5 Apr 2026 23:02:54 +0300 Subject: [PATCH 2/4] feat: create SECURITY.md --- SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..b99af496 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. From f250a23685a5e3302aefd0704aa1519e0b9358fb Mon Sep 17 00:00:00 2001 From: StylerHub Date: Wed, 8 Apr 2026 16:27:14 +0300 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=BB=D0=B5=20tag=20?= =?UTF-8?q?=D1=85=D0=BE=D1=81=D1=82=D0=B0=20=D0=B8=D0=B7=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D0=B8=20=D0=B2=20=D0=BC=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=B8=D0=B2=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Миграция tag: string | null → tags: string[] по аналогии с нодами. --- .../contract/commands/hosts/create.command.ts | 20 +++++++++++-------- .../contract/commands/hosts/update.command.ts | 19 ++++++++++-------- libs/contract/models/hosts.schema.ts | 2 +- .../models/resolved-proxy-config.schema.ts | 2 +- .../migration.sql | 10 ++++++++++ prisma/schema.prisma | 2 +- src/modules/hosts/entities/hosts.entity.ts | 2 +- src/modules/hosts/hosts.converter.ts | 2 +- .../hosts/models/host.response.model.ts | 4 ++-- .../hosts/repositories/hosts.repository.ts | 17 +++++++++------- .../generators/xray-json.generator.service.ts | 12 ++++++++--- .../resolved-proxy-config.interface.ts | 2 +- .../resolve-proxy-config.service.ts | 4 ++-- 13 files changed, 62 insertions(+), 36 deletions(-) create mode 100644 prisma/migrations/20260408_migrate_host_tag_to_tags/migration.sql diff --git a/libs/contract/commands/hosts/create.command.ts b/libs/contract/commands/hosts/create.command.ts index c886dcf5..634292d1 100644 --- a/libs/contract/commands/hosts/create.command.ts +++ b/libs/contract/commands/hosts/create.command.ts @@ -63,19 +63,23 @@ export namespace CreateHostCommand { }) .nullable(), ), - tag: z + tags: z .optional( z - .string() - .regex( - /^[A-Z0-9_:]+$/, - 'Tag can only contain uppercase letters, numbers, underscores and colons', + .array( + z + .string() + .regex( + /^[A-Z0-9_:]+$/, + 'Tag can only contain uppercase letters, numbers, underscores and colons', + ) + .max(32, 'Tag must be less than 32 characters'), ) - .max(32, 'Tag must be less than 32 characters') - .nullable(), + .max(10, 'Maximum 10 tags allowed') + .default([]), ) .describe( - 'Optional. Host tag for categorization. Max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', + 'Optional. Host tags for categorization. Max 10 tags, each max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', ), isHidden: z.optional(z.boolean().default(false)), overrideSniFromAddress: z.optional(z.boolean().default(false)), diff --git a/libs/contract/commands/hosts/update.command.ts b/libs/contract/commands/hosts/update.command.ts index c1b8eb93..709d1197 100644 --- a/libs/contract/commands/hosts/update.command.ts +++ b/libs/contract/commands/hosts/update.command.ts @@ -67,19 +67,22 @@ export namespace UpdateHostCommand { }) .nullable(), ), - tag: z + tags: z .optional( z - .string() - .regex( - /^[A-Z0-9_:]+$/, - 'Tag can only contain uppercase letters, numbers, underscores and colons', + .array( + z + .string() + .regex( + /^[A-Z0-9_:]+$/, + 'Tag can only contain uppercase letters, numbers, underscores and colons', + ) + .max(32, 'Tag must be less than 32 characters'), ) - .max(32, 'Tag must be less than 32 characters') - .nullable(), + .max(10, 'Maximum 10 tags allowed'), ) .describe( - 'Optional. Host tag for categorization. Max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', + 'Optional. Host tags for categorization. Max 10 tags, each max 32 characters, uppercase letters, numbers, underscores and colons are allowed.', ), isHidden: z.optional(z.boolean()), overrideSniFromAddress: z.optional(z.boolean()), diff --git a/libs/contract/models/hosts.schema.ts b/libs/contract/models/hosts.schema.ts index 1a8420f8..d621c12f 100644 --- a/libs/contract/models/hosts.schema.ts +++ b/libs/contract/models/hosts.schema.ts @@ -27,7 +27,7 @@ export const HostsSchema = z.object({ }), serverDescription: z.string().max(30).nullable(), - tag: z.string().nullable(), + tags: z.array(z.string()).default([]), isHidden: z.boolean().default(false), overrideSniFromAddress: z.boolean().default(false), keepSniBlank: z.boolean().default(false), diff --git a/libs/contract/models/resolved-proxy-config.schema.ts b/libs/contract/models/resolved-proxy-config.schema.ts index d30fed71..7ca11ee8 100644 --- a/libs/contract/models/resolved-proxy-config.schema.ts +++ b/libs/contract/models/resolved-proxy-config.schema.ts @@ -203,7 +203,7 @@ export const SecurityVariantSchema = z.discriminatedUnion('security', [ export const ProxyEntryMetadataSchema = z.object({ uuid: z.string().uuid(), - tag: z.string().nullable(), + tags: z.array(z.string()).default([]), excludeFromSubscriptionTypes: z.array(z.nativeEnum(SUBSCRIPTION_TEMPLATE_TYPE)), inboundTag: z.string(), configProfileUuid: z.string().uuid().nullable(), diff --git a/prisma/migrations/20260408_migrate_host_tag_to_tags/migration.sql b/prisma/migrations/20260408_migrate_host_tag_to_tags/migration.sql new file mode 100644 index 00000000..c1d2da2f --- /dev/null +++ b/prisma/migrations/20260408_migrate_host_tag_to_tags/migration.sql @@ -0,0 +1,10 @@ +-- AlterTable: migrate host tag (single string) to tags (array of strings) + +-- Step 1: Add new tags column with default empty array +ALTER TABLE "hosts" ADD COLUMN "tags" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[]; + +-- Step 2: Migrate existing tag data to tags array +UPDATE "hosts" SET "tags" = ARRAY["tag"] WHERE "tag" IS NOT NULL; + +-- Step 3: Drop old tag column +ALTER TABLE "hosts" DROP COLUMN "tag"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7dd5b28b..e250eeab 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -282,7 +282,7 @@ model Hosts { keepSniBlank Boolean @default(false) @map("keep_sni_blank") excludeFromSubscriptionTypes String[] @default([]) @map("exclude_from_subscription_types") - tag String? @map("tag") + tags String[] @default([]) @map("tags") isHidden Boolean @default(false) @map("is_hidden") overrideSniFromAddress Boolean @default(false) @map("override_sni_from_address") diff --git a/src/modules/hosts/entities/hosts.entity.ts b/src/modules/hosts/entities/hosts.entity.ts index d08fd91f..8a9c1686 100644 --- a/src/modules/hosts/entities/hosts.entity.ts +++ b/src/modules/hosts/entities/hosts.entity.ts @@ -26,7 +26,7 @@ export class HostsEntity implements Hosts { serverDescription: null | string; allowInsecure: boolean; - tag: null | string; + tags: string[]; isHidden: boolean; overrideSniFromAddress: boolean; diff --git a/src/modules/hosts/hosts.converter.ts b/src/modules/hosts/hosts.converter.ts index 62c87db9..69e9e66d 100644 --- a/src/modules/hosts/hosts.converter.ts +++ b/src/modules/hosts/hosts.converter.ts @@ -34,7 +34,7 @@ const entityToModel = (entity: HostsEntity): Hosts => { shuffleHost: entity.shuffleHost, mihomoX25519: entity.mihomoX25519, - tag: entity.tag, + tags: entity.tags, isHidden: entity.isHidden, overrideSniFromAddress: entity.overrideSniFromAddress, diff --git a/src/modules/hosts/models/host.response.model.ts b/src/modules/hosts/models/host.response.model.ts index b9d95403..d72c06b8 100644 --- a/src/modules/hosts/models/host.response.model.ts +++ b/src/modules/hosts/models/host.response.model.ts @@ -26,7 +26,7 @@ export class HostResponseModel { public shuffleHost: boolean; public mihomoX25519: boolean; - public tag: null | string; + public tags: string[]; public isHidden: boolean; public overrideSniFromAddress: boolean; @@ -69,7 +69,7 @@ export class HostResponseModel { this.shuffleHost = data.shuffleHost; this.mihomoX25519 = data.mihomoX25519; - this.tag = data.tag; + this.tags = data.tags; this.isHidden = data.isHidden; this.overrideSniFromAddress = data.overrideSniFromAddress; diff --git a/src/modules/hosts/repositories/hosts.repository.ts b/src/modules/hosts/repositories/hosts.repository.ts index 0f6acbc8..213235c3 100644 --- a/src/modules/hosts/repositories/hosts.repository.ts +++ b/src/modules/hosts/repositories/hosts.repository.ts @@ -1,4 +1,5 @@ import { Prisma } from '@prisma/client'; +import { sql } from 'kysely'; import { IReorderHost } from 'src/modules/hosts/interfaces/reorder-host.interface'; @@ -93,6 +94,7 @@ export class HostsRepository implements ICrud { | 'excludedInternalSquads' | 'excludeFromSubscriptionTypes' | 'finalMask' + | 'tags' >, ): Promise { const list = await this.prisma.tx.hosts.findMany({ @@ -245,14 +247,15 @@ export class HostsRepository implements ICrud { } public async getAllHostTags(): Promise { - const result = await this.prisma.tx.hosts.findMany({ - select: { - tag: true, - }, - distinct: ['tag'], - }); + const result = await this.qb.kysely + .selectFrom('hosts') + .select(sql`unnest(tags)`.as('tag')) + .distinct() + .where('tags', 'is not', null) + .orderBy('tag') + .execute(); - return result.map((host) => host.tag).filter((tag) => tag !== null); + return result.map((value) => value.tag); } public async addNodesToHost(hostUuid: string, nodes: string[]): Promise { diff --git a/src/modules/subscription-template/generators/xray-json.generator.service.ts b/src/modules/subscription-template/generators/xray-json.generator.service.ts index 7b60ed03..128ca733 100644 --- a/src/modules/subscription-template/generators/xray-json.generator.service.ts +++ b/src/modules/subscription-template/generators/xray-json.generator.service.ts @@ -351,7 +351,9 @@ export class XrayJsonGeneratorService { } if (useHostTagAsTag) { - return hosts.map((h) => this.buildOutbound(h, h.metadata.tag || h.finalRemark)); + return hosts.map((h) => + this.buildOutbound(h, h.metadata.tags[0] || h.finalRemark), + ); } const proxyTag = tagPrefix ?? 'proxy'; @@ -404,13 +406,17 @@ export class XrayJsonGeneratorService { case 'sameTagAsRecipient': return candidates.filter( (h) => - h.metadata.tag && host.metadata.tag && h.metadata.tag === host.metadata.tag, + h.metadata.tags.length > 0 && + host.metadata.tags.length > 0 && + h.metadata.tags.some((t) => host.metadata.tags.includes(t)), ); case 'tagRegex': { const regex = this.parseRegex(selector.pattern); if (!regex) return []; - return candidates.filter((h) => h.metadata.tag && regex.test(h.metadata.tag)); + return candidates.filter( + (h) => h.metadata.tags.length > 0 && h.metadata.tags.some((t) => regex.test(t)), + ); } } } diff --git a/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts b/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts index 38066f6a..cd39f6f5 100644 --- a/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts +++ b/src/modules/subscription-template/resolve-proxy/interfaces/resolved-proxy-config.interface.ts @@ -186,7 +186,7 @@ export type SecurityVariant = TlsSecurity | RealitySecurity | NoneSecurity; export interface IProxyEntryMetadata { uuid: string; - tag: string | null; + tags: string[]; excludeFromSubscriptionTypes: TSubscriptionTemplateType[]; inboundTag: string; configProfileUuid: string | null; diff --git a/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts b/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts index f1a49a6b..2e7cf5b4 100644 --- a/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts +++ b/src/modules/subscription-template/resolve-proxy/resolve-proxy-config.service.ts @@ -564,7 +564,7 @@ export class ResolveProxyConfigService { }, metadata: { uuid: inputHost.uuid, - tag: inputHost.tag, + tags: inputHost.tags, excludeFromSubscriptionTypes: inputHost.excludeFromSubscriptionTypes, inboundTag: inputHost.inboundTag, configProfileUuid: inputHost.configProfileUuid, @@ -676,7 +676,7 @@ export class ResolveProxyConfigService { }, metadata: { uuid: '00000000-0000-0000-0000-000000000000', - tag: null, + tags: [], excludeFromSubscriptionTypes: [], inboundTag: '', configProfileUuid: null, From 8acbc0d60683da36c66a5514bd22deb8acd50cfb Mon Sep 17 00:00:00 2001 From: StylerHub Date: Wed, 8 Apr 2026 18:51:12 +0300 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=D0=B8=D0=BC=D1=8F=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D0=B2=D0=B5=D0=BD=D1=86=D0=B8=D0=B8=2014-=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B0=D0=B9?= =?UTF-8?q?=D0=BC=D1=81=D1=82=D0=B5=D0=BC=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename prisma/migrations/{20260408_migrate_host_tag_to_tags => 20260408120000_migrate_host_tag_to_tags}/migration.sql (100%) diff --git a/prisma/migrations/20260408_migrate_host_tag_to_tags/migration.sql b/prisma/migrations/20260408120000_migrate_host_tag_to_tags/migration.sql similarity index 100% rename from prisma/migrations/20260408_migrate_host_tag_to_tags/migration.sql rename to prisma/migrations/20260408120000_migrate_host_tag_to_tags/migration.sql