Skip to content

Commit ebe0c2b

Browse files
committed
feat: API key authentication, scopes, and webhooks
- Add ApiKey model with SHA-256 hashing, single-display, scopes, IP allowlist, per-key rate limiting and audit log - Add requireScope middleware for granular permission control - Extend requireAuth to support X-API-Key / Bearer token headers - Add webhookService with HMAC-SHA256 signing, fire-and-forget delivery - Add webhook sub-document to Settings model - Emit webhooks for all user/node/sync/traffic/expiry events - Add API key management and webhook config UI to settings page - Add checkApiKeyRateLimit to cacheService (Redis INCR+EXPIRE) - Update README.md and README.ru.md with full API key and webhook docs
1 parent c426bd3 commit ebe0c2b

15 files changed

Lines changed: 1299 additions & 47 deletions

File tree

README.md

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ MONGO_PASSWORD=yourmongopassword # openssl rand -hex 16
7272
- 📱 **Subscriptions** — Auto-format for Clash, Sing-box, Shadowrocket
7373
- 🔄 **Backup/Restore** — Automatic database backups
7474
- 💻 **SSH Terminal** — Direct node access from browser
75+
- 🔑 **API Keys** — Secure external access with scopes, IP allowlist, rate limiting
76+
- 🪝 **Webhooks** — Real-time event notifications with HMAC-SHA256 signing
7577

7678
---
7779

@@ -133,6 +135,47 @@ Instead of rigid "plans", use flexible groups:
133135

134136
## 📖 API Reference
135137

138+
### API Key Authentication
139+
140+
All `/api/*` endpoints (except `/api/auth` and `/api/files`) require authentication via either an API key or an admin session cookie.
141+
142+
**Create a key:** Settings → Security → API Keys → Create Key
143+
144+
**Usage:**
145+
```http
146+
# Option 1 — header
147+
X-API-Key: ck_your_key_here
148+
149+
# Option 2 — Bearer token
150+
Authorization: Bearer ck_your_key_here
151+
```
152+
153+
#### Scopes
154+
155+
| Scope | Access |
156+
|-------|--------|
157+
| `users:read` | Read users |
158+
| `users:write` | Create / update / delete users |
159+
| `nodes:read` | Read nodes |
160+
| `nodes:write` | Create / update / delete / sync nodes |
161+
| `stats:read` | Read stats and groups |
162+
| `sync:write` | Trigger sync, kick users |
163+
164+
#### Rate Limiting
165+
166+
Each key has a configurable rate limit (default: 60 req/min).
167+
Exceeded requests return `429` with `X-RateLimit-Limit` / `X-RateLimit-Remaining` headers.
168+
169+
#### Error Responses
170+
171+
| Code | Reason |
172+
|------|--------|
173+
| `401` | Invalid, expired, or missing key |
174+
| `403` | Key valid but missing required scope / IP not in allowlist |
175+
| `429` | Rate limit exceeded |
176+
177+
---
178+
136179
### Authentication (for nodes)
137180

138181
#### POST `/api/auth`
@@ -168,18 +211,24 @@ Universal subscription endpoint. Auto-detects format by User-Agent.
168211

169212
### Users
170213

214+
Required scope: `users:read` (GET) / `users:write` (POST, PUT, DELETE)
215+
171216
| Method | Endpoint | Description |
172217
|--------|----------|-------------|
173-
| GET | `/api/users` | List users |
218+
| GET | `/api/users` | List users (pagination, filtering, sorting) |
174219
| GET | `/api/users/:userId` | Get user |
175220
| POST | `/api/users` | Create user |
176221
| PUT | `/api/users/:userId` | Update user |
177222
| DELETE | `/api/users/:userId` | Delete user |
178223
| POST | `/api/users/:userId/enable` | Enable user |
179224
| POST | `/api/users/:userId/disable` | Disable user |
225+
| POST | `/api/users/:userId/groups` | Add user to groups |
226+
| DELETE | `/api/users/:userId/groups/:groupId` | Remove user from group |
180227

181228
### Nodes
182229

