mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-12-17 01:37:57 +00:00
Compare commits
18 Commits
rust-react
...
RUST
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cabdd3aad3 | ||
|
|
8d10a4deef | ||
|
|
eaf7f95cd3 | ||
|
|
b96c520f9d | ||
|
|
656209ee6e | ||
|
|
c189562ac2 | ||
|
|
856dcc9f44 | ||
|
|
5d826545b0 | ||
|
|
b9f0687788 | ||
|
|
2efd5873d5 | ||
|
|
c05d2f6223 | ||
|
|
7e8831b89e | ||
|
|
78bf75b24e | ||
|
|
c6892b1a73 | ||
|
|
dae787657c | ||
|
|
d80ac56b83 | ||
|
|
d972f10f83 | ||
|
|
42c8016d9c |
61
.github/workflows/rust.yml
vendored
Normal file
61
.github/workflows/rust.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: Rust Docker Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'RUST'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: docker.io
|
||||||
|
IMAGE_NAME: ultradesu/outfleet
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract version from Cargo.toml
|
||||||
|
id: extract_version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||||
|
echo "cargo_version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Extracted version: $VERSION"
|
||||||
|
|
||||||
|
- name: Set build variables
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
echo "sha_full=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_date=$(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
GIT_COMMIT=${{ steps.vars.outputs.sha_full }}
|
||||||
|
GIT_COMMIT_SHORT=${{ steps.vars.outputs.sha_short }}
|
||||||
|
BUILD_DATE=${{ steps.vars.outputs.build_date }}
|
||||||
|
BRANCH_NAME=${{ steps.vars.outputs.branch_name }}
|
||||||
|
CARGO_VERSION=${{ steps.extract_version.outputs.cargo_version }}
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_NAME }}:rs-${{ steps.extract_version.outputs.cargo_version }}
|
||||||
|
${{ env.IMAGE_NAME }}:rs-${{ steps.extract_version.outputs.cargo_version }}-${{ steps.vars.outputs.sha_short }}
|
||||||
|
${{ env.IMAGE_NAME }}:rust-latest
|
||||||
174
API.md
174
API.md
@@ -19,6 +19,34 @@ Complete API documentation for OutFleet - a web admin panel for managing xray-co
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### User Subscription
|
||||||
|
- `GET /sub/{user_id}` - Get all user configuration links (subscription endpoint)
|
||||||
|
|
||||||
|
**Description:** Returns all VPN configuration links for a specific user, one per line. This endpoint is designed for VPN clients that support subscription URLs for automatic configuration updates.
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `user_id` (UUID) - The user's unique identifier
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- **Content-Type:** `text/plain; charset=utf-8`
|
||||||
|
- **Success (200):** Base64 encoded string containing configuration URIs (one per line when decoded)
|
||||||
|
- **Not Found (404):** User doesn't exist
|
||||||
|
- **No Content:** Returns base64 encoded comment if no configurations available
|
||||||
|
|
||||||
|
**Example Response:**
|
||||||
|
```
|
||||||
|
dm1lc3M6Ly9leUoySWpvaU1pSXNJbkJ6SWpvaVUyVnlkbVZ5TVNJc0ltRmtaQ0k2SWpFeU55NHdMakF1TVM0eElpd2ljRzl5ZENJNklqUTBNeUlzSWxsa0lqb2lNVEl6TkRVMk56Z3RNVEl6TkMwMU5qYzRMVGxoWW1NdE1USXpORFUyTnpnNVlXSmpJaXdpWVdsa0lqb2lNQ0lzSW5Oamj0SWpvaVlYVjBieUlzSW01bGRDSTZJblJqY0NJc0luUjVjR1VpT2lKdWIyNWxJaXdpYUc5emRDSTZJaUlzSW5CaGRHZ2lPaUlpTEhKMGJITWlPaUowYkhNaUxGTnVhU0k2SWlKOQ0Kdmxlc3M6Ly91dWlkQGhvc3RuYW1lOnBvcnQ/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXRscyZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUjU2VydmVyTmFtZQ0Kc3M6Ly9ZV1Z6TFRJMk5TMW5ZMjFBY0dGemMzZHZjbVE2TVRJNExqQXVNQzR5T2pnd09EQT0jU2VydmVyMg0K
|
||||||
|
```
|
||||||
|
|
||||||
|
**Decoded Example:**
|
||||||
|
```
|
||||||
|
vmess://eyJ2IjoiMiIsInBzIjoiU2VydmVyMSIsImFkZCI6IjEyNy4wLjAuMSIsInBvcnQiOiI0NDMiLCJpZCI6IjEyMzQ1Njc4LTEyMzQtNTY3OC05YWJjLTEyMzQ1Njc4OWFiYyIsImFpZCI6IjAiLCJzY3kiOiJhdXRvIiwibmV0IjoidGNwIiwidHlwZSI6Im5vbmUiLCJob3N0IjoiIiwicGF0aCI6IiIsInRscyI6InRscyIsInNuaSI6IiJ9
|
||||||
|
vless://uuid@hostname:port?encryption=none&security=tls&type=tcp&headerType=none#ServerName
|
||||||
|
ss://YWVzLTI1Ni1nY21AcGFzc3dvcmQ6MTI3LjAuMC4xOjgwODA=#Server2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:** This endpoint is intended for VPN client applications that support subscription URLs. Users can add this URL to their VPN client to automatically receive all their configurations and get updates when configurations change.
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
All API endpoints are prefixed with `/api`.
|
All API endpoints are prefixed with `/api`.
|
||||||
@@ -504,4 +532,148 @@ All API endpoints are prefixed with `/api`.
|
|||||||
"code": "ERROR_CODE",
|
"code": "ERROR_CODE",
|
||||||
"details": "Additional error details"
|
"details": "Additional error details"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Telegram Bot Integration
|
||||||
|
|
||||||
|
OutFleet includes a Telegram bot for user management and configuration access.
|
||||||
|
|
||||||
|
### User Management Endpoints
|
||||||
|
|
||||||
|
#### List User Requests
|
||||||
|
- `GET /api/user-requests` - Get all user access requests
|
||||||
|
- `GET /api/user-requests?status=pending` - Get pending requests only
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"user_id": "uuid|null",
|
||||||
|
"telegram_id": 123456789,
|
||||||
|
"telegram_username": "username",
|
||||||
|
"telegram_first_name": "John",
|
||||||
|
"telegram_last_name": "Doe",
|
||||||
|
"full_name": "John Doe",
|
||||||
|
"telegram_link": "@username",
|
||||||
|
"status": "pending|approved|declined",
|
||||||
|
"request_message": "Access request message",
|
||||||
|
"response_message": "Admin response",
|
||||||
|
"processed_by_user_id": "uuid|null",
|
||||||
|
"processed_at": "timestamp|null",
|
||||||
|
"created_at": "timestamp",
|
||||||
|
"updated_at": "timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 50,
|
||||||
|
"page": 1,
|
||||||
|
"per_page": 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get User Request
|
||||||
|
- `GET /api/user-requests/{id}` - Get specific user request
|
||||||
|
|
||||||
|
#### Approve User Request
|
||||||
|
- `POST /api/user-requests/{id}/approve` - Approve user access request
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"response_message": "Welcome! Your access has been approved."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** Updated user request object
|
||||||
|
|
||||||
|
**Side Effects:**
|
||||||
|
- Creates a new user account
|
||||||
|
- Sends Telegram notification with main menu to the user
|
||||||
|
|
||||||
|
#### Decline User Request
|
||||||
|
- `POST /api/user-requests/{id}/decline` - Decline user access request
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"response_message": "Sorry, your request has been declined."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** Updated user request object
|
||||||
|
|
||||||
|
**Side Effects:**
|
||||||
|
- Sends Telegram notification to the user
|
||||||
|
|
||||||
|
#### Delete User Request
|
||||||
|
- `DELETE /api/user-requests/{id}` - Delete user request
|
||||||
|
|
||||||
|
### Telegram Bot Configuration
|
||||||
|
|
||||||
|
#### Get Telegram Status
|
||||||
|
- `GET /api/telegram/status` - Get bot status and configuration
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"is_running": true,
|
||||||
|
"config": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Bot Name",
|
||||||
|
"bot_token": "masked",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "timestamp",
|
||||||
|
"updated_at": "timestamp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create/Update Telegram Config
|
||||||
|
- `POST /api/telegram/config` - Create new bot configuration
|
||||||
|
- `PUT /api/telegram/config/{id}` - Update bot configuration
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "OutFleet Bot",
|
||||||
|
"bot_token": "bot_token_from_botfather",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Telegram Admin Management
|
||||||
|
- `GET /api/telegram/admins` - Get all Telegram admins
|
||||||
|
- `POST /api/telegram/admins/{user_id}` - Add user as Telegram admin
|
||||||
|
- `DELETE /api/telegram/admins/{user_id}` - Remove user from Telegram admins
|
||||||
|
|
||||||
|
### Telegram Bot Features
|
||||||
|
|
||||||
|
#### User Flow
|
||||||
|
1. **Request Access**: Users send `/start` to the bot and request VPN access
|
||||||
|
2. **Admin Approval**: Admins receive notifications and can approve/decline via Telegram or web interface
|
||||||
|
3. **Configuration Access**: Approved users get access to:
|
||||||
|
- **🔗 Subscription Link**: Personal subscription URL (`/sub/{user_id}`)
|
||||||
|
- **⚙️ My Configs**: Individual configuration management
|
||||||
|
- **💬 Support**: Contact support
|
||||||
|
|
||||||
|
#### Admin Features
|
||||||
|
- **📋 User Requests**: View and manage pending access requests
|
||||||
|
- **📊 Statistics**: View system statistics
|
||||||
|
- **📢 Broadcast**: Send messages to all users
|
||||||
|
- **Approval Workflow**: Approve/decline requests with server selection
|
||||||
|
|
||||||
|
#### Subscription Link Integration
|
||||||
|
When users click "🔗 Subscription Link" in the Telegram bot, they receive:
|
||||||
|
- Personal subscription URL: `{BASE_URL}/sub/{user_id}`
|
||||||
|
- Instructions in their preferred language (Russian/English)
|
||||||
|
- Automatic updates when configurations change
|
||||||
|
|
||||||
|
**Environment Variables:**
|
||||||
|
- `BASE_URL` - Base URL for subscription links (default: `http://localhost:8080`)
|
||||||
|
|
||||||
|
### Bot Commands
|
||||||
|
- `/start` - Start bot and show main menu
|
||||||
|
- `/requests` - [Admin] View pending user requests
|
||||||
|
- `/stats` - [Admin] Show system statistics
|
||||||
|
- `/broadcast <message>` - [Admin] Send message to all users
|
||||||
593
Cargo.lock
generated
593
Cargo.lock
generated
@@ -126,6 +126,20 @@ version = "1.0.99"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aquamarine"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir",
|
||||||
|
"itertools 0.10.5",
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arraydeque"
|
name = "arraydeque"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -138,6 +152,16 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert-json-diff"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@@ -186,6 +210,12 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "auto-future"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -283,6 +313,35 @@ dependencies = [
|
|||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-test"
|
||||||
|
version = "14.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "167294800740b4b6bc7bfbccbf3a1d50a6c6e097342580ec4c11d1672e456292"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"auto-future",
|
||||||
|
"axum",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"http 1.3.1",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper 1.7.0",
|
||||||
|
"hyper-util",
|
||||||
|
"mime",
|
||||||
|
"pretty_assertions",
|
||||||
|
"reserve-port",
|
||||||
|
"rust-multipart-rfc7578_2",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.75"
|
version = "0.3.75"
|
||||||
@@ -532,7 +591,7 @@ dependencies = [
|
|||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
"strsim",
|
"strsim 0.11.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -594,7 +653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
|
checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"convert_case",
|
"convert_case 0.6.0",
|
||||||
"json5",
|
"json5",
|
||||||
"nom",
|
"nom",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
@@ -632,6 +691,12 @@ dependencies = [
|
|||||||
"tiny-keccak",
|
"tiny-keccak",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -641,6 +706,16 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -733,14 +808,38 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.13.4",
|
||||||
|
"darling_macro 0.13.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.20.11",
|
||||||
"darling_macro",
|
"darling_macro 0.20.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim 0.10.0",
|
||||||
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -753,21 +852,50 @@ dependencies = [
|
|||||||
"ident_case",
|
"ident_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim 0.11.1",
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.13.4",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.20.11",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deadpool"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
|
||||||
|
dependencies = [
|
||||||
|
"deadpool-runtime",
|
||||||
|
"lazy_static",
|
||||||
|
"num_cpus",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deadpool-runtime"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -789,6 +917,25 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "0.99.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case 0.4.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -827,6 +974,21 @@ version = "0.15.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dptree"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d81175dab5ec79c30e0576df2ed2c244e1721720c302000bb321b107e82e265c"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dunce"
|
name = "dunce"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -857,6 +1019,16 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "erasable"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -864,7 +1036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -954,6 +1126,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fragile"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fs_extra"
|
name = "fs_extra"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -974,6 +1152,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -1024,6 +1203,17 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -1042,8 +1232,10 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1195,6 +1387,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -1575,6 +1773,25 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir_macros"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@@ -1655,6 +1872,15 @@ version = "1.70.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.10.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -1885,6 +2111,33 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"downcast",
|
||||||
|
"fragile",
|
||||||
|
"lazy_static",
|
||||||
|
"mockall_derive",
|
||||||
|
"predicates",
|
||||||
|
"predicates-tree",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall_derive"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multimap"
|
name = "multimap"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -2001,6 +2254,16 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.36.7"
|
||||||
@@ -2315,6 +2578,42 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates"
|
||||||
|
version = "3.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"predicates-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-core"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-tree"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||||
|
dependencies = [
|
||||||
|
"predicates-core",
|
||||||
|
"termtree",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -2525,6 +2824,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rc-box"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0"
|
||||||
|
dependencies = [
|
||||||
|
"erasable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rcgen"
|
name = "rcgen"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -2618,6 +2926,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -2632,15 +2941,26 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls 0.24.1",
|
"tokio-rustls 0.24.1",
|
||||||
|
"tokio-util",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots 0.25.4",
|
"webpki-roots 0.25.4",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reserve-port"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.16",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -2726,6 +3046,22 @@ dependencies = [
|
|||||||
"ordered-multimap",
|
"ordered-multimap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-multipart-rfc7578_2"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"rand",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.38.0"
|
version = "1.38.0"
|
||||||
@@ -2754,6 +3090,15 @@ version = "2.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -2764,7 +3109,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2895,6 +3240,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scc"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
|
||||||
|
dependencies = [
|
||||||
|
"sdd",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.28"
|
version = "0.1.28"
|
||||||
@@ -2920,6 +3274,12 @@ dependencies = [
|
|||||||
"untrusted 0.9.0",
|
"untrusted 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sdd"
|
||||||
|
version = "3.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-bae"
|
name = "sea-bae"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -3047,7 +3407,7 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
|
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.20.11",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3120,6 +3480,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.225"
|
version = "1.0.225"
|
||||||
@@ -3195,6 +3561,28 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_with_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "1.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.13.4",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml"
|
name = "serde_yaml"
|
||||||
version = "0.9.34+deprecated"
|
version = "0.9.34+deprecated"
|
||||||
@@ -3208,6 +3596,31 @@ dependencies = [
|
|||||||
"unsafe-libyaml",
|
"unsafe-libyaml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serial_test"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"scc",
|
||||||
|
"serial_test_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serial_test_derive"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -3219,6 +3632,12 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1_smol"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -3556,6 +3975,12 @@ dependencies = [
|
|||||||
"unicode-properties",
|
"unicode-properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -3640,12 +4065,92 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "take_mut"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "takecell"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "teloxide"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f79dd283eb21b90451c03fa7c7f83b9985130efb876b33bad89a2c208ccbc16"
|
||||||
|
dependencies = [
|
||||||
|
"aquamarine",
|
||||||
|
"bytes",
|
||||||
|
"derive_more",
|
||||||
|
"dptree",
|
||||||
|
"either",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"pin-project",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"teloxide-core",
|
||||||
|
"teloxide-macros",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "teloxide-core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1642a7ef10e7af63b8298c8d13c0f986d4fc646d42649ff060359607f62f69"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"derive_more",
|
||||||
|
"either",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"once_cell",
|
||||||
|
"pin-project",
|
||||||
|
"rc-box",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
|
"take_mut",
|
||||||
|
"takecell",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "teloxide-macros"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e2d33d809c3e7161a9ab18bedddf98821245014f0a78fa4d2c9430b2ec018c1"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.4.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.22.0"
|
version = "3.22.0"
|
||||||
@@ -3656,9 +4161,15 @@ dependencies = [
|
|||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termtree"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -3859,6 +4370,19 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-test"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.16"
|
version = "0.7.16"
|
||||||
@@ -4236,6 +4760,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sha1_smol",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4261,7 +4786,7 @@ version = "0.18.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10"
|
checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.20.11",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -4408,6 +4933,19 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.80"
|
version = "0.3.80"
|
||||||
@@ -4467,7 +5005,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4851,6 +5389,29 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wiremock"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
||||||
|
dependencies = [
|
||||||
|
"assert-json-diff",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"deadpool",
|
||||||
|
"futures",
|
||||||
|
"http 1.3.1",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper 1.7.0",
|
||||||
|
"hyper-util",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@@ -4874,11 +5435,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xray-admin"
|
name = "xray-admin"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-test",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -4886,6 +5448,7 @@ dependencies = [
|
|||||||
"hyper 1.7.0",
|
"hyper 1.7.0",
|
||||||
"instant-acme",
|
"instant-acme",
|
||||||
"log",
|
"log",
|
||||||
|
"mockall",
|
||||||
"pem",
|
"pem",
|
||||||
"prost",
|
"prost",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -4898,11 +5461,14 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"serial_test",
|
||||||
|
"teloxide",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-cron-scheduler",
|
"tokio-cron-scheduler",
|
||||||
|
"tokio-test",
|
||||||
"toml",
|
"toml",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
@@ -4913,6 +5479,7 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"validator",
|
"validator",
|
||||||
|
"wiremock",
|
||||||
"xray-core",
|
"xray-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xray-admin"
|
name = "xray-admin"
|
||||||
version = "0.1.0"
|
version = "0.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -37,7 +37,7 @@ sea-orm = { version = "1.0", features = ["sqlx-postgres", "runtime-tokio-rustls"
|
|||||||
sea-orm-migration = "1.0"
|
sea-orm-migration = "1.0"
|
||||||
|
|
||||||
# Additional utilities
|
# Additional utilities
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
uuid = { version = "1.0", features = ["v4", "v5", "serde"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
@@ -65,5 +65,13 @@ rustls = { version = "0.23", features = ["aws-lc-rs"] } # TLS library with aws-
|
|||||||
ring = "0.17" # Crypto for ACME
|
ring = "0.17" # Crypto for ACME
|
||||||
pem = "3.0" # PEM format support
|
pem = "3.0" # PEM format support
|
||||||
|
|
||||||
|
# Telegram bot support
|
||||||
|
teloxide = { version = "0.13", features = ["macros"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
|
tokio-test = "0.4"
|
||||||
|
wiremock = "0.6"
|
||||||
|
axum-test = "14.0"
|
||||||
|
serial_test = "3.0"
|
||||||
|
mockall = "0.12"
|
||||||
|
|||||||
64
Dockerfile
64
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Build stage
|
# Build stage with Rust
|
||||||
FROM rust:latest as builder
|
FROM rust:1.90-bookworm AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -10,26 +10,65 @@ RUN apt-get update && apt-get install -y \
|
|||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy dependency files
|
# Build arguments
|
||||||
|
ARG GIT_COMMIT="development"
|
||||||
|
ARG GIT_COMMIT_SHORT="dev"
|
||||||
|
ARG BUILD_DATE="unknown"
|
||||||
|
ARG BRANCH_NAME="unknown"
|
||||||
|
ARG CARGO_VERSION="0.1.0"
|
||||||
|
|
||||||
|
# Environment variables from build args
|
||||||
|
ENV GIT_COMMIT=${GIT_COMMIT}
|
||||||
|
ENV GIT_COMMIT_SHORT=${GIT_COMMIT_SHORT}
|
||||||
|
ENV BUILD_DATE=${BUILD_DATE}
|
||||||
|
ENV BRANCH_NAME=${BRANCH_NAME}
|
||||||
|
ENV CARGO_VERSION=${CARGO_VERSION}
|
||||||
|
|
||||||
|
# Copy dependency files first for caching
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
# Copy source code
|
# Create dummy source files to build dependencies
|
||||||
|
RUN mkdir -p src && \
|
||||||
|
echo "fn main() {}" > src/main.rs && \
|
||||||
|
echo "pub fn lib() {}" > src/lib.rs
|
||||||
|
|
||||||
|
# Build dependencies (this layer will be cached)
|
||||||
|
RUN cargo build --release && \
|
||||||
|
rm -rf src target/release/deps/xray_admin* target/release/xray-admin*
|
||||||
|
|
||||||
|
# Copy actual source code
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY static ./static
|
COPY static ./static
|
||||||
|
|
||||||
# Build the application
|
# Build the application
|
||||||
RUN cargo build --release
|
RUN cargo build --release --locked
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage - Ubuntu for glibc compatibility
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04 AS runtime
|
||||||
|
|
||||||
|
# Build arguments (needed for runtime stage)
|
||||||
|
ARG GIT_COMMIT="development"
|
||||||
|
ARG GIT_COMMIT_SHORT="dev"
|
||||||
|
ARG BUILD_DATE="unknown"
|
||||||
|
ARG BRANCH_NAME="unknown"
|
||||||
|
ARG CARGO_VERSION="0.1.0"
|
||||||
|
|
||||||
|
# Environment variables from build args
|
||||||
|
ENV GIT_COMMIT=${GIT_COMMIT}
|
||||||
|
ENV GIT_COMMIT_SHORT=${GIT_COMMIT_SHORT}
|
||||||
|
ENV BUILD_DATE=${BUILD_DATE}
|
||||||
|
ENV BRANCH_NAME=${BRANCH_NAME}
|
||||||
|
ENV CARGO_VERSION=${CARGO_VERSION}
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install runtime dependencies
|
# Install minimal runtime dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
libssl3 \
|
libssl3 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
libprotobuf32 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& apt-get clean
|
||||||
|
|
||||||
# Copy the binary from builder
|
# Copy the binary from builder
|
||||||
COPY --from=builder /app/target/release/xray-admin /app/xray-admin
|
COPY --from=builder /app/target/release/xray-admin /app/xray-admin
|
||||||
@@ -40,6 +79,11 @@ COPY --from=builder /app/static ./static
|
|||||||
# Copy config file
|
# Copy config file
|
||||||
COPY config.docker.toml ./config.toml
|
COPY config.docker.toml ./config.toml
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN groupadd -r outfleet && useradd -r -g outfleet -s /bin/false outfleet
|
||||||
|
RUN chown -R outfleet:outfleet /app
|
||||||
|
USER outfleet
|
||||||
|
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
CMD ["/app/xray-admin", "--host", "0.0.0.0"]
|
CMD ["/app/xray-admin", "--host", "0.0.0.0"]
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './inbound-binding';
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { RouteObject } from 'react-router';
|
|
||||||
import { AddTemplate, fetchTemplates, getTemplatesState, TemplateList } from '../../features/templates';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../common/hooks';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export const InboundTemplates = () => {
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const { loading, templates } = useAppSelector(getTemplatesState);
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
dispatch(fetchTemplates())
|
|
||||||
}, [dispatch])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="templates" className="tab-content active">
|
|
||||||
<div className="section">
|
|
||||||
<h2>Add Template</h2>
|
|
||||||
<AddTemplate />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="section">
|
|
||||||
<h2>Templates List</h2>
|
|
||||||
<div id="templatesList" className="loading">
|
|
||||||
{loading && 'Loading...'}
|
|
||||||
{templates.length ? (
|
|
||||||
<TemplateList templates={templates}/>
|
|
||||||
) : (
|
|
||||||
<p>No templates found</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InboundTemplatesRoute: RouteObject = {
|
|
||||||
path: '/inbound-templates',
|
|
||||||
Component: InboundTemplates,
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './inboud-templates';
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export * from './home';
|
|
||||||
export * from './certificates';
|
|
||||||
export * from './dashboard';
|
|
||||||
export * from './inbound-binding';
|
|
||||||
export * from './inbound-templates';
|
|
||||||
export * from './servers';
|
|
||||||
export * from './users';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './servers';
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import type { RouteObject } from 'react-router';
|
|
||||||
import { AddServer } from '../../features/servers/components/add-server/add-server';
|
|
||||||
import { fetchServers, getServersState } from '../../features';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../common/hooks';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { ServersList } from '../../features/servers/components/servers-list';
|
|
||||||
|
|
||||||
export const Servers = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { loading, servers } = useAppSelector(getServersState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchServers());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="servers" className="tab-content active">
|
|
||||||
<AddServer />
|
|
||||||
|
|
||||||
<div className="section">
|
|
||||||
<h2>Servers List</h2>
|
|
||||||
<div id="serversList" className={clsx({ loading: loading })}>
|
|
||||||
{loading && 'Loading...'}
|
|
||||||
{servers.length ? (
|
|
||||||
<ServersList servers={servers} />
|
|
||||||
) : (
|
|
||||||
<p>No servers found</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ServersRoute: RouteObject = {
|
|
||||||
path: '/servers',
|
|
||||||
Component: Servers,
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './users';
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user