173 Commits

Author SHA1 Message Date
Home
743ca72965 feat: create certificate page 2025-10-18 18:53:38 +03:00
Home
f572b28711 feat: added template innbounding feature 2025-10-18 17:38:41 +03:00
Home
781d7439af feat: added server workflow 2025-10-11 19:24:30 +03:00
Home
894dd4da95 feat: added create server 2025-10-11 01:32:59 +03:00
Home
45c21cca82 feat: added redux & ducks 2025-10-11 00:20:28 +03:00
Home
de6f4bc6f9 fix: key in map nav-menu 2025-10-10 23:09:19 +03:00
Home
d264968cc8 fix: dependencies 2025-10-07 14:11:20 +03:00
Boris Cherepanov
1a42dc9d4c feat: fetch dashboard data 2025-10-03 01:57:47 +03:00
Boris Cherepanov
bfa2878109 feat: added nav 2025-10-03 01:10:06 +03:00
Boris Cherepanov
8472e21955 feat: added vite+react+ts 2025-10-03 00:05:32 +03:00
Ultradesu
e4984dd29d Added docker-compose 2025-09-29 11:19:11 +01:00
Ultradesu
76afa0797b Letsencrypt works 2025-09-24 00:30:03 +01:00
Ultradesu
59b8cbb582 URI works on android. Shadowsocks doesn't work on iPhone. it's ok - will be fixed. 2025-09-23 16:50:12 +01:00
Ultradesu
572b5e19c0 API works. next: generate URI 2025-09-23 14:17:32 +01:00
Ultradesu
2b5b09a213 Cert works 2025-09-21 16:38:10 +01:00
Ultradesu
0386ab4976 next: rework grpc connections pool 2025-09-19 18:31:35 +03:00
Ultradesu
f59ef73c12 Useradd works 2025-09-19 18:30:50 +03:00
Ultradesu
e20c8d69fd Useradd works 2025-09-18 16:50:47 +03:00
Ultradesu
dedb7287f7 TLS WORKS 2025-09-18 16:42:18 +03:00
Ultradesu
d5d6ebdf7b tls not working 2025-09-18 16:38:14 +03:00
Ultradesu
0e0e90c946 tls not working 2025-09-18 16:32:16 +03:00
Ultradesu
8aff8f2fb5 init rust. WIP: tls for inbounds 2025-09-18 02:56:59 +03:00
Ultradesu
777af49ebf Fixed TG messages quotes. Fixed sync tasks loop.
All checks were successful
Docker hub build / docker (push) Successful in 9m33s
2025-09-17 16:34:55 +03:00
Ultradesu
d4042435fe Fixed TG messages quotes. Fixed sync tasks loop. 2025-09-17 13:54:48 +03:00
Ultradesu
f304825836 Fixed TG messages quotes. Fixed sync tasks loop. 2025-09-17 13:34:46 +03:00
Ultradesu
c4057180b9 Fixed TG messages quotes. Fixed sync tasks loop. 2025-09-17 13:20:20 +03:00
Ultradesu
7584e80477 Added tg bot autoconfirm
All checks were successful
Docker hub build / docker (push) Successful in 5m39s
2025-08-15 17:09:31 +03:00
Ultradesu
95e0d08b51 Added tg bot autoconfirm 2025-08-15 16:33:23 +03:00
AB
57cef79748 Approve reworked 2025-08-15 15:37:58 +03:00
Ultradesu
9158e330e5 Added TG bot
All checks were successful
Docker hub build / docker (push) Successful in 7m48s
2025-08-15 05:28:21 +03:00
Ultradesu
14590aaddc Added TG bot 2025-08-15 05:15:13 +03:00
Ultradesu
afd7ad2b28 Added TG bot 2025-08-15 04:53:30 +03:00
Ultradesu
36f9e495b5 Added TG bot 2025-08-15 04:02:22 +03:00
AB from home.homenet
402e4d84fc Fixed sync user tasks .
All checks were successful
Docker hub build / docker (push) Successful in 5m36s
2025-08-08 14:35:19 +03:00
AB from home.homenet
c148bb99dc Fixed multiuser outline and xray . 2025-08-08 14:24:05 +03:00
Alexandr Bogomyakov
dcad41711e Update README.md 2025-08-08 12:46:31 +03:00
AB from home.homenet
05465f9595 Fixed multiuser outline and xray . 2025-08-08 12:41:33 +03:00
AB from home.homenet
4c32679d86 Fixed cert generation .
All checks were successful
Docker hub build / docker (push) Successful in 5m43s
2025-08-08 10:32:14 +03:00
AB from home.homenet
397e05b3cc Fixed cert generation . 2025-08-08 09:08:18 +03:00
AB from home.homenet
99b79c38a0 Fixed xray grps user update . 2025-08-08 08:48:56 +03:00
AB from home.homenet
042ce6bd3f Xray works. 2025-08-08 08:35:47 +03:00
AB from home.homenet
9363bd4db8 Xray works. 2025-08-08 07:47:23 +03:00
AB from home.homenet
2fe59062c9 Xray works. 2025-08-08 07:39:01 +03:00
AB from home.homenet
fe56811b33 Xray works. fixed certs. 2025-08-08 06:50:04 +03:00
AB from home.homenet
787432cbcf Xray works 2025-08-08 05:46:36 +03:00
AB from home.homenet
56b0b160e3 Fixed sub links generation
All checks were successful
Docker hub build / docker (push) Successful in 6m12s
2025-08-05 01:50:11 +03:00
AB from home.homenet
1f7953a74c Fixed sub links generation 2025-08-05 01:40:10 +03:00
AB from home.homenet
ea3d74ccbd Xray init support 2025-08-05 01:23:07 +03:00
Alexandr Bogomiakov
c5a94d17dc Added initial xray plugin support 2025-07-27 20:37:21 +03:00
Ultradesu
17f9f5c045 Improve server page
All checks were successful
Docker hub build / docker (push) Successful in 3m40s
2025-07-21 18:55:59 +03:00
Ultradesu
4f7131ff5a Improve server page 2025-07-21 18:26:29 +03:00
Ultradesu
fa7ec5a87e Improve server page
All checks were successful
Docker hub build / docker (push) Successful in 3m37s
2025-07-21 17:40:03 +03:00
Ultradesu
05d19b88af Improve CI 2025-07-21 17:20:08 +03:00
Ultradesu
a75d55ac9d Added outline server managment page template 2025-07-21 17:15:35 +03:00
Ultradesu
90001a1d1e management command for cleanup old access logs 2025-07-21 15:30:57 +03:00
Ultradesu
9325a94cb2 Merged user statistics and acl manager 2025-07-21 14:40:52 +03:00
Ultradesu
8854aacf88 Fixed migrations 2025-07-21 13:55:49 +03:00
Ultradesu
3f346bc6c6 Added statistics cache 2025-07-21 13:49:43 +03:00
Ultradesu
b4bdffbbe3 Added statistics cache 2025-07-21 13:30:09 +03:00
Ultradesu
f5e5298461 Added statistics cache 2025-07-21 13:23:10 +03:00
Ultradesu
243a6734fd Improve acl model 2025-07-21 12:47:47 +03:00
Ultradesu
df5493bf14 Improve acl model 2025-07-21 12:25:37 +03:00
Ultradesu
ba62e214ce Improve acl model 2025-07-21 12:12:31 +03:00
Ultradesu
664bafe067 Added indexes to logs
All checks were successful
Docker hub build / docker (push) Successful in 3m18s
2025-07-21 04:27:29 +03:00
Ultradesu
6d56eb7eab Added indexes to logs 2025-07-21 04:18:27 +03:00
Ultradesu
47572d64c6 Force sync and purge 2025-07-21 03:48:35 +03:00
Ultradesu
8a521dc12e Force sync and purge 2025-07-21 03:32:37 +03:00
Ultradesu
a938dde77c Fixed UI graph 2025-07-21 02:14:38 +03:00
Ultradesu
7efe87c1d2 Added UI features
All checks were successful
Docker hub build / docker (push) Successful in 5m11s
2025-07-21 00:52:45 +03:00
Ultradesu
67f1c4d147 Fix docker compose 2025-07-20 23:35:14 +03:00
Ultradesu
2d4c862c5e Added User UI 2025-07-20 23:32:56 +03:00
Ultradesu
9bd4896040 Added User UI 2025-07-20 23:04:58 +03:00
Ultradesu
ec869b2974 fixed tasks log 2025-07-20 22:55:07 +03:00
Ultradesu
dc6d170f08 Fixed last release 2025-07-20 22:50:22 +03:00
Ultradesu
42a923799b Fixed last release 2025-07-20 22:30:04 +03:00
Alexandr Bogomyakov
9c8f0463a5 Create SECURITY.md
All checks were successful
Docker hub build / docker (push) Successful in 3m30s
2025-06-27 21:24:42 +03:00
Ultradesu
d57232ac98 Added move clients feature 2025-06-27 17:23:05 +03:00
Ultradesu
664c2b5ec4 Added move clients feature 2025-06-27 17:21:54 +03:00
Ultradesu
281b8270ce Added move clients feature 2025-06-27 17:08:32 +03:00
Ultradesu
10b5e5f86a Added move clients feature 2025-06-27 16:36:02 +03:00
Ultradesu
20e322a2e8 Added move clients feature
All checks were successful
Docker hub build / docker (push) Successful in 9m13s
2025-06-27 16:20:31 +03:00
Ultradesu
e77d13ab4e Added move clients feature 2025-06-27 16:02:13 +03:00
Ultradesu
cb9be75e90 Disable unused menus
All checks were successful
Docker hub build / docker (push) Successful in 12m5s
2025-06-20 11:35:29 +01:00
Ultradesu
8e378cb787 Fixed text search fileds for ACL and Logs. Added version info to footer. 2025-06-20 11:30:56 +01:00
Alexandr Bogomyakov
bf4bc505de Update README.md
All checks were successful
Docker hub build / docker (push) Successful in 4m38s
2025-06-16 14:32:58 +01:00
Alexandr Bogomyakov
c4dc0a1b42 Update README.md 2025-06-16 14:30:02 +01:00
Alexandr Bogomyakov
e22b26b1aa Update Dockerfile
All checks were successful
Docker hub build / docker (push) Successful in 2m51s
2025-03-17 15:09:36 +02:00
Alexandr Bogomyakov
8d8d6bb671 Update Dockerfile
Some checks failed
Docker hub build / docker (push) Failing after 22s
2025-03-17 15:04:59 +02:00
Alexandr Bogomyakov
c6da8ea250 Update Dockerfile
Some checks failed
Docker hub build / docker (push) Failing after 24s
2025-03-17 15:03:39 +02:00
Alexandr Bogomyakov
26a94d0e72 Update Dockerfile
Some checks failed
Docker hub build / docker (push) Failing after 24s
2025-03-17 15:00:52 +02:00
Alexandr Bogomyakov
72f59563f5 Bump python base image
All checks were successful
Docker hub build / docker (push) Successful in 2m54s
2025-03-17 14:42:10 +02:00
Ultradesu
fbf5019c32 Adjust server info
All checks were successful
Docker hub build / docker (push) Successful in 3m4s
2025-03-13 02:36:04 +02:00
Ultradesu
53dcc29dc7 Bump django 2025-03-13 01:54:35 +02:00
Ultradesu
89eee8fe3e Bump django 2025-03-13 01:49:35 +02:00
Ultradesu
7c47a3935a Bump django 2025-03-13 01:44:31 +02:00
Ultradesu
43c86e2075 Fix key ids 2025-03-13 01:43:33 +02:00
Ultradesu
ed8bfe7f06 Fixed client names in outline
Some checks are pending
Docker hub build / docker (push) Waiting to run
2025-03-12 23:18:17 +02:00
Ultradesu
ca463fe5ab Added link generator. Added link to /stat/ json object
Some checks failed
Docker hub build / docker (push) Has been cancelled
2025-03-04 17:19:13 +00:00
Alexandr Bogomyakov
8f51b4cf9e Update README.md 2025-03-04 11:40:57 +00:00
Ultradesu
760c1c7647 Bump deps
Some checks failed
Docker hub build / docker (push) Has been cancelled
2025-02-25 13:44:07 +02:00
Ultradesu
d1908e879b Added user dashboard 2025-02-25 12:39:08 +02:00
A B
c24c35f443 Fixed content type
Some checks failed
Docker hub build / docker (push) Has been cancelled
2025-02-23 21:29:03 +00:00
A B
7527ddfcb9 Fixed content type 2025-02-23 21:22:12 +00:00
A B
d9bf110ba9 Fixed content type 2025-02-23 20:19:26 +00:00
A B
e3682fd121 Added json lib 2025-02-23 19:33:54 +00:00
A B
d02377a270 Added yaml lib 2025-02-23 19:27:53 +00:00
A B
35e3980487 Added yaml lib 2025-02-23 19:25:22 +00:00
A B
f139e0bcc6 Some fix 2025-02-23 19:18:23 +00:00
AB
b22477b3e2 Added comment beside of links
Some checks failed
Docker hub build / docker (push) Has been cancelled
2025-01-15 15:34:30 +02:00
ultradesu
2323151242 Fix access log filter
Some checks failed
Docker hub build / docker (push) Has been cancelled
2025-01-10 11:32:30 +00:00
ultradesu
2ca317a9a2 Added ACLLink comments.
Some checks are pending
Docker hub build / docker (push) Waiting to run
2025-01-09 17:24:23 +00:00
Alexandr Bogomyakov
826827f85e Update README.md 2025-01-09 16:47:20 +00:00
Alexandr Bogomiakov
9763a6e48d Added to run local copy 2025-01-09 16:37:09 +00:00
Alexandr Bogomiakov
4fad180ec9 Adjusted access links in admin interface
Some checks are pending
Docker hub build / docker (push) Waiting to run
2025-01-09 08:49:52 +00:00
Alexandr Bogomyakov
7b5f47fa64 Update Dockerfile
Some checks failed
Docker hub build / docker (push) Has been cancelled
2024-11-20 13:35:56 +02:00
A B
a790da0793 Adjust ACLLinks length. Added links generator
Some checks failed
Docker hub build / docker (push) Has been cancelled
2024-11-18 20:34:54 +00:00
A B
a8ddadbe6d Fix autolink creation
Some checks failed
Docker hub build / docker (push) Has been cancelled
2024-10-28 17:32:06 +00:00
A B
6710cf211c Fix user hash editable 2024-10-28 17:21:23 +00:00
A B
0880401cc4 Added access logs. 2024-10-28 17:15:49 +00:00
A B
7cf99af20d Autologin
Some checks are pending
Docker hub build / docker (push) Waiting to run
2024-10-28 00:06:35 +00:00
A B
b6ad6e8578 Trying remote-auth 2024-10-27 23:37:02 +00:00
A B
7585fb94a1 Fix users
Some checks are pending
Docker hub build / docker (push) Waiting to run
2024-10-27 01:18:06 +00:00
A B
d324edec69 Merge vpn.Users with Django Users 2024-10-27 01:06:37 +00:00
A B
dda9b4ba5a Added keys count on outline page. 2024-10-26 23:36:18 +00:00
A B
75126b09ff Fix
Some checks are pending
Docker hub build / docker (push) Waiting to run
2024-10-26 12:38:50 +00:00
A B
a1ff998b68 Fix 2024-10-26 12:22:19 +00:00
A B
c4d9254824 Update task
Some checks failed
Docker hub build / docker (push) Has been cancelled
2024-10-21 20:36:30 +00:00
A B
2c74667945 Fix beat import to dev
Some checks are pending
Docker hub build / docker (push) Waiting to run
2024-10-21 13:26:22 +00:00
A B
538bc2f65e Fixed tasks 2024-10-21 13:22:03 +00:00
ab
bc5f774d9f fix ci 2024-10-20 22:43:31 +00:00
ab
7bf998ece5 Django UI 2024-10-20 21:57:12 +00:00
Alexandr Bogomyakov
9680ce802d Update README.md
Some checks failed
Docker hub build / docker (push) Has been cancelled
2024-09-28 22:04:27 +03:00
Alexandr Bogomyakov
e4fd6ea5d7 Update README.md 2024-09-28 22:03:07 +03:00
Alexandr Bogomyakov
22cca991fc Update README.md 2024-09-28 21:50:23 +03:00
Alexandr Bogomyakov
dd5f0c4e2f Update windows-helper.ps1 2024-09-28 21:37:42 +03:00
Alexandr Bogomyakov
98d993423d Update windows-helper.ps1 2024-09-28 21:19:42 +03:00
Alexandr Bogomyakov
db382f2b27 Rename windows_task.ps1 to tools/windows_task.ps1 2024-09-28 21:19:28 +03:00
Alexandr Bogomyakov
7e08bd465b Create windows-helper.ps1 2024-09-28 21:19:09 +03:00
Alexandr Bogomyakov
f7ce671427 Update windows_task.ps1 2024-09-28 21:17:34 +03:00
Alexandr Bogomyakov
dceb07137a Update windows_task.ps1 2024-09-28 20:44:08 +03:00
Alexandr Bogomyakov
e41febe061 Update windows_task.ps1 2024-09-28 20:42:36 +03:00
Alexandr Bogomyakov
2397a05a08 Update windows_task.ps1 2024-09-28 20:36:27 +03:00
Alexandr Bogomyakov
c940e9f38b Update windows_task.ps1 2024-09-28 20:31:25 +03:00
Alexandr Bogomyakov
315be97354 Update windows_task.ps1 2024-09-28 19:53:01 +03:00
Alexandr Bogomyakov
8a5e1d2d69 Update and rename windows_service.ps1 to windows_task.ps1 2024-09-28 19:32:30 +03:00
Alexandr Bogomyakov
22eb5ec7af Create windows_service.ps1 2024-09-28 18:15:43 +03:00
Alexandr Bogomyakov
3da1d4f5f7 Bump version 2024-06-12 12:21:03 +03:00
Alexandr Bogomyakov
c8dcd4439c Fix logging 2024-06-12 12:20:09 +03:00
Alexandr Bogomyakov
b01d86251c fix logging 2024-06-12 12:08:41 +03:00
AB
58be345610 New UI fix
Co-authored-by: XakPlant <xakplant@users.noreply.github.com>
2024-04-28 15:06:58 +03:00
AB
48521cb8a3 New UI
Co-authored-by: XakPlant <xakplant@users.noreply.github.com>
2024-04-28 13:15:07 +03:00
35f57de110 Reworked dynamic keys link generation. 2024-04-23 19:30:14 +03:00
423c408893 new links 2024-04-19 20:10:31 +03:00
Alexandr Bogomyakov
788797f3ef Merge pull request #10 from Sanapach/master
Outfleet::fix empty config
2024-04-17 20:16:39 +03:00
c9ae1bbbbd Outfleet::fix empty config 2024-04-17 20:15:09 +03:00
5cc32b18af Outfleet::fix empty config 2024-04-17 20:13:10 +03:00
Alexandr Bogomyakov
f6bcb42ec4 Bump version 2024-03-24 18:07:23 +02:00
Alexandr Bogomyakov
bae0b91bab Merge pull request #7 from Sanapach/master
Outfleet::log fixing
2024-03-24 18:04:18 +02:00
mmilavkin
ab6d53a837 Outfleet::log fixing::2 2024-03-24 18:02:51 +02:00
mmilavkin
9709d2f029 Outfleet::log fixing 2024-03-24 17:58:54 +02:00
e818d63cad fix k8s things 2024-03-19 02:47:29 +02:00
2039654f12 fix k8s things 2024-03-19 01:44:38 +02:00
f82631b174 fix k8s things 2024-03-19 01:11:08 +02:00
77b78ec751 fix k8s things 2024-03-18 22:53:43 +02:00
f6a728ef1a fix k8s things 2024-03-18 22:08:43 +02:00
5c1ffcbdc3 fix k8s things 2024-03-18 21:30:34 +02:00
f6c3262fb8 fix V1 api define 2024-03-18 20:42:14 +02:00
607730e781 fix V1 api define 2024-03-18 20:36:07 +02:00
0e7cabe336 fix V1 api define 2024-03-18 20:02:33 +02:00
614140840d Fix k8s exception 2024-03-18 19:50:24 +02:00
Alexandr Bogomyakov
3a6a60032e Update README.md 2024-03-18 19:39:57 +02:00
263acf540d Fix docker build 2024-03-18 19:21:25 +02:00
Alexandr Bogomyakov
443198aad1 Merge pull request #6 from house-of-vanity/k8s
K8s
2024-03-18 19:12:05 +02:00
203 changed files with 32886 additions and 1615 deletions

