mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-12-17 09:47:53 +00:00
Compare commits
1 Commits
rust-react
...
new-ui
| Author | SHA1 | Date | |
|---|---|---|---|
| c7a41e6a2f |
31
.env.example
31
.env.example
@@ -1,31 +0,0 @@
|
|||||||
# 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
|
|
||||||
36
.github/workflows/main.yml
vendored
Executable file
36
.github/workflows/main.yml
vendored
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
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
15
.gitignore
vendored
@@ -1,10 +1,9 @@
|
|||||||
|
config.yaml
|
||||||
|
__pycache__/
|
||||||
|
sync.log
|
||||||
|
main.py
|
||||||
|
.idea/*
|
||||||
|
.vscode/*
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
*.swn
|
||||||
/target/
|
|
||||||
config.toml
|
|
||||||
|
|
||||||
# macOS system files
|
|
||||||
._*
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
|
|||||||
507
API.md
507
API.md
@@ -1,507 +0,0 @@
|
|||||||
# 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
5063
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
69
Cargo.toml
69
Cargo.toml
@@ -1,69 +0,0 @@
|
|||||||
[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"
|
|
||||||
48
Dockerfile
Normal file → Executable file
48
Dockerfile
Normal file → Executable file
@@ -1,45 +1,13 @@
|
|||||||
# Build stage
|
FROM python:3-alpine
|
||||||
FROM rust:latest as builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
COPY requirements.txt .
|
||||||
RUN apt-get update && apt-get install -y \
|
COPY static static
|
||||||
pkg-config \
|
COPY templates templates
|
||||||
libssl-dev \
|
COPY *.py .
|
||||||
protobuf-compiler \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy dependency files
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
|
|
||||||
# Copy source code
|
EXPOSE 5000
|
||||||
COPY src ./src
|
CMD ["python", "main.py"]
|
||||||
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
Executable file
13
LICENSE
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
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.
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# 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.
|
|
||||||
91
README.md
Executable file
91
README.md
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
  
|
||||||
|
|
||||||
|
## About The Project
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
* Centralized Key Management
|
||||||
|
Administer user keys from one unified dashboard. Add, delete, and allocate users to specific servers effortlessly.
|
||||||
|
|
||||||
|
* 
|
||||||
|
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) - *Author*
|
||||||
|
* **Contributors**
|
||||||
|
* * @Sanapach
|
||||||
151
URI.md
151
URI.md
@@ -1,151 +0,0 @@
|
|||||||
# 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`
|
|
||||||
5
buildx.yaml
Executable file
5
buildx.yaml
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
platforms:
|
||||||
|
- name: amd64
|
||||||
|
architecture: amd64
|
||||||
|
- name: arm64
|
||||||
|
architecture: arm64
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
VITE_API_BASE=/api
|
|
||||||
VITE_API_HOST=http://localhost
|
|
||||||
VITE_API_PORT=8081
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
VITE_API_BASE=/api
|
|
||||||
VITE_API_HOST=https://localhost
|
|
||||||
VITE_API_PORT=8081
|
|
||||||
24
client/.gitignore
vendored
24
client/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# 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 +0,0 @@
|
|||||||
public-hoist-pattern[]=*@heroui/*
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# 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...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<!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
8160
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"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 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,9 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
export * from './api'
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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 <></>;
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './apply-notificator'
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './use-app-dispatch'
|
|
||||||
export * from './use-app-selector'
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import type { AppDispatch } from '../../store'
|
|
||||||
|
|
||||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { useSelector } from 'react-redux'
|
|
||||||
import type { RootState } from '../../store'
|
|
||||||
|
|
||||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './nav-menu';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './nav-menu';
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './certificate-list'
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './create-certificate'
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './create-certificate'
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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}`);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './actions'
|
|
||||||
export * from './dto'
|
|
||||||
export * from './slice'
|
|
||||||
export * from './selectors'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import type { RootState } from '../../../store';
|
|
||||||
|
|
||||||
export const getCertificatesState = (state: RootState) => state.certificates;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './duck'
|
|
||||||
export * from './components'
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './servers'
|
|
||||||
export * from './templates'
|
|
||||||
export * from './users'
|
|
||||||
export * from './certificates'
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './servers-list'
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './actions'
|
|
||||||
export * from './dto'
|
|
||||||
export * from './slice'
|
|
||||||
export * from './selectors'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import type { RootState } from '../../../store';
|
|
||||||
|
|
||||||
export const getServersState = (state: RootState) => state.servers;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './duck'
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface CreateServerForm {
|
|
||||||
name: string;
|
|
||||||
hostname: string;
|
|
||||||
grpc_port: string;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './form'
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './add-template'
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { Protocol } from '../../duck/dto'
|
|
||||||
|
|
||||||
export const protocolOptions: Record<Protocol, string> = {
|
|
||||||
vless: 'VLESS',
|
|
||||||
vmess: 'VMess',
|
|
||||||
trojan: 'Trojan',
|
|
||||||
shadowsocks: 'Shadowsocks'
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './add-template'
|
|
||||||
export * from './template-list'
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './template-list'
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
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`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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}`);
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './actions'
|
|
||||||
export * from './dto'
|
|
||||||
export * from './slice'
|
|
||||||
export * from './selectors'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import type { RootState } from '../../../store';
|
|
||||||
|
|
||||||
export const getTemplatesState = (state: RootState) => state.templates;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './duck'
|
|
||||||
export * from './components'
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './form'
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import type { AxiosResponse } from 'axios';
|
|
||||||
import { api } from '../../../api/api';
|
|
||||||
import type { UserDTO } from './dto';
|
|
||||||
|
|
||||||
export const getUsers = () => api.get<never, AxiosResponse<UserDTO[]>>('/users');
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface User {}
|
|
||||||
|
|
||||||
export interface UserDTO {
|
|
||||||
page: number
|
|
||||||
per_page: number
|
|
||||||
total: number
|
|
||||||
users: User[]
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './actions'
|
|
||||||
export * from './dto'
|
|
||||||
export * from './slice'
|
|
||||||
export * from './selectors'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import type { RootState } from '../../../store';
|
|
||||||
|
|
||||||
export const getUsersState = (state: RootState) => state.users;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './duck'
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { heroui } from "@heroui/react";
|
|
||||||
export default heroui();
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@plugin './hero.ts';
|
|
||||||
|
|
||||||
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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>,
|
|
||||||
);
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './certificates';
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './dashboard';
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
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')"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</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')"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './home';
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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
Reference in New Issue
Block a user