230+
Required scope: `nodes:read` (GET) / `nodes:write` (POST, PUT, DELETE)
231+
183232
| Method | Endpoint | Description |
184233
|--------|----------|-------------|
185234
| GET | `/api/nodes` | List nodes |
@@ -188,13 +237,73 @@ Universal subscription endpoint. Auto-detects format by User-Agent.
188237
| PUT | `/api/nodes/:id` | Update node |
189238
| DELETE | `/api/nodes/:id` | Delete node |
190239
| GET | `/api/nodes/:id/config` | Get node config (YAML) |
240+
| POST | `/api/nodes/:id/sync` | Sync specific node |
191241
| POST | `/api/nodes/:id/update-config` | Push config via SSH |
192242

193-
### Sync
243+
### Stats & Sync
194244

195-
| Method | Endpoint | Description |
196-
|--------|----------|-------------|
197-
| POST | `/api/sync` | Sync all nodes |
245+
Required scope: `stats:read` / `sync:write`
246+
247+
| Method | Endpoint | Scope | Description |
248+
|--------|----------|-------|-------------|
249+
| GET | `/api/stats` | `stats:read` | Panel statistics |
250+
| GET | `/api/groups` | `stats:read` | List server groups |
251+
| POST | `/api/sync` | `sync:write` | Sync all nodes |
252+
| POST | `/api/kick/:userId` | `sync:write` | Kick user from all nodes |
253+
254+
---
255+
256+
## 🪝 Webhooks
257+
258+
Send real-time event notifications to any HTTP endpoint.
259+
260+
**Configure:** Settings → Security → Webhooks
261+
262+
### Request Format
263+
264+
```http
265+
POST https://your-endpoint.com/webhook
266+
Content-Type: application/json
267+
X-Webhook-Event: user.created
268+
X-Webhook-Timestamp: 1700000000
269+
X-Webhook-Signature: sha256=<hmac>
270+
User-Agent: C3-Celerity-Webhook/1.0
271+
272+
{
273+
"event": "user.created",
274+
"timestamp": "2024-01-01T00:00:00.000Z",
275+
"data": { ... }
276+
}
277+
```
278+
279+
### Signature Verification
280+
281+
```js
282+
const crypto = require('crypto');
283+
const expected = 'sha256=' + crypto
284+
.createHmac('sha256', YOUR_SECRET)
285+
.update(`${timestamp}.${rawBody}`)
286+
.digest('hex');
287+
// compare with X-Webhook-Signature header
288+
```
289+
290+
### Events
291+
292+
| Event | Trigger |
293+
|-------|---------|
294+
| `user.created` | User created |
295+
| `user.updated` | User updated |
296+
| `user.deleted` | User deleted |
297+
| `user.enabled` | User enabled |
298+
| `user.disabled` | User disabled |
299+
| `user.traffic_exceeded` | User traffic limit reached |
300+
| `user.expired` | User subscription expired |
301+
| `node.online` | Node came online |
302+
| `node.offline` | Node went offline |
303+
| `node.error` | Node sync/config error |
304+
| `sync.completed` | Full sync cycle finished |
305+
306+
Leave the events list empty to receive **all** events.
198307

199308
---
200309

README.ru.md

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ MONGO_PASSWORD=парольмонго # openssl rand -hex 16
7272
- 📱 **Подписки** — автоформаты для Clash, Sing-box, Shadowrocket
7373
- 🔄 **Бэкап/Восстановление** — автоматические бэкапы базы
7474
- 💻 **SSH-терминал** — прямой доступ к нодам из браузера
75+
- 🔑 **API-ключи** — безопасный внешний доступ со скоупами, IP-фильтром и rate limiting
76+
- 🪝 **Вебхуки** — уведомления о событиях с подписью HMAC-SHA256
7577

7678
---
7779

@@ -133,6 +135,47 @@ MONGO_PASSWORD=парольмонго # openssl rand -hex 16
133135

134136
## 📖 API
135137