31
.env.example Normal file
View File

@@ -0,0 +1,31 @@
# Environment Variables Example for Xray Admin Panel
# Copy this file to .env and modify the values as needed
# Database Configuration
DATABASE_URL=postgresql://xray_admin:password@localhost:5432/xray_admin
XRAY_ADMIN__DATABASE__MAX_CONNECTIONS=20
XRAY_ADMIN__DATABASE__CONNECTION_TIMEOUT=30
XRAY_ADMIN__DATABASE__AUTO_MIGRATE=true
# Web Server Configuration
XRAY_ADMIN__WEB__HOST=0.0.0.0
XRAY_ADMIN__WEB__PORT=8080
XRAY_ADMIN__WEB__JWT_SECRET=your-super-secret-jwt-key-change-this
XRAY_ADMIN__WEB__JWT_EXPIRY=86400
# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi
XRAY_ADMIN__TELEGRAM__WEBHOOK_URL=https://your-domain.com/telegram/webhook
# Xray Configuration
XRAY_ADMIN__XRAY__DEFAULT_API_PORT=62789
XRAY_ADMIN__XRAY__HEALTH_CHECK_INTERVAL=30
# Logging Configuration
XRAY_ADMIN__LOGGING__LEVEL=info
XRAY_ADMIN__LOGGING__FILE_PATH=./logs/xray-admin.log
XRAY_ADMIN__LOGGING__JSON_FORMAT=false
# Runtime Environment
RUST_ENV=development
ENVIRONMENT=development

View File

@@ -1,36 +0,0 @@
name: Docker hub build
on:
push:
branches:
- 'master'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set outputs
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Check outputs
run: echo ${{ steps.vars.outputs.sha_short }}
-
name: Build and push
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ultradesu/outfleet:latest,ultradesu/outfleet:${{ steps.vars.outputs.sha_short }}

15
.gitignore vendored
View File

