Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
86308e2
feat: implement bulk user management with add and remove user command…
kastov Dec 28, 2025
1c5fe1f
chore: bump version to 2.5.0 in package.json
kastov Dec 28, 2025
bb8fe47
chore: bump version to 2.5.1 in package.json; add EVENTS_SCOPES const…
kastov Dec 28, 2025
a7bc54f
fix: filter active
kastov Dec 28, 2025
daaf655
feat: add revokeOnlyPasswords option to RevokeUserSubscriptionCommand
kastov Dec 29, 2025
1889d02
chore: update version to 2.5.0 in package.json and package-lock.json
kastov Dec 29, 2025
9234c62
fix: correct webhook zod schema
kastov Dec 29, 2025
a1cd32e
feat: add HWID custom remarks and update subscription handling for de…
kastov Dec 29, 2025
7fd5727
fix: update subscription handling
kastov Dec 29, 2025
b970562
feat: add metadata retrieval to system service and controller, update…
kastov Dec 30, 2025
27063a9
fix: reorganize metadata arguments in Dockerfile
kastov Dec 30, 2025
35b4113
refactor: version extraction
kastov Dec 30, 2025
72f74d6
feat(subscription-page): Update template keys and configuration
kastov Dec 30, 2025
5505c9d
fix: update default subpage config
kastov Dec 30, 2025
9bf25b0
chore: clean up unused remarks in subscription settings
kastov Dec 30, 2025
8362ada
feat: template variable in maxDevicesAnnounce
kastov Dec 30, 2025
ef5641c
fix: correct typo in default subpage config
kastov Jan 2, 2026
4ef8ee5
feat: add new traffic-related template keys
kastov Jan 3, 2026
ff7ddcd
refactor: change template value handling to lazy evaluation
kastov Jan 5, 2026
80eefef
fix: accessible nodes ordering
kastov Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,23 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN_GH_DEPLOY }}

- name: Prepare build metadata
id: meta
run: |
echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
echo "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "frontend_commit=$(curl -s https://api.github.com/repos/remnawave/frontend/commits/${{ github.ref_name }} | jq -r '.sha')" >> $GITHUB_OUTPUT

- name: Build and push
uses: docker/build-push-action@v3
with:
build-args: |
__RW_METADATA_GIT_BRANCH=${{ github.ref_name }}
__RW_METADATA_BUILD_TIME=${{ steps.meta.outputs.build_time }}
__RW_METADATA_BUILD_NUMBER=${{ github.run_number }}
__RW_METADATA_VERSION=${{ steps.meta.outputs.version }}
__RW_METADATA_GIT_BACKEND_COMMIT=${{ github.sha }}
__RW_METADATA_GIT_FRONTEND_COMMIT=${{ steps.meta.outputs.frontend_commit }}
context: .
platforms: linux/amd64,linux/arm64
push: true
Expand Down
25 changes: 18 additions & 7 deletions .github/workflows/build-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,33 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN_GH_DEPLOY }}