138+
### Аутентификация через API-ключ
139+
140+
Все эндпоинты `/api/*` (кроме `/api/auth` и `/api/files`) требуют аутентификации — через API-ключ или cookie сессии администратора.
141+
142+
**Создать ключ:** Настройки → Безопасность → API-ключи → Создать ключ
143+
144+
**Использование:**
145+
```http
146+
# Вариант 1 — заголовок
147+
X-API-Key: ck_your_key_here
148+
149+
# Вариант 2 — Bearer токен
150+
Authorization: Bearer ck_your_key_here
151+
```
152+
153+
#### Скоупы (права доступа)
154+
155+
| Скоуп | Доступ |
156+
|-------|--------|
157+
| `users:read` | Чтение пользователей |
158+
| `users:write` | Создание / изменение / удаление пользователей |
159+
| `nodes:read` | Чтение нод |
160+
| `nodes:write` | Создание / изменение / удаление / синхронизация нод |
161+
| `stats:read` | Статистика и группы |
162+
| `sync:write` | Запуск синхронизации, кик пользователей |
163+
164+
#### Rate Limiting
165+
166+
Каждый ключ имеет настраиваемый лимит (по умолчанию: 60 req/мин).
167+
При превышении возвращается `429` с заголовками `X-RateLimit-Limit` / `X-RateLimit-Remaining`.
168+
169+
#### Коды ошибок
170+
171+
| Код | Причина |
172+
|-----|---------|
173+
| `401` | Ключ недействителен, истёк или не передан |
174+
| `403` | Ключ валиден, но нет нужного скоупа / IP не в списке |
175+
| `429` | Превышен лимит запросов |
176+
177+
---
178+
136179
### Авторизация (для нод)
137180

138181
#### POST `/api/auth`
@@ -168,18 +211,24 @@ MONGO_PASSWORD=парольмонго # openssl rand -hex 16
168211

169212
### Пользователи
170213

214+
Требуемый скоуп: `users:read` (GET) / `users:write` (POST, PUT, DELETE)
215+
171216
| Метод | Эндпоинт | Описание |
172217
|-------|----------|----------|
173-
| GET | `/api/users` | Список пользователей |
218+
| GET | `/api/users` | Список пользователей (пагинация, фильтры, сортировка) |
174219
| GET | `/api/users/:userId` | Получить пользователя |
175220
| POST | `/api/users` | Создать пользователя |
176221
| PUT | `/api/users/:userId` | Обновить пользователя |
177222
| DELETE | `/api/users/:userId` | Удалить пользователя |
178223
| POST | `/api/users/:userId/enable` | Включить |
179224
| POST | `/api/users/:userId/disable` | Отключить |
225+
| POST | `/api/users/:userId/groups` | Добавить в группы |
226+
| DELETE | `/api/users/:userId/groups/:groupId` | Удалить из группы |
180227

181228
### Ноды
182229

230+
Требуемый скоуп: `nodes:read` (GET) / `nodes:write` (POST, PUT, DELETE)
231+
183232
| Метод | Эндпоинт | Описание |
184233
|-------|----------|----------|
185234
| GET | `/api/nodes` | Список нод |
@@ -188,13 +237,73 @@ MONGO_PASSWORD=парольмонго # openssl rand -hex 16
188237
| PUT | `/api/nodes/:id` | Обновить ноду |
189238
| DELETE | `/api/nodes/:id` | Удалить ноду |
190239
| GET | `/api/nodes/:id/config` | Получить конфиг (YAML) |
240+
| POST | `/api/nodes/:id/sync` | Синхронизировать ноду |
191241
| POST | `/api/nodes/:id/update-config` | Отправить конфиг через SSH |
192242

193-
### Синхронизация
243+
### Статистика и синхронизация
194244