@@ -1,9 +1,10 @@
config.yaml
__pycache__/
sync.log
main.py
.idea/*
.vscode/*
*.swp
*.swo
*.swn
/target/
config.toml
# macOS system files
._*
.DS_Store

507
API.md Normal file
View File

@@ -0,0 +1,507 @@
# OutFleet Xray Admin API
Base URL: `http://localhost:8080`
## Overview
Complete API documentation for OutFleet - a web admin panel for managing xray-core VPN proxy servers.
## Base Endpoints
### Health Check
- `GET /health` - Service health check
**Response:**
```json
{
"status": "ok",
"service": "xray-admin",
"version": "0.1.0"
}
```
## API Endpoints
All API endpoints are prefixed with `/api`.
### Users
#### List Users
- `GET /users?page=1&per_page=20` - Get paginated list of users
#### Search Users
- `GET /users/search?q=john` - Universal search for users
**Search capabilities:**
- By name (partial match, case-insensitive): `?q=john`
- By telegram_id: `?q=123456789`
- By user UUID: `?q=550e8400-e29b-41d4-a716-446655440000`
**Response:** Array of user objects (limited to 100 results)
```json
[
{
"id": "uuid",
"name": "string",
"comment": "string|null",
"telegram_id": "number|null",
"created_at": "timestamp",
"updated_at": "timestamp"
}
]
```
#### Get User
- `GET /users/{id}` - Get user by ID
#### Create User
- `POST /users` - Create new user
```json
{
"name": "John Doe",
"comment": "Admin user",
"telegram_id": 123456789
}
```
#### Update User
- `PUT /users/{id}` - Update user by ID
```json
{
"name": "Jane Doe",
"comment": null,
"telegram_id": 987654321
}
```
#### Delete User
- `DELETE /users/{id}` - Delete user by ID
#### Get User Access
- `GET /users/{id}/access?include_uris=true` - Get user access to inbounds (optionally with client URIs)
**Query Parameters:**
- `include_uris`: boolean (optional) - Include client configuration URIs in response
**Response (without URIs):**
```json
[
{
"id": "uuid",
"user_id": "uuid",
"server_inbound_id": "uuid",
"xray_user_id": "string",
"level": 0,
"is_active": true
}
]
```
**Response (with URIs):**
```json
[
{
"id": "uuid",
"user_id": "uuid",
"server_inbound_id": "uuid",
"xray_user_id": "string",
"level": 0,
"is_active": true,
"uri": "vless://uuid@hostname:port?parameters#alias",
"protocol": "vless",
"server_name": "Server Name",
"inbound_tag": "inbound-tag"
}
]
```
#### Generate Client Configurations
- `GET /users/{user_id}/configs` - Get all client configuration URIs for a user
- `GET /users/{user_id}/access/{inbound_id}/config` - Get specific client configuration URI
**Response:**
```json
{
"user_id": "uuid",
"server_name": "string",
"inbound_tag": "string",
"protocol": "vmess|vless|trojan|shadowsocks",
"uri": "protocol://uri_string",
"qr_code": null
}
```
### Servers
#### List Servers
- `GET /servers` - Get all servers
**Response:**
```json
[
{
"id": "uuid",
"name": "string",
"hostname": "string",
"grpc_hostname": "string",
"grpc_port": 2053,
"status": "online|offline|error|unknown",
"default_certificate_id": "uuid|null",
"created_at": "timestamp",
"updated_at": "timestamp",
"has_credentials": true
}
]
```
#### Get Server
- `GET /servers/{id}` - Get server by ID
#### Create Server
- `POST /servers` - Create new server
**Request:**
```json
{
"name": "Server Name",
"hostname": "server.example.com",
"grpc_hostname": "192.168.1.100", // optional, defaults to hostname
"grpc_port": 2053, // optional, defaults to 2053
"api_credentials": "optional credentials",
"default_certificate_id": "uuid" // optional
}
```
#### Update Server
- `PUT /servers/{id}` - Update server
**Request:** (all fields optional)
```json
{
"name": "New Server Name",
"hostname": "new.server.com",
"grpc_hostname": "192.168.1.200",
"grpc_port": 2054,
"api_credentials": "new credentials",
"status": "online",
"default_certificate_id": "uuid"
}
```
#### Delete Server
- `DELETE /servers/{id}` - Delete server
#### Test Server Connection
- `POST /servers/{id}/test` - Test connection to server
**Response:**
```json
{
"connected": true,
"endpoint": "192.168.1.100:2053"
}
```
#### Get Server Statistics
- `GET /servers/{id}/stats` - Get server statistics
### Server Inbounds
#### List Server Inbounds
- `GET /servers/{server_id}/inbounds` - Get all inbounds for a server
**Response:**
```json
[
{
"id": "uuid",
"server_id": "uuid",
"template_id": "uuid",
"template_name": "string",
"tag": "string",
"port_override": 8080,
"certificate_id": "uuid|null",
"variable_values": {},
"is_active": true,
"created_at": "timestamp",
"updated_at": "timestamp"
}
]
```
#### Get Server Inbound
- `GET /servers/{server_id}/inbounds/{inbound_id}` - Get specific inbound
#### Create Server Inbound
- `POST /servers/{server_id}/inbounds` - Create new inbound for server
**Request:**
```json
{
"template_id": "uuid",
"port": 8080,
"certificate_id": "uuid", // optional
"is_active": true
}
```
#### Update Server Inbound
- `PUT /servers/{server_id}/inbounds/{inbound_id}` - Update inbound
**Request:** (all fields optional)
```json
{
"tag": "new-tag",
"port_override": 8081,
"certificate_id": "uuid",
"variable_values": {"domain": "example.com"},
"is_active": false
}
```
#### Delete Server Inbound
- `DELETE /servers/{server_id}/inbounds/{inbound_id}` - Delete inbound
### User-Inbound Management
#### Add User to Inbound
- `POST /servers/{server_id}/inbounds/{inbound_id}/users` - Grant user access to inbound
**Request:**
```json
{
"user_id": "uuid", // optional, will create new user if not provided
"name": "username",
"comment": "User description", // optional
"telegram_id": 123456789, // optional
"level": 0 // optional, defaults to 0
}
```
#### Remove User from Inbound
- `DELETE /servers/{server_id}/inbounds/{inbound_id}/users/{email}` - Remove user access
#### Get Inbound Client Configurations
- `GET /servers/{server_id}/inbounds/{inbound_id}/configs` - Get all client configuration URIs for an inbound
**Response:**
```json
[
{
"user_id": "uuid",
"server_name": "string",
"inbound_tag": "string",
"protocol": "vmess|vless|trojan|shadowsocks",
"uri": "protocol://uri_string",
"qr_code": null
}
]
```
### Certificates
#### List Certificates
- `GET /certificates` - Get all certificates
#### Get Certificate
- `GET /certificates/{id}` - Get certificate by ID
#### Create Certificate
- `POST /certificates` - Create new certificate
**Request:**
```json
{
"name": "Certificate Name",
"cert_type": "self_signed|letsencrypt",
"domain": "example.com",
"auto_renew": true,
"certificate_pem": "-----BEGIN CERTIFICATE-----...",
"private_key": "-----BEGIN PRIVATE KEY-----..."
}
```
#### Update Certificate
- `PUT /certificates/{id}` - Update certificate
**Request:** (all fields optional)
```json
{
"name": "New Certificate Name",
"auto_renew": false
}
```
#### Delete Certificate
- `DELETE /certificates/{id}` - Delete certificate
#### Get Certificate Details
- `GET /certificates/{id}/details` - Get detailed certificate information
#### Get Expiring Certificates
- `GET /certificates/expiring` - Get certificates that are expiring soon
### Templates
#### List Templates
- `GET /templates` - Get all inbound templates
#### Get Template
- `GET /templates/{id}` - Get template by ID
#### Create Template
- `POST /templates` - Create new inbound template
**Request:**
```json
{
"name": "Template Name",
"protocol": "vmess|vless|trojan|shadowsocks",
"default_port": 8080,
"requires_tls": true,
"config_template": "JSON template string"
}
```
#### Update Template
- `PUT /templates/{id}` - Update template
**Request:** (all fields optional)
```json
{
"name": "New Template Name",
"description": "Template description",
"default_port": 8081,
"base_settings": {},
"stream_settings": {},
"requires_tls": false,
"requires_domain": true,
"variables": [],
"is_active": true
}
```
#### Delete Template
- `DELETE /templates/{id}` - Delete template
## Response Format
### User Object
```json
{
"id": "uuid",
"name": "string",
"comment": "string|null",
"telegram_id": "number|null",
"created_at": "timestamp",
"updated_at": "timestamp"
}
```
### Users List Response
```json
{
"users": [UserObject],
"total": 100,
"page": 1,
"per_page": 20
}
```
### Server Object
```json
{
"id": "uuid",
"name": "string",
"hostname": "string",
"grpc_hostname": "string",
"grpc_port": 2053,
"status": "online|offline|error|unknown",
"default_certificate_id": "uuid|null",
"created_at": "timestamp",
"updated_at": "timestamp",
"has_credentials": true
}
```
### Server Inbound Object
```json
{
"id": "uuid",
"server_id": "uuid",
"template_id": "uuid",
"template_name": "string",
"tag": "string",
"port_override": 8080,
"certificate_id": "uuid|null",
"variable_values": {},
"is_active": true,
"created_at": "timestamp",
"updated_at": "timestamp"
}
```
### Certificate Object
```json
{
"id": "uuid",
"name": "string",
"cert_type": "self_signed|letsencrypt",
"domain": "string",
"auto_renew": true,
"expires_at": "timestamp|null",
"created_at": "timestamp",
"updated_at": "timestamp"
}
```
### Template Object
```json
{
"id": "uuid",
"name": "string",
"description": "string|null",
"protocol": "vmess|vless|trojan|shadowsocks",
"default_port": 8080,
"base_settings": {},
"stream_settings": {},
"requires_tls": true,
"requires_domain": false,
"variables": [],
"is_active": true,
"created_at": "timestamp",
"updated_at": "timestamp"
}
```
### Inbound User Object
```json
{
"id": "uuid",
"user_id": "uuid",
"server_inbound_id": "uuid",
"xray_user_id": "string",
"password": "string|null",
"level": 0,
"is_active": true,
"created_at": "timestamp",
"updated_at": "timestamp"
}
```
## Status Codes
- `200` - Success
- `201` - Created
- `204` - No Content (successful deletion)
- `400` - Bad Request (invalid data)
- `404` - Not Found
- `409` - Conflict (duplicate data, e.g. telegram_id)
- `500` - Internal Server Error
## Error Response Format
```json
{
"error": "Error message description",
"code": "ERROR_CODE",
"details": "Additional error details"
}
```

5063
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

69
Cargo.toml Normal file
View File

@@ -0,0 +1,69 @@
[package]
name = "xray-admin"
version = "0.1.0"
edition = "2021"
[dependencies]
# Async runtime
tokio = { version = "1.0", features = ["full"] }
tokio-cron-scheduler = "0.10"
# Serialization/deserialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
toml = "0.8"
# Configuration
config = "0.14"
clap = { version = "4.0", features = ["derive", "env"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Utilities
anyhow = "1.0"
thiserror = "1.0"
# Validation
validator = { version = "0.18", features = ["derive"] }
# URL parsing
url = "2.5"
# Database and ORM
sea-orm = { version = "1.0", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] }
sea-orm-migration = "1.0"
# Additional utilities
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
async-trait = "0.1"
log = "0.4"
urlencoding = "2.1"
rand = "0.8"
# Web server
axum = { version = "0.7", features = ["macros", "json"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs"] }
hyper = { version = "1.0", features = ["full"] }
# Xray integration
xray-core = "0.2.1" # gRPC client for Xray
tonic = "0.12" # gRPC client/server framework
prost = "0.13" # Protocol Buffers implementation
rcgen = { version = "0.12", features = ["pem"] } # For self-signed certificates
time = "0.3" # For certificate date/time handling
base64 = "0.21" # For PEM to DER conversion
# ACME/Let's Encrypt support
instant-acme = "0.8" # ACME client for Let's Encrypt
reqwest = { version = "0.11", features = ["json", "rustls-tls"] } # HTTP client for Cloudflare API
rustls = { version = "0.23", features = ["aws-lc-rs"] } # TLS library with aws-lc-rs crypto provider
ring = "0.17" # Crypto for ACME
pem = "3.0" # PEM format support
[dev-dependencies]
tempfile = "3.0"

49
Dockerfile Executable file → Normal file
View File

@@ -1,14 +1,45 @@
FROM python:3-alpine
# Build stage
FROM rust:latest as builder
WORKDIR /app
COPY requirements.txt .
COPY static static
COPY templates templates
COPY main.py .
COPY lib.py .
# Install system dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
# Copy dependency files
COPY Cargo.toml Cargo.lock ./
EXPOSE 5000
CMD ["python", "main.py"]
# Copy source code
COPY src ./src
COPY static ./static
# Build the application
RUN cargo build --release
# Runtime stage
FROM ubuntu:24.04
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# Copy the binary from builder
COPY --from=builder /app/target/release/xray-admin /app/xray-admin
# Copy static files
COPY --from=builder /app/static ./static
# Copy config file
COPY config.docker.toml ./config.toml
EXPOSE 8081
CMD ["/app/xray-admin", "--host", "0.0.0.0"]

13
LICENSE
View File

@@ -1,13 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

132
LLM_PROJECT_CONTEXT.md Normal file
View File

@@ -0,0 +1,132 @@
# LLM Project Context - Xray Admin Panel
## Project Overview
Rust-based administration panel for managing xray-core VPN proxy servers. Uses real gRPC integration with xray-core library for server communication.
## Current Architecture
### Core Technologies
- **Language**: Rust (edition 2021)
- **Web Framework**: Axum with tower-http
- **Database**: PostgreSQL with Sea-ORM
- **Xray Integration**: xray-core 0.2.1 library with real gRPC communication
- **Frontend**: Vanilla HTML/CSS/JS with toast notifications
### Module Structure
```
src/
├── config/ # Configuration management (args, env, file)
├── database/ # Sea-ORM entities, repositories, migrations
├── services/ # Business logic (xray gRPC client, certificates)
├── web/ # Axum handlers and routes
└── main.rs # Application entry point
```
## Key Features Implemented
### 1. Database Entities
- **Users**: Basic user management
- **Servers**: Xray server definitions with gRPC endpoints
- **Certificates**: TLS certificates with PEM storage (binary format)
- **InboundTemplates**: Reusable inbound configurations
- **ServerInbounds**: Template bindings to servers with ports/certificates
### 2. Xray gRPC Integration
**Location**: `src/services/xray/client.rs`
- Real xray-core library integration (NOT mock/CLI)
- Methods: `add_inbound_with_certificate()`, `remove_inbound()`, `get_stats()`
- **CRITICAL**: TLS certificate configuration via streamSettings with proper protobuf messages
- Supports VLESS, VMess, Trojan, Shadowsocks protocols
### 3. Certificate Management
**Location**: `src/database/entities/certificate.rs`
- Self-signed certificate generation using rcgen
- Binary storage (cert_data, key_data as Vec<u8>)
- PEM conversion methods: `certificate_pem()`, `private_key_pem()`
- Separate endpoints: `/certificates/{id}` (basic) and `/certificates/{id}/details` (with PEM)
### 4. Template-Based Architecture
Templates define reusable inbound configurations that can be bound to servers with:
- Port overrides
- Certificate assignments
- Active/inactive states
## Current Status & Issues
### ✅ Working Features
- Complete CRUD for all entities
- Real xray gRPC communication with TLS certificate support
- Toast notification system (absolute positioning)
- Modal-based editing interface
- Password masking in database URL logging
- Certificate details display with PEM content
### 🔧 Recent Fixes
- **StreamConfig Integration**: Fixed TLS certificate configuration in xray gRPC calls
- **Certificate Display**: Added `/certificates/{id}/details` endpoint for PEM viewing
- **Active/Inactive Management**: Inbounds automatically added/removed from xray when toggled
### ⚠️ Current Issue
User reported certificate details still showing "Not available" - this was just fixed with the new `/certificates/{id}/details` endpoint.
## API Structure
### Endpoints
```
/api/users/* # User management
/api/servers/* # Server management
/api/servers/{id}/inbounds/* # Server inbound management
/api/certificates/* # Certificate management (basic)
/api/certificates/{id}/details # Certificate details with PEM
/api/templates/* # Template management
```
## Configuration
- **Default port**: 8080 (user tested on 8082)
- **Database**: PostgreSQL with auto-migration
- **Environment variables**: XRAY_ADMIN__* prefix
- **Config file**: config.toml support
## Testing Commands
```bash
# Run application
cargo run -- --host 0.0.0.0 --port 8082
# Test xray integration
xray api lsi --server 100.91.97.36:10085
# Check compilation
cargo check
```
## Key Implementation Details
### Xray TLS Configuration
**Location**: `src/services/xray/client.rs:185-194`
```rust
let stream_config = StreamConfig {
protocol_name: "tcp".to_string(),
security_type: "tls".to_string(),
security_settings: vec![tls_message],
// ... other fields
};
```
### Certificate Data Flow
1. User creates certificate via web interface
2. PEM data stored as binary in database (cert_data, key_data)
3. When creating inbound, certificate fetched and converted back to PEM
4. PEM passed to xray gRPC client for TLS configuration
### Database Migrations
Auto-migration enabled by default. All entities use UUID primary keys with timestamps.
## Development Notes
- **User prefers English in code/comments**
- **No emoji usage unless explicitly requested**
- **Prefer editing existing files over creating new ones**
- **Real xray-core integration required** (user specifically asked not to abandon it)
- **Application tested with actual xray server at 100.91.97.36:10085**
## Last Working State
All features implemented and compiling. StreamConfig properly configured for TLS certificate transmission to xray servers. Certificate viewing endpoint fixed for PEM display.

View File

@@ -1,89 +0,0 @@
<p align="center">
<h1 align="center">OutFleet: Master Your OutLine VPN</h1>
<p align="center">
Streamline OutLine VPN experience. OutFleet offers centralized key control for many servers and always-updated Dynamic Access Keys instead of ss:// links
<br/>
<br/>
<a href="https://github.com/house-of-vanity/outfleet/issues">Request Feature</a>
</p>
</p>
![Forks](https://img.shields.io/github/forks/house-of-vanity/outfleet?style=social) ![Stargazers](https://img.shields.io/github/stars/house-of-vanity/outfleet?style=social) ![License](https://img.shields.io/github/license/house-of-vanity/outfleet)
## About The Project
![Screen Shot](img/servers.png)
### Key Features
* Centralized Key Management
Administer user keys from one unified dashboard. Add, delete, and allocate users to specific servers effortlessly.
* ![Dynamic Access Keys](https://www.reddit.com/r/outlinevpn/wiki/index/dynamic_access_keys/)
Distribute ssconf:// links that are always up-to-date with your current server configurations. Eliminate the need for manual link updates.
### Why OutFleet?
Tired of juggling multiple home servers and the headache of individually managing users on each? OutFleet was born out of the frustration of not finding a suitable tool for efficiently managing a bunch of home servers.
## Built With
Python, Flask and offer hassle-free deployment.
### Installation
Docker deploy is easy:
```
docker run --restart always -p 5000:5000 -d --name outfleet --mount type=bind,source=/etc/outfleet/config.yaml,target=/usr/local/etc/outfleet/config.yaml ultradesu/outfleet:latest
```
#### Use reverse proxy to secure ALL path of OutFleet except of `/dynamic/*`
```nginx
server {
listen 443 ssl http2;
server_name server.name;
# Specify SSL config if using a shared one.
#include conf.d/ssl/ssl.conf;
# Allow large attachments
client_max_body_size 128M;
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/server.name/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/server.name/privkey.pem; # managed by Certbot
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
auth_basic "Private Place";
auth_basic_user_file /etc/nginx/htpasswd;
}
location /dynamic {
auth_basic off;
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
access_log /var/log/nginx/server.name.access.log;
error_log /var/log/nginx/server.name.error.log;
}
server {
listen 80;
server_name server.name;
listen [::]:80;
return 301 https://$host$request_uri;
}
```
Keep in mind that all user keys are stored in a single **config.yaml** file. If this file is lost, user keys will remain on the servers, but OutFleet will lose the ability to manage them. Handle with extreme caution and use backups.
## Authors
* **UltraDesu** - *Humble amateur developer* - [UltraDesu](https://github.com/house-of-vanity) - *All the work*

151
URI.md Normal file
View File

@@ -0,0 +1,151 @@
# Xray Client URI Generation
## VMess URI Format
VMess URIs use two formats:
### 1. Query Parameter Format
```
vmess://uuid@hostname:port?parameters#alias
```
**Parameters:**
- `encryption=auto` - Encryption method
- `security=tls|none` - Security layer (TLS or none)
- `sni=domain` - Server Name Indication for TLS
- `fp=chrome|firefox|safari` - TLS fingerprint
- `type=ws|tcp|grpc|http` - Transport type
- `path=/path` - WebSocket/HTTP path
- `host=domain` - Host header for WebSocket
**Example:**
```
vmess://2c981164-9b93-4bca-94ff-b78d3f8498d7@v2ray.codefyinc.com:443?encryption=auto&security=tls&sni=example.com&fp=chrome&type=ws&path=/ws&host=v2ray.codefyinc.com#MyServer
```
### 2. Base64 JSON Format
```
vmess://base64(json_config)#alias
```
**JSON Structure:**
```json
{
"v": "2",
"ps": "Server Name",
"add": "hostname",
"port": "443",
"id": "uuid",
"aid": "0",
"scy": "auto",
"net": "ws",
"type": "none",
"host": "domain",
"path": "/path",
"tls": "tls",
"sni": "domain",
"alpn": "",
"fp": "chrome"
}
```
## VLESS URI Format
```
vless://uuid@hostname:port?parameters#alias
```
**Key Parameters:**
- `encryption=none` - VLESS uses no encryption
- `security=tls|reality|none` - Security layer
- `type=ws|tcp|grpc|http|httpupgrade|xhttp` - Transport type
- `flow=xtls-rprx-vision` - Flow control (for XTLS)
- `headerType=none|http` - Header type for TCP
- `mode=auto|gun|stream-one` - Transport mode
- `serviceName=name` - gRPC service name
- `authority=domain` - gRPC authority
- `spx=/path` - Split HTTP path (for xhttp)
**REALITY Parameters:**
- `pbk=public_key` - Public key
- `sid=short_id` - Short ID
- `fp=chrome|firefox|safari` - TLS fingerprint
- `sni=domain` - Server Name Indication
**Examples:**
```
vless://uuid@server.com:443?type=tcp&security=none&headerType=none#Basic
vless://uuid@server.com:443?type=ws&security=tls&path=/ws&host=example.com#WebSocket
vless://uuid@server.com:443?type=grpc&security=reality&serviceName=grpcService&pbk=key&sid=id#gRPC-Reality
```
## Generation Algorithm
1. **UUID**: Use `inbound_users.xray_user_id`
2. **Hostname**: From `servers.hostname`
3. **Port**: From `server_inbounds.port_override` or template default
4. **Transport**: From inbound template `stream_settings`
5. **Security**: Based on certificate configuration
6. **Path**: From WebSocket stream settings
7. **Alias**: User name + server name
## Shadowsocks URI Format
```
ss://password@hostname:port?parameters#alias
```
**Parameters:**
- `encryption=none` - Usually none for modern configs
- `security=tls|reality|none` - Security layer
- `type=ws|tcp|grpc|xhttp` - Transport type
- `path=/path` - WebSocket/HTTP path
- `host=domain` - Host header
- `mode=auto|gun|stream-one` - Transport mode
- `headerType=none|http` - Header type for TCP
- `flow=xtls-rprx-vision` - Flow control (for REALITY)
- `pbk=key` - Public key (for REALITY)
- `sid=id` - Short ID (for REALITY)
**Example:**
```
ss://my-password@server.com:443?type=ws&security=tls&path=/ws&host=example.com#MyServer
```
## Trojan URI Format
```
trojan://password@hostname:port?parameters#alias
```
**Parameters:**
- `security=tls|reality|none` - Security layer
- `type=ws|tcp|grpc` - Transport type
- `sni=domain` - Server Name Indication
- `fp=chrome|firefox|randomized` - TLS fingerprint
- `flow=xtls-rprx-vision` - Flow control
- `allowInsecure=1` - Allow insecure connections
- `headerType=http|none` - Header type for TCP
- `mode=gun` - gRPC mode
- `serviceName=name` - gRPC service name
**WebSocket Parameters:**
- `path=/path` - WebSocket path
- `host=domain` - Host header
- `alpn=http/1.1|h2` - ALPN protocols
**Examples:**
```
trojan://password@server.com:443?type=tcp&security=tls&sni=example.com#Basic
trojan://password@server.com:443?type=ws&security=tls&path=/ws&host=example.com&sni=example.com#WebSocket
trojan://password@server.com:443?type=grpc&security=tls&serviceName=grpcService&mode=gun&sni=example.com#gRPC
```
## Implementation Notes
- VMess requires `aid=0` for modern clients
- VLESS doesn't use `aid` parameter
- Shadowsocks uses password instead of UUID
- Base64 encoding required for VMess JSON format
- URL encoding needed for special characters in parameters
- REALITY parameters: `pbk`, `sid`, `fp`, `sni`

View File

@@ -1,5 +0,0 @@
platforms:
- name: amd64
architecture: amd64
- name: arm64
architecture: arm64

3
client/.env Normal file
View File

@@ -0,0 +1,3 @@
VITE_API_BASE=/api
VITE_API_HOST=http://localhost
VITE_API_PORT=8081

3
client/.env.example Normal file
View File

@@ -0,0 +1,3 @@
VITE_API_BASE=/api
VITE_API_HOST=https://localhost
VITE_API_PORT=8081

24
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
client/.npmrc Normal file
View File

@@ -0,0 +1 @@
public-hoist-pattern[]=*@heroui/*

23
client/.prettierrc Normal file
View File

@@ -0,0 +1,23 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"objectWrap": "preserve",
"bracketSpacing": true,
"semi": true,
"experimentalOperatorPosition": "end",
"experimentalTernaries": false,
"singleQuote": true,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "all",
"singleAttributePerLine": false,
"htmlWhitespaceSensitivity": "css",
"vueIndentScriptAndStyle": false,
"proseWrap": "preserve",
"insertPragma": false,
"printWidth": 80,
"requirePragma": false,
"tabWidth": 2,
"useTabs": false,
"embeddedLanguageFormatting": "auto"
}

73
client/README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
client/eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

14
client/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>client</title>
<link href="/src/style.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

8160
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
client/package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"engines": {
"node": "^20"
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@heroui/react": "^2.8.5",
"@reduxjs/toolkit": "^2.9.0",
"@tailwindcss/vite": "^4.1.14",
"axios": "^1.12.2",
"clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"motion": "^12.23.24",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.64.0",
"react-redux": "^9.2.0",
"react-router": "^7.9.3",
"tailwindcss": "^4.1.14"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"lint-staged": "^16.2.3",
"prettier": "^3.6.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
}

1
client/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

9
client/src/api/api.ts Normal file
View File

@@ -0,0 +1,9 @@
import axios from 'axios';
const VITE_API_BASE = import.meta.env.VITE_API_BASE;
const VITE_API_HOST = import.meta.env.VITE_API_HOST;
const VITE_API_PORT = import.meta.env.VITE_API_PORT;
export const api = axios.create({
baseURL: `${VITE_API_HOST}:${VITE_API_PORT}${VITE_API_BASE}`,
});

1
client/src/api/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './api'

View File

@@ -0,0 +1,33 @@
import {addToast, type ToastProps} from "@heroui/toast";
import { useEffect } from 'react';
import {
appNotificator,
type Notice,
type NoticeType,
} from '../../../utils/notification/app-notificator';
const colorMap = new Map<NoticeType, string>([
['success', 'Success'],
['error', 'Danger'],
['warn', 'Warning'],
]);
const paramsMappers = (notice: Notice): Partial<ToastProps> => {
const { type, message } = notice;
const color = colorMap.get(type);
return {
description: message,
color: color?.toLowerCase() as ToastProps['color'],
};
};
export const ApplyNotificator = () => {
useEffect(() => {
appNotificator.applyProvider({
paramsMappers,
show: (params: Partial<ToastProps>) => addToast(params),
});
}, []);
return <></>;
};

View File

@@ -0,0 +1 @@
export * from './apply-notificator'

View File

@@ -0,0 +1,2 @@
export * from './use-app-dispatch'
export * from './use-app-selector'

View File

@@ -0,0 +1,4 @@
import { useDispatch } from 'react-redux'
import type { AppDispatch } from '../../store'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()

View File

@@ -0,0 +1,4 @@
import { useSelector } from 'react-redux'
import type { RootState } from '../../store'
export const useAppSelector = useSelector.withTypes<RootState>()

View File

@@ -0,0 +1 @@
export * from './nav-menu';

View File

@@ -0,0 +1 @@
export * from './nav-menu';

View File

@@ -0,0 +1,32 @@
import { Link, useLocation } from 'react-router';
import { clsx } from 'clsx';
interface NavMenuItems {
href: string;
label: string;
}
interface NavMenuProps {
items: NavMenuItems[];
}
export const NavMenu = (props: NavMenuProps) => {
const { items } = props;
const { pathname } = useLocation();
return (
<div className="tabs">
{items.map(({ href, label }) => (
<Link
key={label}
className={clsx('tab', {
active: href === pathname,
})}
to={href}
>
{label}
</Link>
))}
</div>
);
};

View File

@@ -0,0 +1,76 @@
import type { FC } from 'react';
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
} from '@heroui/react';
import type { CertificateDTO } from '../../duck';
export interface CertificateDetailProps {
cetificate: CertificateDTO;
isOpen: boolean;
onOpenChange: () => void;
}
export const CertificateDetail: FC<CertificateDetailProps> = (props) => {
const { cetificate, isOpen, onOpenChange } = props;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
Modal Title
</ModalHeader>
<ModalBody>
<div>
<h4>Basic Information</h4>
<p>
<strong>Name:</strong> ${cetificate.name}
</p>
<p>
<strong>Domain:</strong> ${cetificate.domain}
</p>
<p>
<strong>Type:</strong> ${cetificate.cert_type}
</p>
<p>
<strong>Auto Renew:</strong>
{cetificate.auto_renew ? 'Yes' : 'No'}
</p>
<p>
<strong>Created:</strong>
{new Date(cetificate.created_at).toLocaleString()}
</p>
<p>
<strong>Expires:</strong>
{new Date(cetificate.expires_at).toLocaleString()}
</p>
<h4>Certificate PEM</h4>
<div className="cert-details">
{cetificate.certificate_pem || 'Not available'}
</div>
<h4>Private Key</h4>
<div className="cert-details">
{cetificate.has_private_key
? '[Hidden for security]'
: 'Not available'}
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,93 @@
import { useEffect, type FC } from 'react';
import { useForm } from 'react-hook-form';
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
} from '@heroui/react';
import { useAppDispatch } from '../../../../common/hooks';
import { getCertificate } from '../../duck/api';
import { updateCertificate, type EditCertificateDTO } from '../../duck';
export interface CertificateEditProps {
certificateId: string;
isOpen: boolean;
onOpenChange: () => void;
}
export const CertificateEdit: FC<CertificateEditProps> = (props) => {
const dispatch = useAppDispatch();
const { certificateId, isOpen, onOpenChange } = props;
const { register, handleSubmit, reset } = useForm<EditCertificateDTO>();
useEffect(() => {
getCertificate(certificateId).then((response) => {
const { data } = response;
reset({
...data,
});
});
}, [certificateId]);
const onSubmit = (values: EditCertificateDTO) => {
dispatch(
updateCertificate({
id: certificateId,
certificate: values
}),
).then(() => {
onOpenChange();
});
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<form onSubmit={handleSubmit(onSubmit)}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
Modal Title
</ModalHeader>
<ModalBody>
<div>
<div className="form-group">
<label>Name:</label>
<input
type="text"
{...register('name', { required: true })}
/>
</div>
<div className="form-group">
<label>Domain:</label>
<input
type="text"
{...register('domain', { required: true })}
/>
</div>
<div className="form-group">
<label>
<input type="checkbox" {...register('auto_renew')} /> Auto
Renew
</label>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" type="submit">
Save
</Button>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</form>
</Modal>
);
};

View File

@@ -0,0 +1,24 @@
import type { FC } from 'react';
import type { CertificateDTO } from '../../duck';
import { CertificateView } from './certificate-view';
export interface CertificateList {
certificates: CertificateDTO[];
}
export const CertificateList: FC<CertificateList> = ({ certificates }) => {
return (
<table>
<tr>
<th>Name</th>
<th>Domain</th>
<th>Type</th>
<th>Expires</th>
<th>Auto Renew</th>
<th>Actions</th>
</tr>
{certificates
.map((certificate)=><CertificateView certificate={certificate} key={certificate.id}/>)}
</table>
);
};

View File

@@ -0,0 +1,56 @@
import type { FC } from 'react';
import { deleteCertificateAction, type CertificateDTO } from '../../duck';
import { useDisclosure } from '@heroui/react';
import { CertificateDetail } from './certificate-details';
import { CertificateEdit } from './certificate-edit';
import { useAppDispatch } from '../../../../common/hooks';
export interface CertificateViewProps {
certificate: CertificateDTO;
}
export const CertificateView: FC<CertificateViewProps> = ({ certificate }) => {
const dispatch = useAppDispatch()
const detailDisclosure = useDisclosure();
const editDisclosure = useDisclosure();
const handleDeleteCertificate = () => {
if (confirm('Delete certificate?')) {
dispatch(deleteCertificateAction(certificate.id));
}
};
return (
<>
<tr>
<td>{certificate.name}</td>
<td>{certificate.domain}</td>
<td>{certificate.cert_type}</td>
<td>{new Date(certificate.expires_at).toLocaleDateString()}</td>
<td>{certificate.auto_renew ? 'Yes' : 'No'}</td>
<td>
<button
className="btn btn-secondary"
onClick={detailDisclosure.onOpenChange}
>
View
</button>
<button
className="btn btn-primary"
onClick={editDisclosure.onOpenChange}
>
Edit
</button>
<button
className="btn btn-danger"
onClick={handleDeleteCertificate}
>
Delete
</button>
</td>
</tr>
<CertificateDetail cetificate={certificate} {...detailDisclosure} />
<CertificateEdit {...editDisclosure} certificateId={certificate.id} />
</>
);
};

View File

@@ -0,0 +1 @@
export * from './certificate-list'

View File

@@ -0,0 +1,51 @@
import type { FC } from 'react';
import { useForm } from 'react-hook-form';
import { createCertificateAction, type CreateCertificateDTO } from '../../duck';
import { useAppDispatch } from '../../../../common/hooks';
export const CreateCertificate: FC = () => {
const { handleSubmit, register, reset } = useForm<CreateCertificateDTO>();
const dispatch = useAppDispatch();
const onSubmit = (values: CreateCertificateDTO) => {
dispatch(createCertificateAction(values)).then(() => {
reset();
});
};
return (
<form id="certificateForm" onSubmit={handleSubmit(onSubmit)}>
<div className="form-group">
<label>Name:</label>
<input type="text" {...register('name', { required: true })} />
</div>
<div className="form-group">
<label>Domain:</label>
<input
type="text"
placeholder="example.com"
{...register('domain', { required: true })}
/>
</div>
<div className="form-group">
<label>Certificate Type:</label>
<select id="certType" {...register('cert_type', { required: true })}>
<option value="self_signed">Self-Signed</option>
</select>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
id="certAutoRenew"
{...register('auto_renew')}
/>{' '}
Auto Renew
</label>
</div>
<button type="submit" className="btn btn-primary">
Generate Certificate
</button>
</form>
);
};

View File

@@ -0,0 +1 @@
export * from './create-certificate'

View File

@@ -0,0 +1 @@
export * from './create-certificate'

View File

@@ -0,0 +1,99 @@
import { certificateSlice } from './slice';
import { createCertificate, deleteCertificate, getCertificates, patchCertificate } from './api';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getCertificatesState } from './selectors';
import type { RootState } from '../../../store';
import { appNotificator } from '../../../utils/notification/app-notificator';
import type { CreateCertificateDTO, EditCertificateDTO } from './dto';
const PREFFIX = 'certificates';
export const fetchCertificates = createAsyncThunk(
`${PREFFIX}/fetchAll`,
async (_, { dispatch, getState }) => {
const { loading } = getCertificatesState(getState() as RootState);
try {
if (loading) {
return;
}
dispatch(certificateSlice.actions.setLoading(true));
const response = await getCertificates().then(({ data }) => data);
dispatch(certificateSlice.actions.setUsers(response));
} catch (e) {
const message =
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
dispatch(certificateSlice.actions.setError(message));
} finally {
dispatch(certificateSlice.actions.setLoading(false));
}
},
);
export const createCertificateAction = createAsyncThunk(
`${PREFFIX}/createCertificates`,
async (params: CreateCertificateDTO, { dispatch }) => {
try {
await createCertificate(params);
dispatch(fetchCertificates());
} catch (e) {
appNotificator.add({
message:
e instanceof Error
? e.message
: `Unknown error in ${PREFFIX}/createCertificates`,
type: 'error',
});
}
},
);
export const updateCertificate = createAsyncThunk(
`${PREFFIX}/updateCertificate`,
async (
params: {
id: string;
certificate: EditCertificateDTO;
},
{ dispatch },
) => {
try {
await patchCertificate(params.id, params.certificate);
dispatch(fetchCertificates());
appNotificator.add({
message: 'Template updated',
type: 'success',
});
} catch (e) {
appNotificator.add({
type: 'error',
message:
e instanceof Error
? `Error updating: ${e.message}`
: `Unknown error in ${PREFFIX}/updateTemplate`,
});
}
},
);
export const deleteCertificateAction = createAsyncThunk(
`${PREFFIX}/deleteCertificate`,
async (id: string, { dispatch }) => {
try {
await deleteCertificate(id);
appNotificator.add({
message: 'Certificate deleted',
type: 'success',
});
dispatch(fetchCertificates());
} catch (e) {
appNotificator.add({
type: 'error',
message:
e instanceof Error
? `Delete error: ${e.message}`
: `Unknown error in ${PREFFIX}/deleteCertificate`,
});
}
},
);

View File

@@ -0,0 +1,20 @@
import type { AxiosResponse } from 'axios';
import { api } from '../../../api/api';
import type { CertificateDTO, CreateCertificateDTO, EditCertificateDTO } from './dto';
export const getCertificates = () =>
api.get<never, AxiosResponse<CertificateDTO[]>>('/certificates');
export const createCertificate = (params: CreateCertificateDTO) =>
api.post<AxiosResponse>('/certificates', params, {
headers: { 'Content-Type': 'application/json' },
});
export const getCertificate = (id: string) => api.get<never, AxiosResponse<CertificateDTO>>(`/certificates/${id}`)
export const patchCertificate = (id: string, certificate: EditCertificateDTO) =>
api.put(`/certificates/${id}`, certificate, {
headers: { 'Content-Type': 'application/json' },
});
export const deleteCertificate = (id: string) => api.delete(`/certificates/${id}`);

View File

@@ -0,0 +1,24 @@
export interface CertificateDTO {
name: string;
domain: string;
cert_type: string;
expires_at: string;
auto_renew: boolean;
id: string
created_at: string
certificate_pem: string
has_private_key: boolean
}
export interface CreateCertificateDTO {
name: string;
domain: string;
cert_type: string;
auto_renew: boolean;
}
export interface EditCertificateDTO {
name: string
domain: string
auto_renew: boolean
}

View File

@@ -0,0 +1,4 @@
export * from './actions'
export * from './dto'
export * from './slice'
export * from './selectors'

View File

@@ -0,0 +1,3 @@
import type { RootState } from '../../../store';
export const getCertificatesState = (state: RootState) => state.certificates;

View File

@@ -0,0 +1,32 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { CertificateDTO } from './dto';
export interface CertificatesState {
loading: boolean;
certificates: CertificateDTO[]
error: null | string;
}
const initialState: CertificatesState = {
loading: false,
certificates: [],
error: null,
};
export const certificateSlice = createSlice({
name: 'certificates',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload
},
setUsers: (state, action: PayloadAction<CertificateDTO[]>) => {
state.certificates = action.payload
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload
}
},
});

View File

@@ -0,0 +1,2 @@
export * from './duck'
export * from './components'

View File

@@ -0,0 +1,4 @@
export * from './servers'
export * from './templates'
export * from './users'
export * from './certificates'

View File

@@ -0,0 +1,44 @@
import { useForm, type SubmitHandler } from 'react-hook-form';
import { createServerAction } from '../../duck';
import { useAppDispatch } from '../../../../common/hooks';
import type { CreateServerForm } from '../../types';
export const AddServer = () => {
const dispatch = useAppDispatch();
const { register, handleSubmit, reset } = useForm<CreateServerForm>();
const onSubmit: SubmitHandler<CreateServerForm> = (values) => {
const data = {
...values,
grpc_port: parseInt(values.grpc_port)
}
dispatch(createServerAction(data)).then(() => {
reset();
});
};
return (
<div className="section">
<h2>Add Server</h2>
<form onSubmit={handleSubmit(onSubmit)} id="serverForm">
<div className="form-group">
<label>Name:</label>
<input {...register('name', { required: true })} />
</div>
<div className="form-group">
<label>Hostname:</label>
<input {...register('hostname', { required: true })} />
</div>
<div className="form-group">
<label>gRPC Port:</label>
<input {...register('grpc_port', { required: true })} />
</div>
<button type="submit" className="btn btn-primary">
Add Server
</button>
</form>
</div>
);
};

View File

@@ -0,0 +1 @@
export * from './servers-list'

View File

@@ -0,0 +1,94 @@
import { useEffect, type FC } from 'react';
import { useForm } from 'react-hook-form';
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
} from '@heroui/react';
import type { CreateServerForm } from '../../types';
import { getServer } from '../../duck/api';
import { useAppDispatch } from '../../../../common/hooks';
import { updateServer } from '../../duck';
export interface ServerEditProps {
serverId: string;
isOpen: boolean;
onOpenChange: () => void;
}
export const ServerEdit: FC<ServerEditProps> = (props) => {
const dispatch = useAppDispatch();
const { serverId, isOpen, onOpenChange } = props;
const { register, handleSubmit, reset } = useForm<CreateServerForm>();
useEffect(() => {
getServer(serverId).then((response) => {
const { data } = response;
reset({
...data,
grpc_port: String(data.grpc_port),
});
});
}, [serverId]);
const onSubmit = (values: CreateServerForm) => {
const data = {
...values,
grpc_port: parseInt(values.grpc_port),
};
dispatch(
updateServer({
id: serverId,
server: data,
}),
).then(() => {
onOpenChange();
});
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<form onSubmit={handleSubmit(onSubmit)}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
Modal Title
</ModalHeader>
<ModalBody>
<div className="form-group">
<label>Name:</label>
<input {...register('name', { required: true })} />
</div>
<div className="form-group">
<label>Hostname:</label>
<input {...register('hostname', { required: true })} />
</div>
<div className="form-group">
<label>gRPC Port:</label>
<input
type="number"
{...register('grpc_port', { required: true })}
/>
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" type="submit">
Save
</Button>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</form>
</Modal>
);
};

View File

@@ -0,0 +1,60 @@
import type { FC } from 'react';
import { useDisclosure } from '@heroui/react';
import { deleteServerAction, type ServerDTO } from '../../duck';
import { testServer } from '../../duck/api';
import { appNotificator } from '../../../../utils/notification/app-notificator';
import { useAppDispatch } from '../../../../common/hooks';
import { ServerEdit } from './server-edit';
export interface ServerViewProps {
server: ServerDTO;
}
export const ServerView: FC<ServerViewProps> = ({ server }) => {
const dispatch = useAppDispatch();
const handleTestServer = () => {
testServer(server.id).then((result) => {
const { connected } = result.data;
appNotificator.add({
message: connected ? 'Connection OK' : 'Connection failed',
type: connected ? 'success' : 'error',
});
});
};
const handleDeleteServer = () => {
if (confirm('Delete server?')) {
dispatch(deleteServerAction(server.id));
}
};
const { isOpen, onOpen, onOpenChange } = useDisclosure();
return (
<>
<tr>
<td>{server.name}</td>
<td>{server.hostname}</td>
<td>{server.grpc_port}</td>
<td>{server.status}</td>
<td>
<button className="btn btn-success" onClick={handleTestServer}>
Test
</button>
<button className="btn btn-primary" onClick={onOpen}>
Edit
</button>
<button className="btn btn-danger" onClick={handleDeleteServer}>
Delete
</button>
</td>
</tr>
<ServerEdit
serverId={server.id}
isOpen={isOpen}
onOpenChange={onOpenChange}
/>
</>
);
};

View File

@@ -0,0 +1,30 @@
import type { FC } from 'react';
import type { ServerDTO } from '../../duck';
import { ServerView } from './server-view';
export interface ServersListProps {
servers: ServerDTO[];
}
export const ServersList: FC<ServersListProps> = (props) => {
const { servers } = props;
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Hostname</th>
<th>Port</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{servers.map((server) => (
<ServerView key={server.id} server={server} />
))}
</tbody>
</table>
);
};

View File

@@ -0,0 +1,99 @@
import { serversSlice } from './slice';
import { createServer, deleteServer, getServers, patchServer } from './api';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getServersState } from './selectors';
import type { RootState } from '../../../store';
import type { CreateServerDTO } from './dto';
import { appNotificator } from '../../../utils/notification/app-notificator';
const PREFFIX = 'servers';
export const fetchServers = createAsyncThunk(
`${PREFFIX}/fetchAll`,
async (_, { dispatch, getState }) => {
const { loading } = getServersState(getState() as RootState);
try {
if (loading) {
return;
}
dispatch(serversSlice.actions.setLoading(true));
const response = await getServers().then(({ data }) => data);
dispatch(serversSlice.actions.setServers(response));
} catch (e) {
const message =
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
dispatch(serversSlice.actions.setError(message));
} finally {
dispatch(serversSlice.actions.setLoading(false));
}
},
);
export const createServerAction = createAsyncThunk(
`${PREFFIX}/createServer`,
async (params: CreateServerDTO, { dispatch }) => {
try {
await createServer(params);
dispatch(fetchServers());
} catch (e) {
appNotificator.add({
message:
e instanceof Error
? e.message
: `Unknown error in ${PREFFIX}/createServer`,
type: 'error',
});
}
},
);
export const deleteServerAction = createAsyncThunk(
`${PREFFIX}/deleteServer`,
async (id: string, { dispatch }) => {
try {
await deleteServer(id);
appNotificator.add({
message: 'Server deleted',
type: 'success',
});
dispatch(fetchServers());
} catch (e) {
appNotificator.add({
type: 'error',
message:
e instanceof Error
? `Delete error: ${e.message}`
: `Unknown error in ${PREFFIX}/deleteServer`,
});
}
},
);
export const updateServer = createAsyncThunk(
`${PREFFIX}/updateServer`,
async (
params: {
id: string;
server: CreateServerDTO;
},
{ dispatch },
) => {
try {
await patchServer(params.id, params.server);
dispatch(fetchServers());
appNotificator.add({
message: 'Server updated',
type: 'success',
});
} catch (e) {
appNotificator.add({
type: 'error',
message:
e instanceof Error
? `Error updating: ${e.message}`
: `Unknown error in ${PREFFIX}/deleteServer`,
});
}
},
);

View File

@@ -0,0 +1,28 @@
import type { AxiosResponse } from 'axios';
import { api } from '../../../api/api';
import type { ServerDTO, CreateServerDTO, TestServerDTO } from './dto';
export const getServers = () =>
api.get<never, AxiosResponse<ServerDTO[]>>('/servers');
export const createServer = (params: CreateServerDTO) =>
api.post<AxiosResponse>('servers', params, {
headers: {
'Content-Type': 'application/json',
},
});
export const testServer = (id: string) =>
api.post<TestServerDTO>(`/servers/${id}/test`);
export const deleteServer = (id: string) => api.delete(`/servers/${id}`);
export const getServer = (id: string) =>
api.get<string, AxiosResponse<ServerDTO>>(`/servers/${id}`);
export const patchServer = (id: string, server: CreateServerDTO) =>
api.put(`/servers/${id}`, server, {
headers: { 'Content-Type': 'application/json' },
});

View File

@@ -0,0 +1,18 @@
export interface ServerDTO {
id: string
name: string
hostname: string
grpc_port: number
status: string
}
export interface CreateServerDTO {
name: string;
hostname: string;
grpc_port: number;
}
export interface TestServerDTO {
connected: boolean,
endpoint: string
}

View File

@@ -0,0 +1,4 @@
export * from './actions'
export * from './dto'
export * from './slice'
export * from './selectors'

View File

@@ -0,0 +1,3 @@
import type { RootState } from '../../../store';
export const getServersState = (state: RootState) => state.servers;

View File

@@ -0,0 +1,32 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { ServerDTO } from './dto';
export interface ServersState {
loading: boolean;
servers: ServerDTO[];
error: null | string;
}
const initialState: ServersState = {
loading: false,
servers: [],
error: null,
};
export const serversSlice = createSlice({
name: 'servers',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload
},
setServers: (state, action: PayloadAction<ServerDTO[]>) => {
state.servers = action.payload
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload
}
},
});

View File

@@ -0,0 +1 @@
export * from './duck'

View File

@@ -0,0 +1,5 @@
export interface CreateServerForm {
name: string;
hostname: string;
grpc_port: string;
}

View File

@@ -0,0 +1 @@
export * from './form'

View File

@@ -0,0 +1,57 @@
import { useForm, type SubmitHandler } from 'react-hook-form'
import type { CreateTemplateForm } from '../../types';
import { useAppDispatch } from '../../../../common/hooks';
import { protocolOptions } from './util'
import type { CreateTemplateDTO } from '../../duck/dto';
import { createTemplateAction } from '../../duck';
export const AddTemplate = () => {
const dispatch = useAppDispatch()
const { register, handleSubmit, reset } = useForm<CreateTemplateForm>({
defaultValues: {
default_port: '443'
}
});
const onSubmit: SubmitHandler<CreateTemplateForm> = (values) => {
const data: CreateTemplateDTO = {
...values,
default_port: parseInt(values.default_port),
config_template: ''
}
dispatch(createTemplateAction(data)).then(() => {
reset();
});
};
return (
<form onSubmit={handleSubmit(onSubmit)} id="templateForm">
<div className="form-group">
<label>Name:</label>
<input type="text" {...register('name', { required: true })} />
</div>
<div className="form-group">
<label>Protocol:</label>
<select {...register('protocol', {required: true})}>
{Object.entries(protocolOptions).map((protocolTupple)=> (
<option value={protocolTupple[0]}>{protocolTupple[1]}</option>
))}
</select>
</div>
<div className="form-group">
<label>Default Port:</label>
<input type="number" {...register('default_port', {required: true})} />
</div>
<div className="form-group">
<label>
<input type="checkbox" {...register('requires_tls')}/> Requires TLS
</label>
</div>
<button type="submit" className="btn btn-primary">
Add Template
</button>
</form>
)
}

View File

@@ -0,0 +1 @@
export * from './add-template'

View File

@@ -0,0 +1,9 @@
import type { Protocol } from '../../duck/dto'
export const protocolOptions: Record<Protocol, string> = {
vless: 'VLESS',
vmess: 'VMess',
trojan: 'Trojan',
shadowsocks: 'Shadowsocks'
}

View File

@@ -0,0 +1,2 @@
export * from './add-template'
export * from './template-list'

View File

@@ -0,0 +1 @@
export * from './template-list'

View File

@@ -0,0 +1,120 @@
import { useEffect, type FC } from 'react';
import { useForm } from 'react-hook-form';
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
} from '@heroui/react';
import type { EditTemplateForm } from '../../types';
import { useAppDispatch } from '../../../../common/hooks';
import { getTemplateById } from '../../duck/api';
import { protocolOptions } from '../add-template/util';
import { updateTemplate } from '../../duck';
export interface TemplateEditProps {
templateId: string;
isOpen: boolean;
onOpenChange: () => void;
}
export const TemplateEdit: FC<TemplateEditProps> = (props) => {
const dispatch = useAppDispatch();
const { templateId, isOpen, onOpenChange } = props;
const { register, handleSubmit, reset } = useForm<EditTemplateForm>();
useEffect(() => {
getTemplateById(templateId).then((response) => {
const { data } = response;
reset({
...data,
default_port: String(data.default_port),
});
});
}, [templateId]);
const onSubmit = (values: EditTemplateForm) => {
const data = {
...values,
default_port: parseInt(values.default_port),
};
dispatch(
updateTemplate({
id: templateId,
template: data,
}),
).then(() => {
onOpenChange();
});
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<form onSubmit={handleSubmit(onSubmit)}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
Modal Title
</ModalHeader>
<ModalBody>
<div>
<div className="form-group">
<label>Name:</label>
<input
type="text"
{...register('name', { required: true })}
/>
</div>
<div className="form-group">
<label>Protocol:</label>
<select
id="editProtocol"
{...register('protocol', { required: true })}
>
{Object.entries(protocolOptions).map((protocolTupple) => (
<option value={protocolTupple[0]}>
{protocolTupple[1]}
</option>
))}
</select>
</div>
<div className="form-group">
<label>Default Port:</label>
<input
type="number"
{...register('default_port', {required: true})}
/>
</div>
<div className="form-group">
<label>
<input type="checkbox" {...register('requires_tls')} />{' '}
Requires TLS
</label>
</div>
<div className="form-group">
<label>
<input type="checkbox" {...register('is_active')} />{' '}
Active
</label>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" type="submit">
Save
</Button>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</form>
</Modal>
);
};

View File

@@ -0,0 +1,29 @@
import type { FC } from 'react';
import type { TemplateDTO } from '../../duck';
import { TemplateView } from './template-view';
export interface TemplateListProps {
templates: TemplateDTO[];
}
export const TemplateList: FC<TemplateListProps> = ({ templates }) => {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Protocol</th>
<th>Port</th>
<th>TLS</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{templates.map((template) => (
<TemplateView template={template} key={template.id}/>
))}
</tbody>
</table>
);
};

View File

@@ -0,0 +1,48 @@
import type { FC } from 'react';
import { deleteTemplateAction, type TemplateDTO } from '../../duck';
import { useDisclosure } from '@heroui/react';
import { TemplateEdit } from './template-edit';
import { useAppDispatch } from '../../../../common/hooks';
export interface TemplateViewProps {
template: TemplateDTO;
}
export const TemplateView: FC<TemplateViewProps> = ({ template }) => {
const dispatch = useAppDispatch();
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const handleDeleteTemplate = () => {
if (confirm('Delete template?')) {
dispatch(deleteTemplateAction(template.id));
}
};
return (
<>
<tr>
<td>{template.name}</td>
<td>{template.protocol}</td>
<td>{template.default_port}</td>
<td>{template.requires_tls ? 'Yes' : 'No'}</td>
<td>{template.is_active ? 'Yes' : 'No'}</td>
<td>
<button className="btn btn-primary" onClick={onOpen}>
Edit
</button>
<button
className="btn btn-danger"
onClick={handleDeleteTemplate}
>
Delete
</button>
</td>
</tr>
<TemplateEdit
templateId={template.id}
onOpenChange={onOpenChange}
isOpen={isOpen}
/>
</>
);
};

View File

@@ -0,0 +1,102 @@
import { templatesSlice } from './slice';
import { getTemplates, createTemplate, patchTemplate, deleteTemplate } from './api';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getTemplatesState } from './selectors';
import type { RootState } from '../../../store';
import type { CreateTemplateDTO, EditTemplateDTO } from './dto';
import { appNotificator } from '../../../utils/notification/app-notificator';
const PREFFIX = 'templates';
export const fetchTemplates = createAsyncThunk(
`${PREFFIX}/fetchAll`,
async (_, { dispatch, getState }) => {
const { loading } = getTemplatesState(getState() as RootState);
try {
if (loading) {
return;
}
dispatch(templatesSlice.actions.setLoading(true));
const response = await getTemplates().then(({ data }) => data);
dispatch(templatesSlice.actions.setTemplates(response));
} catch (e) {
const message =
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
dispatch(templatesSlice.actions.setError(message));
} finally {
dispatch(templatesSlice.actions.setLoading(false));
}
},
);
export const createTemplateAction = createAsyncThunk(
`${PREFFIX}/createTemplate`,
async (params: CreateTemplateDTO, { dispatch }) => {
try {
await createTemplate(params);
dispatch(fetchTemplates());
} catch (e) {
appNotificator.add({
message:
e instanceof Error
? e.message
: `Unknown error in ${PREFFIX}/createTemplate`,
type: 'error',
});
}
},
);
export const updateTemplate = createAsyncThunk(
`${PREFFIX}/updateTemplate`,
async (
params: {
id: string;
template: EditTemplateDTO;
},
{ dispatch },
) => {
try {
await patchTemplate(params.id, params.template);
dispatch(fetchTemplates());
appNotificator.add({
message: 'Template updated',
type: 'success',
});
} catch (e) {
appNotificator.add({
type: 'error',
message:
e instanceof Error
? `Error updating: ${e.message}`
: `Unknown error in ${PREFFIX}/updateTemplate`,
});
}
},
);
export const deleteTemplateAction = createAsyncThunk(
`${PREFFIX}/deleteTemplate`,
async (id: string, { dispatch }) => {
try {
await deleteTemplate(id);
appNotificator.add({
message: 'Template deleted',
type: 'success',
});
dispatch(fetchTemplates());
} catch (e) {
appNotificator.add({
type: 'error',
message:
e instanceof Error
? `Delete error: ${e.message}`
: `Unknown error in ${PREFFIX}/deleteTemplate`,
});
}
},
);

View File

@@ -0,0 +1,23 @@
import type { AxiosResponse } from 'axios';
import { api } from '../../../api/api';
import type { TemplateDTO, CreateTemplateDTO, EditTemplateDTO } from './dto';
export const getTemplates = () =>
api.get<never, AxiosResponse<TemplateDTO[]>>('/templates');
export const createTemplate = (params: CreateTemplateDTO) =>
api.post<AxiosResponse>('templates', params, {
headers: {
'Content-Type': 'application/json',
},
});
export const getTemplateById = (id: string) =>
api.get<string, AxiosResponse<TemplateDTO>>(`/templates/${id}`);
export const patchTemplate = (id: string, template: EditTemplateDTO) =>
api.put(`/templates/${id}`, template, {
headers: { 'Content-Type': 'application/json' },
});
export const deleteTemplate = (id: string) => api.delete(`/templates/${id}`);

View File

@@ -0,0 +1,33 @@
export type Protocol = 'vless' | 'vmess' | 'trojan' | 'shadowsocks';
export interface TemplateDTO {
base_settings: Record<string, unknown>; // TODO define unknown
created_at: string;
default_port: number;
description: string;
id: string;
is_active: boolean;
name: string;
protocol: Protocol;
requires_domain: boolean;
requires_tls: boolean;
stream_settings: Record<string, unknown>; // TOD define unknown
updated_at: string;
variables: unknown[]; // TOD define unknown
}
export interface CreateTemplateDTO {
name: string;
protocol: Protocol;
default_port: number;
requires_tls: boolean;
config_template: '';
}
export interface EditTemplateDTO {
name: string,
protocol: Protocol
default_port: number
requires_tls: boolean
is_active: boolean
}

View File

@@ -0,0 +1,4 @@
export * from './actions'
export * from './dto'
export * from './slice'
export * from './selectors'

View File

@@ -0,0 +1,3 @@
import type { RootState } from '../../../store';
export const getTemplatesState = (state: RootState) => state.templates;

View File

@@ -0,0 +1,32 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { TemplateDTO } from './dto';
export interface TemplateState {
loading: boolean;
templates: TemplateDTO[];
error: null | string;
}
const initialState: TemplateState = {
loading: false,
templates: [],
error: null,
};
export const templatesSlice = createSlice({
name: 'templates',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload
},
setTemplates: (state, action: PayloadAction<TemplateDTO[]>) => {
state.templates = action.payload
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload
}
},
});

View File

@@ -0,0 +1,2 @@
export * from './duck'
export * from './components'

View File

@@ -0,0 +1,16 @@
import type { Protocol } from "../duck/dto"
export interface CreateTemplateForm {
name: string,
protocol: Protocol
default_port: string
requires_tls: boolean
}
export interface EditTemplateForm {
name: string,
protocol: Protocol
default_port: string
requires_tls: boolean
is_active: boolean
}

View File

@@ -0,0 +1 @@
export * from './form'

View File

@@ -0,0 +1,30 @@
import { usersSlice } from './slice';
import { getUsers } from './api';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getUsersState } from './selectors';
import type { RootState } from '../../../store';
const PREFFIX = 'users'
export const fetchUsers = createAsyncThunk(
`${PREFFIX}/fetchAll`,
async (_, { dispatch, getState }) => {
const { loading } = getUsersState(getState() as RootState);
try {
if (loading) {
return;
}
dispatch(usersSlice.actions.setLoading(true));
const response = await getUsers().then(({ data }) => data);
dispatch(usersSlice.actions.setUsers(response));
} catch (e) {
const message =
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
dispatch(usersSlice.actions.setError(message));
} finally {
dispatch(usersSlice.actions.setLoading(false));
}
},
);

View File

@@ -0,0 +1,5 @@
import type { AxiosResponse } from 'axios';
import { api } from '../../../api/api';
import type { UserDTO } from './dto';
export const getUsers = () => api.get<never, AxiosResponse<UserDTO[]>>('/users');

View File

@@ -0,0 +1,8 @@
export interface User {}
export interface UserDTO {
page: number
per_page: number
total: number
users: User[]
}

View File

@@ -0,0 +1,4 @@
export * from './actions'
export * from './dto'
export * from './slice'
export * from './selectors'

View File

@@ -0,0 +1,3 @@
import type { RootState } from '../../../store';
export const getUsersState = (state: RootState) => state.users;

View File

@@ -0,0 +1,32 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { UserDTO, User } from './dto';
export interface UsersState {
loading: boolean;
users: User[]
error: null | string;
}
const initialState: UsersState = {
loading: false,
users: [],
error: null,
};
export const usersSlice = createSlice({
name: 'certificate',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload
},
setUsers: (state, action: PayloadAction<UserDTO[]>) => {
state.users = action.payload.users
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload
}
},
});

View File

@@ -0,0 +1 @@
export * from './duck'

2
client/src/hero.ts Normal file
View File

@@ -0,0 +1,2 @@
import { heroui } from "@heroui/react";
export default heroui();

5
client/src/index.css Normal file
View File

@@ -0,0 +1,5 @@
@import "tailwindcss";
@plugin './hero.ts';
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@custom-variant dark (&:is(.dark *));

22
client/src/main.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router/dom';
import { store } from './store/store';
import { Provider } from 'react-redux';
import { HeroUIProvider } from '@heroui/react';
import {ToastProvider} from "@heroui/toast";
import { router } from './router';
import './index.css';
import { ApplyNotificator } from './common/components/apply-notificator';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Provider store={store}>
<HeroUIProvider>
<RouterProvider router={router} />
<ToastProvider/>
<ApplyNotificator/>
</HeroUIProvider>
</Provider>
</StrictMode>,
);

View File

@@ -0,0 +1,38 @@
import type { RouteObject } from 'react-router';
import { useAppDispatch, useAppSelector } from '../../common/hooks';
import { useEffect } from 'react';
import { fetchCertificates, getCertificatesState } from '../../features';
import { CreateCertificate } from '../../features/certificates';
import { CertificateList } from '../../features/certificates/components/certificate-list';
export const Certificates = () => {
const dispatch = useAppDispatch()
const { loading, certificates } = useAppSelector(getCertificatesState)
useEffect(()=>{
dispatch(fetchCertificates())
}, [dispatch])
return (
<div id="certificates" className="tab-content active">
<div className="section">
<h2>Add Certificate</h2>
<CreateCertificate/>
</div>
<div className="section">
<h2>Certificates List</h2>
<div id="certificatesList" className="loading">
{ loading && 'Loading...' }
{ certificates.length ? <CertificateList certificates={certificates}/> : <p>No certificates found</p> }
</div>
</div>
</div>
);
};
export const CertificatesRoute: RouteObject = {
path: '/certificates',
Component: Certificates,
};

View File

@@ -0,0 +1 @@
export * from './certificates';

View File

@@ -0,0 +1,72 @@
import type { RouteObject } from 'react-router';
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../common/hooks';
import {
fetchServers,
getServersState,
fetchTemplates,
getTemplatesState,
fetchUsers,
getUsersState,
getCertificatesState,
fetchCertificates,
} from '../../features';
export const Dashboard = () => {
const dispatch = useAppDispatch();
const { loading: serverLoading, servers } = useAppSelector(getServersState);
const { loading: usersLoading, users } = useAppSelector(getUsersState);
const { loading: certificatesLoading, certificates } =
useAppSelector(getCertificatesState);
const { loading: templatesLoading, templates } =
useAppSelector(getTemplatesState);
useEffect(() => {
dispatch(fetchServers());
dispatch(fetchTemplates());
dispatch(fetchUsers());
dispatch(fetchCertificates());
}, [dispatch]);
return (
<div id="dashboard" className="tab-content active">
<div className="section">
<h2>Statistics</h2>
<p>
Servers:{' '}
<span id="serverCount">
{serverLoading === true && 'Loading...'}
{servers && String(servers.length)}
</span>
</p>
<p>
Templates:{' '}
<span id="templateCount">
{templatesLoading && 'Loading...'}
{templates && String(templates.length)}
</span>
</p>
<p>
Certificates:{' '}
<span id="certCount">
{certificatesLoading && 'Loading...'}
{certificates && String(certificates.length)}
</span>
</p>
<p>
Users:{' '}
<span id="userCount">
{usersLoading && 'Loading...'}
{users && String(users.length)}
</span>
</p>
</div>
</div>
);
};
export const DashboardRoute: RouteObject = {
index: true,
path: '/',
Component: Dashboard,
};

View File

@@ -0,0 +1 @@
export * from './dashboard';

View File

@@ -0,0 +1,238 @@
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.section {
background: white;
margin: 20px 0;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
}
h2 {
color: #666;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background: #f9f9f9;
}
.btn {
padding: 6px 12px;
margin: 2px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.form-group {
margin: 10px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 300px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* Toast notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
pointer-events: none;
}
.toast {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
margin-bottom: 10px;
padding: 16px 20px;
min-width: 300px;
max-width: 400px;
position: relative;
transform: translateX(100%);
transition:
transform 0.3s ease-in-out,
opacity 0.3s ease-in-out;
opacity: 0;
pointer-events: auto;
border-left: 4px solid #007bff;
}
.toast.show {
transform: translateX(0);
opacity: 1;
}
.toast.success {
border-left-color: #28a745;
}
.toast.error {
border-left-color: #dc3545;
}
.toast-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.toast-title {
font-weight: bold;
color: #333;
}
.toast-close {
background: none;
border: none;
font-size: 18px;
color: #999;
cursor: pointer;
padding: 0;
margin-left: 10px;
}
.toast-close:hover {
color: #666;
}
.toast-body {
color: #666;
line-height: 1.4;
}
.toast.success .toast-title {
color: #155724;
}
.toast.error .toast-title {
color: #721c24;
}
.tabs {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tab {
display: inline-block;
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab.active {
border-bottom-color: #007bff;
background: #f8f9fa;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 20px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
}
.modal-close:hover {
color: #666;
}
.modal-body {
margin-bottom: 20px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid #eee;
padding-top: 15px;
}
.cert-details {
font-family: monospace;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
white-space: pre-wrap;
overflow-x: auto;
max-height: 300px;
}

View File

@@ -0,0 +1,82 @@
import { Outlet } from 'react-router';
import './home.css';
import { NavMenu } from '../../components/nav-menu/nav-menu';
import { navItems } from './utils';
export const Home = () => {
return (
<div>
<div className="container">
<h1 className="text-3xl font-bold underline">Xray Admin Panel - Test Interface</h1>
{/* <!-- Toast notifications container --> */}
<div className="toast-container" id="toastContainer"></div>
<NavMenu items={navItems} />
<Outlet />
</div>
{/* <!-- Modal dialogs --> */}
<div id="editModal" className="modal">
<div className="modal-content">
<div className="modal-header">
<div className="modal-title" id="editModalTitle">
Edit Item
</div>
<button
className="modal-close"
// onClick="closeModal('editModal')"
>
&times;
</button>
</div>
<div className="modal-body" id="editModalBody">
{/* <!-- Content will be dynamically loaded --> */}
</div>
<div className="modal-footer">
<button
className="btn btn-secondary"
// onClick="closeModal('editModal')"
>
Cancel
</button>
<button
className="btn btn-primary"
id="saveEditBtn"
// onClick="saveEdit()"
>
Save
</button>
</div>
</div>
</div>
<div id="viewModal" className="modal">
<div className="modal-content">
<div className="modal-header">
<div className="modal-title" id="viewModalTitle">
View Details
</div>
<button
className="modal-close"
//onClick="closeModal('viewModal')"
>
&times;
</button>
</div>
<div className="modal-body" id="viewModalBody">
{/* <!-- Content will be dynamically loaded --> */}
</div>
<div className="modal-footer">
<button
className="btn btn-secondary"
// onClick="closeModal('viewModal')"
>
Close
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export * from './home';