- name: Prepare build metadata
id: meta
run: |
echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
echo "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "frontend_commit=$(curl -s https://api.github.com/repos/remnawave/frontend/commits/${{ github.ref_name }} | jq -r '.sha')" >> $GITHUB_OUTPUT

- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
build-args: |
FRONTEND_URL=https://github.com/remnawave/frontend/releases/download/dev-build/remnawave-frontend.zip
FRONTEND_WITH_CROWDIN=https://github.com/remnawave/frontend/releases/download/dev-build/remnawave-frontend-loc.zip
BRANCH=dev

context: .
file: Dockerfile
platforms: linux/amd64
push: true
tags: |
remnawave/backend:dev
ghcr.io/remnawave/backend:dev

build-args: |
FRONTEND_URL=https://github.com/remnawave/frontend/releases/download/dev-build/remnawave-frontend.zip
FRONTEND_WITH_CROWDIN=https://github.com/remnawave/frontend/releases/download/dev-build/remnawave-frontend-loc.zip
BRANCH=dev
__RW_METADATA_VERSION=${{ steps.meta.outputs.version }}
__RW_METADATA_GIT_BRANCH=${{ github.ref_name }}
__RW_METADATA_BUILD_TIME=${{ steps.meta.outputs.build_time }}
__RW_METADATA_BUILD_NUMBER=${{ github.run_number }}
__RW_METADATA_GIT_BACKEND_COMMIT=${{ github.sha }}
__RW_METADATA_GIT_FRONTEND_COMMIT=${{ steps.meta.outputs.frontend_commit }}
send-finish-tg-msg:
name: Send TG message
needs: [build-docker-image]
Expand Down
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ WORKDIR /opt/app

ARG BRANCH=main

ARG __RW_METADATA_VERSION=1.1.1
ARG __RW_METADATA_GIT_BACKEND_COMMIT=0f344f388807f5323b49024a563b3f8146d66857
ARG __RW_METADATA_GIT_FRONTEND_COMMIT=0f344f388807f5323b49024a563b3f8146d66857
ARG __RW_METADATA_GIT_BRANCH=dev
ARG __RW_METADATA_BUILD_TIME=2011-11-11T11:11:11Z
ARG __RW_METADATA_BUILD_NUMBER=0

# Install jemalloc
# RUN apk add --no-cache jemalloc
# ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2
Expand All @@ -69,6 +76,13 @@ ENV PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1
ENV PM2_DISABLE_VERSION_CHECK=true
ENV NODE_OPTIONS="--max-old-space-size=16384"

ENV __RW_METADATA_VERSION=${__RW_METADATA_VERSION}
ENV __RW_METADATA_GIT_BACKEND_COMMIT=${__RW_METADATA_GIT_BACKEND_COMMIT}
ENV __RW_METADATA_GIT_FRONTEND_COMMIT=${__RW_METADATA_GIT_FRONTEND_COMMIT}
ENV __RW_METADATA_GIT_BRANCH=${__RW_METADATA_GIT_BRANCH}
ENV __RW_METADATA_BUILD_TIME=${__RW_METADATA_BUILD_TIME}
ENV __RW_METADATA_BUILD_NUMBER=${__RW_METADATA_BUILD_NUMBER}

COPY --from=backend-build /opt/app/dist ./dist
COPY --from=frontend /opt/frontend/frontend_temp/dist ./frontend
COPY --from=frontend /opt/frontend/frontend_crowdin_temp/dist ./frontend-crowdin
Expand Down
1 change: 1 addition & 0 deletions libs/contract/api/controllers/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const SYSTEM_ROUTES = {
ENCRYPT_HAPP_CRYPTO_LINK: 'tools/happ/encrypt',
},
HEALTH: 'health',
METADATA: 'metadata',
TESTERS: {
SRR_MATCHER: 'testers/srr-matcher',
},
Expand Down
1 change: 1 addition & 0 deletions libs/contract/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export const REST_API = {
},
SYSTEM: {
HEALTH: `${ROOT}/${CONTROLLERS.SYSTEM_CONTROLLER}/${CONTROLLERS.SYSTEM_ROUTES.HEALTH}`,
METADATA: `${ROOT}/${CONTROLLERS.SYSTEM_CONTROLLER}/${CONTROLLERS.SYSTEM_ROUTES.METADATA}`,
STATS: {
SYSTEM_STATS: `${ROOT}/${CONTROLLERS.SYSTEM_CONTROLLER}/${CONTROLLERS.SYSTEM_ROUTES.STATS.SYSTEM_STATS}`,
BANDWIDTH_STATS: `${ROOT}/${CONTROLLERS.SYSTEM_CONTROLLER}/${CONTROLLERS.SYSTEM_ROUTES.STATS.BANDWIDTH_STATS}`,
Expand Down
38 changes: 38 additions & 0 deletions libs/contract/commands/system/get-metadata.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from 'zod';

import { getEndpointDetails } from '../../constants';
import { REST_API, SYSTEM_ROUTES } from '../../api';

export namespace GetMetadataCommand {
export const url = REST_API.SYSTEM.METADATA;
export const TSQ_url = url;

export const endpointDetails = getEndpointDetails(
SYSTEM_ROUTES.METADATA,
'get',
'Get Remnawave Information',
);

export const ResponseSchema = z.object({
response: z.object({
version: z.string(),
build: z.object({
time: z.string(),
number: z.string(),
}),
git: z.object({
backend: z.object({
commitSha: z.string(),
branch: z.string(),
commitUrl: z.string(),
}),
frontend: z.object({
commitSha: z.string(),
commitUrl: z.string(),
}),
}),
}),
});

export type Response = z.infer<typeof ResponseSchema>;
}
1 change: 1 addition & 0 deletions libs/contract/commands/system/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './get-bandwidth-stats.command';
export * from './get-metadata.command';
export * from './get-nodes-metrics.command';
export * from './get-nodes-statistics';
export * from './get-remnawave-health.command';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export namespace RevokeUserSubscriptionCommand {
export const RequestBodySchema = z.preprocess(
(val) => val || {},
z.object({
revokeOnlyPasswords: z.optional(
z
.boolean()
.default(false)
.describe(
'Optional. If true, only passwords will be revoked, without changing the short UUID (Subscription URL).',
),
),

shortUuid: z.optional(
z
.string()
Expand Down
17 changes: 17 additions & 0 deletions libs/contract/constants/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,20 @@ export type TAllEvents =
| TCRMEvents
| TUserHwidDevicesEvents;
export type TAllEventChannels = 'telegram' | 'webhook';

export const EVENTS_SCOPES = {
USER: 'user',
USER_HWID_DEVICES: 'user_hwid_devices',
NODE: 'node',
SERVICE: 'service',
ERRORS: 'errors',
CRM: 'crm',
} as const;

export type TEventsScope = (typeof EVENTS_SCOPES)[keyof typeof EVENTS_SCOPES];

type ObjectValues<T> = T[keyof T];
type NonEmptyArray<T> = [T, ...T[]];

export const toZodEnum = <T extends Record<string, string>>(obj: T) =>
Object.values(obj) as NonEmptyArray<ObjectValues<T>>;
3 changes: 3 additions & 0 deletions libs/contract/constants/templates/template-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ export const TEMPLATE_KEYS = [
'EXPIRE_UNIX',
'SHORT_UUID',
'ID',
'TRAFFIC_USED_BYTES',
'TRAFFIC_LEFT_BYTES',
'TOTAL_TRAFFIC_BYTES',
] as const;
export type TemplateKeys = (typeof TEMPLATE_KEYS)[number];
1 change: 1 addition & 0 deletions libs/contract/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './subscription-settings.schema';
export * from './subscription-template.schema';
export * from './tanstack-query';
export * from './users.schema';
export * from './webhook';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export const CustomRemarksSchema = z.object({
limitedUsers: z.array(z.string()).min(1),
disabledUsers: z.array(z.string()).min(1),
emptyHosts: z.array(z.string()).min(1),
emptyInternalSquads: z.array(z.string()).min(1),
HWIDMaxDevicesExceeded: z.array(z.string()).min(1),
HWIDNotSupported: z.array(z.string()).min(1),
});

export type TCustomRemarks = z.infer<typeof CustomRemarksSchema>;
1 change: 1 addition & 0 deletions libs/contract/models/webhook/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './webhook.schema';
115 changes: 115 additions & 0 deletions libs/contract/models/webhook/webhook.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import z from 'zod';

import { EVENTS, EVENTS_SCOPES, toZodEnum } from '../../constants';
import { HwidUserDeviceSchema } from '../hwid-user-device.schema';
import { ExtendedUsersSchema } from '../extended-users.schema';
import { NodesSchema } from '../nodes.schema';

export const RemnawaveWebhookUserEvents = z.object({
scope: z.literal(EVENTS_SCOPES.USER),
event: z.enum(toZodEnum(EVENTS.USER)),
timestamp: z
.string()
.datetime()
.transform((str) => new Date(str)),
data: ExtendedUsersSchema,
meta: z
.object({
notConnectedAfterHours: z.number().int().nullable().optional(),
})
.nullable(),
});

export const RemnawaveWebhookUserHwidDevicesEvents = z.object({
scope: z.literal(EVENTS_SCOPES.USER_HWID_DEVICES),
event: z.enum(toZodEnum(EVENTS.USER_HWID_DEVICES)),
timestamp: z
.string()
.datetime()
.transform((str) => new Date(str)),
data: z.object({
user: ExtendedUsersSchema,
hwidUserDevice: HwidUserDeviceSchema,
}),
});

export const RemnawaveWebhookNodeEvents = z.object({
scope: z.literal(EVENTS_SCOPES.NODE),
event: z.enum(toZodEnum(EVENTS.NODE)),
timestamp: z
.string()
.datetime()
.transform((str) => new Date(str)),
data: NodesSchema,
});

export const RemnawaveWebhookServiceEvents = z.object({
scope: z.literal(EVENTS_SCOPES.SERVICE),
event: z.enum(toZodEnum(EVENTS.SERVICE)),
timestamp: z
.string()
.datetime()
.transform((str) => new Date(str)),
data: z.object({
loginAttempt: z
.object({
username: z.string(),
ip: z.string(),
userAgent: z.string(),
description: z.string().optional(),
password: z.string().optional(),
})
.optional(),
panelVersion: z.string().optional(),
}),
});

export const RemnawaveWebhookErrorsEvents = z.object({
scope: z.literal(EVENTS_SCOPES.ERRORS),
event: z.enum(toZodEnum(EVENTS.ERRORS)),
timestamp: z
.string()
.datetime()
.transform((str) => new Date(str)),
data: z.object({
description: z.string(),
}),
});

export const RemnawaveWebhookCrmEvents = z.object({
scope: z.literal(EVENTS_SCOPES.CRM),
event: z.enum(toZodEnum(EVENTS.CRM)),
timestamp: z
.string()
.datetime()
.transform((str) => new Date(str)),
data: z.object({
providerName: z.string(),
nodeName: z.string(),
nextBillingAt: z
.string()
.datetime()
.transform((str) => new Date(str)),
loginUrl: z.string(),
}),
});

export const RemnawaveWebhookEventSchema = z.discriminatedUnion('scope', [
RemnawaveWebhookUserEvents,
RemnawaveWebhookUserHwidDevicesEvents,
RemnawaveWebhookNodeEvents,
RemnawaveWebhookServiceEvents,
RemnawaveWebhookErrorsEvents,
RemnawaveWebhookCrmEvents,
]);

export type TRemnawaveWebhookEvent = z.infer<typeof RemnawaveWebhookEventSchema>;

export type TRemnawaveWebhookUserEvent = z.infer<typeof RemnawaveWebhookUserEvents>;
export type TRemnawaveWebhookNodeEvent = z.infer<typeof RemnawaveWebhookNodeEvents>;
export type TRemnawaveWebhookServiceEvent = z.infer<typeof RemnawaveWebhookServiceEvents>;
export type TRemnawaveWebhookErrorsEvent = z.infer<typeof RemnawaveWebhookErrorsEvents>;
export type TRemnawaveWebhookCrmEvent = z.infer<typeof RemnawaveWebhookCrmEvents>;
export type TRemnawaveWebhookUserHwidDevicesEvent = z.infer<
typeof RemnawaveWebhookUserHwidDevicesEvents
>;
2 changes: 1 addition & 1 deletion libs/contract/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@remnawave/backend-contract",
"version": "2.4.1",
"version": "2.5.9",
"public": true,
"license": "AGPL-3.0-only",
"description": "A contract library for Remnawave Backend. It can be used in backend and frontend.",
Expand Down
7 changes: 6 additions & 1 deletion libs/subscription-page/constants/template-keys.constant.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export const SUBSCRIPTION_PAGE_TEMPLATE_KEYS = ['USERNAME', 'SUBSCRIPTION_LINK'] as const;
export const SUBSCRIPTION_PAGE_TEMPLATE_KEYS = [
'USERNAME',
'SUBSCRIPTION_LINK',
'HAPP_CRYPT3_LINK',
'HAPP_CRYPT4_LINK',
] as const;
export type TSubscriptionPageTemplateKey = (typeof SUBSCRIPTION_PAGE_TEMPLATE_KEYS)[number];
10 changes: 6 additions & 4 deletions libs/subscription-page/models/subscription-page-config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,16 @@ const SubscriptionPageTranslateKeysSchema = z.object({

const BaseSettingsSchema = z
.object({
metaTitle: z.string().default('Remnawave Subscription Page'),
metaDescription: z.string().default('Remnawave Subscription Page'),
metaTitle: z.string().default('Subscription'),
metaDescription: z.string().default('Subscription'),
showConnectionKeys: z.boolean().default(false),
hideGetLinkButton: z.boolean().default(false),
})
.default({
metaTitle: 'Remnawave Subscription Page',
metaDescription: 'Remnawave Subscription Page',
metaTitle: 'Subscription',
metaDescription: 'Subscription',
showConnectionKeys: false,
hideGetLinkButton: false,
});

export const SubscriptionPageRawConfigSchema = z
Expand Down
2 changes: 1 addition & 1 deletion libs/subscription-page/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@remnawave/subscription-page-types",
"version": "0.3.3",
"version": "0.4.0",
"public": true,
"license": "AGPL-3.0-only",
"description": "A types library for Remnawave Subscription Page.",
Expand Down
Loading