195-
| Метод | Эндпоинт | Описание |
196-
|-------|----------|----------|
197-
| POST | `/api/sync` | Синхронизировать все ноды |
245+
Требуемый скоуп: `stats:read` / `sync:write`
246+
247+
| Метод | Эндпоинт | Скоуп | Описание |
248+
|-------|----------|-------|----------|
249+
| GET | `/api/stats` | `stats:read` | Статистика панели |
250+
| GET | `/api/groups` | `stats:read` | Список групп серверов |
251+
| POST | `/api/sync` | `sync:write` | Синхронизировать все ноды |
252+
| POST | `/api/kick/:userId` | `sync:write` | Кикнуть пользователя со всех нод |
253+
254+
---
255+
256+
## 🪝 Вебхуки
257+
258+
Отправляйте уведомления о событиях в реальном времени на любой HTTP-эндпоинт.
259+
260+
**Настройка:** Настройки → Безопасность → Вебхуки
261+
262+
### Формат запроса
263+
264+
```http
265+
POST https://your-endpoint.com/webhook
266+
Content-Type: application/json
267+
X-Webhook-Event: user.created
268+
X-Webhook-Timestamp: 1700000000
269+
X-Webhook-Signature: sha256=<hmac>
270+
User-Agent: C3-Celerity-Webhook/1.0
271+
272+
{
273+
"event": "user.created",
274+
"timestamp": "2024-01-01T00:00:00.000Z",
275+
"data": { ... }
276+
}
277+
```
278+
279+
### Проверка подписи
280+
281+
```js
282+
const crypto = require('crypto');
283+
const expected = 'sha256=' + crypto
284+
.createHmac('sha256', YOUR_SECRET)
285+
.update(`${timestamp}.${rawBody}`)
286+
.digest('hex');
287+
// сравните с заголовком X-Webhook-Signature
288+
```
289+
290+
### События
291+
292+
| Событие | Когда |
293+
|---------|-------|
294+
| `user.created` | Создан пользователь |
295+
| `user.updated` | Обновлён пользователь |
296+
| `user.deleted` | Удалён пользователь |
297+
| `user.enabled` | Пользователь включён |
298+
| `user.disabled` | Пользователь отключён |
299+
| `user.traffic_exceeded` | Достигнут лимит трафика |
300+
| `user.expired` | Истёк срок подписки |
301+
| `node.online` | Нода перешла в онлайн |
302+
| `node.offline` | Нода ушла в оффлайн |
303+
| `node.error` | Ошибка синхронизации/конфига ноды |
304+
| `sync.completed` | Завершён полный цикл синхронизации |
305+
306+
Оставьте список событий пустым, чтобы получать **все** события.
198307

199308
---
200309

index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const { WebSocketServer } = require('ws');
1717
const config = require('./config');
1818
const logger = require('./src/utils/logger');
1919
const requireAuth = require('./src/middleware/auth');
20+
const { requireScope } = requireAuth;
2021
const { i18nMiddleware } = require('./src/middleware/i18n');
2122
const { countRequest } = require('./src/middleware/rpsCounter');
2223
const syncService = require('./src/services/syncService');
@@ -196,7 +197,7 @@ app.use('/api', subscriptionRoutes);
196197
app.use('/api/users', requireAuth, usersRoutes);
197198
app.use('/api/nodes', requireAuth, nodesRoutes);
198199

199-
app.get('/api/groups', requireAuth, async (req, res) => {
200+
app.get('/api/groups', requireAuth, requireScope('stats:read'), async (req, res) => {
200201
try {
201202
const { getActiveGroups } = require('./src/utils/helpers');
202203
const groups = await getActiveGroups();
@@ -206,7 +207,7 @@ app.get('/api/groups', requireAuth, async (req, res) => {
206207
}
207208
});
208209

209-
app.get('/api/stats', requireAuth, async (req, res) => {
210+
app.get('/api/stats', requireAuth, requireScope('stats:read'), async (req, res) => {
210211
try {
211212
const HyUser = require('./src/models/hyUserModel');
212213
const HyNode = require('./src/models/hyNodeModel');
@@ -233,7 +234,7 @@ app.get('/api/stats', requireAuth, async (req, res) => {
233234
}
234235
});
235236

236-
app.post('/api/sync', requireAuth, async (req, res) => {
237+
app.post('/api/sync', requireAuth, requireScope('sync:write'), async (req, res) => {
237238
if (syncService.isSyncing) {
238239
return res.status(409).json({ error: 'Sync already in progress' });
239240
}
@@ -245,7 +246,7 @@ app.post('/api/sync', requireAuth, async (req, res) => {
245246
res.json({ message: 'Sync started' });
246247
});
247248

248-
app.post('/api/kick/:userId', requireAuth, async (req, res) => {
249+
app.post('/api/kick/:userId', requireAuth, requireScope('sync:write'), async (req, res) => {
249250
try {
250251
await syncService.kickUser(req.params.userId);
251252
await cacheService.clearDeviceIPs(req.params.userId);

0 commit comments

Comments
 (0)