View File

@@ -0,0 +1,26 @@
export const navItems = [
{
href: '/',
label: 'Dashboard',
},
{
href: '/servers',
label: 'Servers',
},
{
href: '/inbound-templates',
label: 'Inbound Templates',
},
{
href: '/certificates',
label: 'Certificates',
},
{
href: '/inbound-binding',
label: 'Inbound Binding',
},
{
href: '/users',
label: 'Users',
},
];

View File

@@ -0,0 +1,55 @@
import type { RouteObject } from 'react-router';
export const InboundBinding = () => {
return (
<div id="inbounds" className="tab-content active">
<div className="section">
<h2>Bind Template to Server</h2>
<form id="inboundForm">
<div className="form-group">
<label>Server:</label>
<select id="inboundServer" required>
<option value="">Select Server...</option>
</select>
</div>
<div className="form-group">
<label>Template:</label>
<select id="inboundTemplate" required>
<option value="">Select Template...</option>
</select>
</div>
<div className="form-group">
<label>Port:</label>
<input type="number" id="inboundPort" value="443" required />
</div>
<div className="form-group">
<label>Certificate:</label>
<select id="inboundCertificate">
<option value="">No Certificate</option>
</select>
</div>
<div className="form-group">
<label>
<input type="checkbox" id="inboundActive" checked /> Active
</label>
</div>
<button type="submit" className="btn btn-primary">
Bind Template
</button>
</form>
</div>
<div className="section">
<h2>Server Inbounds</h2>
<div id="inboundsList" className="loading">
Loading...
</div>
</div>
</div>
);
};
export const InboundBindingRoute: RouteObject = {
path: '/inbound-binding',
Component: InboundBinding,
};

Some files were not shown because too many files have changed in this diff Show More