mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-12-16 17:37:51 +00:00
Compare commits
10 Commits
RUST
...
rust-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743ca72965 | ||
|
|
f572b28711 | ||
|
|
781d7439af | ||
|
|
894dd4da95 | ||
|
|
45c21cca82 | ||
|
|
de6f4bc6f9 | ||
|
|
d264968cc8 | ||
|
|
1a42dc9d4c | ||
|
|
bfa2878109 | ||
|
|
8472e21955 |
61
.github/workflows/rust.yml
vendored
61
.github/workflows/rust.yml
vendored
@@ -1,61 +0,0 @@
|
||||
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,34 +19,6 @@ 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
|
||||
|
||||
All API endpoints are prefixed with `/api`.
|
||||
@@ -532,148 +504,4 @@ All API endpoints are prefixed with `/api`.
|
||||
"code": "ERROR_CODE",
|
||||
"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,20 +126,6 @@ version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "arraydeque"
|
||||
version = "0.5.1"
|
||||
@@ -152,16 +138,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
@@ -210,12 +186,6 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "auto-future"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
@@ -313,35 +283,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
@@ -591,7 +532,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim 0.11.1",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -653,7 +594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"convert_case 0.6.0",
|
||||
"convert_case",
|
||||
"json5",
|
||||
"nom",
|
||||
"pathdiff",
|
||||
@@ -691,12 +632,6 @@ dependencies = [
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
@@ -706,16 +641,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -808,38 +733,14 @@ dependencies = [
|
||||
"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]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"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",
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -852,50 +753,21 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.11.1",
|
||||
"strsim",
|
||||
"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]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"darling_core",
|
||||
"quote",
|
||||
"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]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -917,25 +789,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -974,21 +827,6 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
@@ -1019,16 +857,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -1036,7 +864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1126,12 +954,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fragile"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
@@ -1152,7 +974,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -1203,17 +1024,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
@@ -1232,10 +1042,8 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
@@ -1387,12 +1195,6 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -1773,25 +1575,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -1872,15 +1655,6 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -2111,33 +1885,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "multimap"
|
||||
version = "0.10.1"
|
||||
@@ -2254,16 +2001,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
@@ -2578,42 +2315,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -2824,15 +2525,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rcgen"
|
||||
version = "0.12.1"
|
||||
@@ -2926,7 +2618,6 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@@ -2941,26 +2632,15 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.25.4",
|
||||
"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]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3046,22 +2726,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rust_decimal"
|
||||
version = "1.38.0"
|
||||
@@ -3090,15 +2754,6 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
@@ -3109,7 +2764,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3240,15 +2895,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
|
||||
dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
@@ -3274,12 +2920,6 @@ dependencies = [
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdd"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
||||
|
||||
[[package]]
|
||||
name = "sea-bae"
|
||||
version = "0.2.1"
|
||||
@@ -3407,7 +3047,7 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"darling",
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3480,12 +3120,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.225"
|
||||
@@ -3561,28 +3195,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
@@ -3596,31 +3208,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -3632,12 +3219,6 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -3975,12 +3556,6 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -4065,92 +3640,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tempfile"
|
||||
version = "3.22.0"
|
||||
@@ -4161,15 +3656,9 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -4370,19 +3859,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.16"
|
||||
@@ -4760,7 +4236,6 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"sha1_smol",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -4786,7 +4261,7 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"darling",
|
||||
"once_cell",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
@@ -4933,19 +4408,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "web-sys"
|
||||
version = "0.3.80"
|
||||
@@ -5005,7 +4467,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5389,29 +4851,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
@@ -5435,12 +4874,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xray-admin"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-test",
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -5448,7 +4886,6 @@ dependencies = [
|
||||
"hyper 1.7.0",
|
||||
"instant-acme",
|
||||
"log",
|
||||
"mockall",
|
||||
"pem",
|
||||
"prost",
|
||||
"rand",
|
||||
@@ -5461,14 +4898,11 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"teloxide",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-cron-scheduler",
|
||||
"tokio-test",
|
||||
"toml",
|
||||
"tonic",
|
||||
"tower 0.4.13",
|
||||
@@ -5479,7 +4913,6 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"validator",
|
||||
"wiremock",
|
||||
"xray-core",
|
||||
]
|
||||
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "xray-admin"
|
||||
version = "0.1.3"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -37,7 +37,7 @@ sea-orm = { version = "1.0", features = ["sqlx-postgres", "runtime-tokio-rustls"
|
||||
sea-orm-migration = "1.0"
|
||||
|
||||
# Additional utilities
|
||||
uuid = { version = "1.0", features = ["v4", "v5", "serde"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
async-trait = "0.1"
|
||||
log = "0.4"
|
||||
@@ -65,13 +65,5 @@ rustls = { version = "0.23", features = ["aws-lc-rs"] } # TLS library with aws-
|
||||
ring = "0.17" # Crypto for ACME
|
||||
pem = "3.0" # PEM format support
|
||||
|
||||
# Telegram bot support
|
||||
teloxide = { version = "0.13", features = ["macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
tokio-test = "0.4"
|
||||
wiremock = "0.6"
|
||||
axum-test = "14.0"
|
||||
serial_test = "3.0"
|
||||
mockall = "0.12"
|
||||
tempfile = "3.0"
|
||||
64
Dockerfile
64
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Build stage with Rust
|
||||
FROM rust:1.90-bookworm AS builder
|
||||
# Build stage
|
||||
FROM rust:latest as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,65 +10,26 @@ RUN apt-get update && apt-get install -y \
|
||||
protobuf-compiler \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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 dependency files
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
# 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 source code
|
||||
COPY src ./src
|
||||
COPY static ./static
|
||||
|
||||
# Build the application
|
||||
RUN cargo build --release --locked
|
||||
RUN cargo build --release
|
||||
|
||||
# Runtime stage - Ubuntu for glibc compatibility
|
||||
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}
|
||||
# Runtime stage
|
||||
FROM ubuntu:24.04
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install minimal runtime dependencies
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
libssl3 \
|
||||
libprotobuf32 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the binary from builder
|
||||
COPY --from=builder /app/target/release/xray-admin /app/xray-admin
|
||||
@@ -79,11 +40,6 @@ COPY --from=builder /app/static ./static
|
||||
# Copy config file
|
||||
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
|
||||
|
||||
CMD ["/app/xray-admin", "--host", "0.0.0.0"]
|
||||
CMD ["/app/xray-admin", "--host", "0.0.0.0"]
|
||||
|
||||
3
client/.env
Normal file
3
client/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
VITE_API_BASE=/api
|
||||
VITE_API_HOST=http://localhost
|
||||
VITE_API_PORT=8081
|
||||
3
client/.env.example
Normal file
3
client/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
VITE_API_BASE=/api
|
||||
VITE_API_HOST=https://localhost
|
||||
VITE_API_PORT=8081
|
||||
24
client/.gitignore
vendored
Normal file
24
client/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
client/.npmrc
Normal file
1
client/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
public-hoist-pattern[]=*@heroui/*
|
||||
23
client/.prettierrc
Normal file
23
client/.prettierrc
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSameLine": false,
|
||||
"objectWrap": "preserve",
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"experimentalOperatorPosition": "end",
|
||||
"experimentalTernaries": false,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"singleAttributePerLine": false,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"proseWrap": "preserve",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"requirePragma": false,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
73
client/README.md
Normal file
73
client/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
23
client/eslint.config.js
Normal file
23
client/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
14
client/index.html
Normal file
14
client/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>client</title>
|
||||
<link href="/src/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
8160
client/package-lock.json
generated
Normal file
8160
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
client/package.json
Normal file
49
client/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.5",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.9.3",
|
||||
"tailwindcss": "^4.1.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"lint-staged": "^16.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^7.1.7"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
}
|
||||
}
|
||||
1
client/public/vite.svg
Normal file
1
client/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
9
client/src/api/api.ts
Normal file
9
client/src/api/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const VITE_API_BASE = import.meta.env.VITE_API_BASE;
|
||||
const VITE_API_HOST = import.meta.env.VITE_API_HOST;
|
||||
const VITE_API_PORT = import.meta.env.VITE_API_PORT;
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: `${VITE_API_HOST}:${VITE_API_PORT}${VITE_API_BASE}`,
|
||||
});
|
||||
1
client/src/api/index.ts
Normal file
1
client/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './api'
|
||||
@@ -0,0 +1,33 @@
|
||||
import {addToast, type ToastProps} from "@heroui/toast";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
appNotificator,
|
||||
type Notice,
|
||||
type NoticeType,
|
||||
} from '../../../utils/notification/app-notificator';
|
||||
|
||||
const colorMap = new Map<NoticeType, string>([
|
||||
['success', 'Success'],
|
||||
['error', 'Danger'],
|
||||
['warn', 'Warning'],
|
||||
]);
|
||||
|
||||
const paramsMappers = (notice: Notice): Partial<ToastProps> => {
|
||||
const { type, message } = notice;
|
||||
const color = colorMap.get(type);
|
||||
return {
|
||||
description: message,
|
||||
color: color?.toLowerCase() as ToastProps['color'],
|
||||
};
|
||||
};
|
||||
|
||||
export const ApplyNotificator = () => {
|
||||
useEffect(() => {
|
||||
appNotificator.applyProvider({
|
||||
paramsMappers,
|
||||
show: (params: Partial<ToastProps>) => addToast(params),
|
||||
});
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
1
client/src/common/components/apply-notificator/index.ts
Normal file
1
client/src/common/components/apply-notificator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './apply-notificator'
|
||||
2
client/src/common/hooks/index.ts
Normal file
2
client/src/common/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './use-app-dispatch'
|
||||
export * from './use-app-selector'
|
||||
4
client/src/common/hooks/use-app-dispatch.ts
Normal file
4
client/src/common/hooks/use-app-dispatch.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useDispatch } from 'react-redux'
|
||||
import type { AppDispatch } from '../../store'
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
4
client/src/common/hooks/use-app-selector.ts
Normal file
4
client/src/common/hooks/use-app-selector.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import type { RootState } from '../../store'
|
||||
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
1
client/src/components/index.ts
Normal file
1
client/src/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './nav-menu';
|
||||
1
client/src/components/nav-menu/index.ts
Normal file
1
client/src/components/nav-menu/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './nav-menu';
|
||||
32
client/src/components/nav-menu/nav-menu.tsx
Normal file
32
client/src/components/nav-menu/nav-menu.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Link, useLocation } from 'react-router';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface NavMenuItems {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface NavMenuProps {
|
||||
items: NavMenuItems[];
|
||||
}
|
||||
|
||||
export const NavMenu = (props: NavMenuProps) => {
|
||||
const { items } = props;
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
{items.map(({ href, label }) => (
|
||||
<Link
|
||||
key={label}
|
||||
className={clsx('tab', {
|
||||
active: href === pathname,
|
||||
})}
|
||||
to={href}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@heroui/react';
|
||||
import type { CertificateDTO } from '../../duck';
|
||||
|
||||
export interface CertificateDetailProps {
|
||||
cetificate: CertificateDTO;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
}
|
||||
|
||||
export const CertificateDetail: FC<CertificateDetailProps> = (props) => {
|
||||
const { cetificate, isOpen, onOpenChange } = props;
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Modal Title
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>
|
||||
<h4>Basic Information</h4>
|
||||
<p>
|
||||
<strong>Name:</strong> ${cetificate.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Domain:</strong> ${cetificate.domain}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Type:</strong> ${cetificate.cert_type}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Auto Renew:</strong>
|
||||
{cetificate.auto_renew ? 'Yes' : 'No'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Created:</strong>
|
||||
{new Date(cetificate.created_at).toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Expires:</strong>
|
||||
{new Date(cetificate.expires_at).toLocaleString()}
|
||||
</p>
|
||||
|
||||
<h4>Certificate PEM</h4>
|
||||
<div className="cert-details">
|
||||
{cetificate.certificate_pem || 'Not available'}
|
||||
</div>
|
||||
|
||||
<h4>Private Key</h4>
|
||||
<div className="cert-details">
|
||||
{cetificate.has_private_key
|
||||
? '[Hidden for security]'
|
||||
: 'Not available'}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useEffect, type FC } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@heroui/react';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import { getCertificate } from '../../duck/api';
|
||||
import { updateCertificate, type EditCertificateDTO } from '../../duck';
|
||||
|
||||
export interface CertificateEditProps {
|
||||
certificateId: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
}
|
||||
|
||||
export const CertificateEdit: FC<CertificateEditProps> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { certificateId, isOpen, onOpenChange } = props;
|
||||
const { register, handleSubmit, reset } = useForm<EditCertificateDTO>();
|
||||
|
||||
useEffect(() => {
|
||||
getCertificate(certificateId).then((response) => {
|
||||
const { data } = response;
|
||||
reset({
|
||||
...data,
|
||||
});
|
||||
});
|
||||
}, [certificateId]);
|
||||
|
||||
const onSubmit = (values: EditCertificateDTO) => {
|
||||
dispatch(
|
||||
updateCertificate({
|
||||
id: certificateId,
|
||||
certificate: values
|
||||
}),
|
||||
).then(() => {
|
||||
onOpenChange();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Modal Title
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('name', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Domain:</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('domain', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input type="checkbox" {...register('auto_renew')} /> Auto
|
||||
Renew
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { FC } from 'react';
|
||||
import type { CertificateDTO } from '../../duck';
|
||||
import { CertificateView } from './certificate-view';
|
||||
|
||||
export interface CertificateList {
|
||||
certificates: CertificateDTO[];
|
||||
}
|
||||
|
||||
export const CertificateList: FC<CertificateList> = ({ certificates }) => {
|
||||
return (
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Domain</th>
|
||||
<th>Type</th>
|
||||
<th>Expires</th>
|
||||
<th>Auto Renew</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{certificates
|
||||
.map((certificate)=><CertificateView certificate={certificate} key={certificate.id}/>)}
|
||||
</table>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { FC } from 'react';
|
||||
import { deleteCertificateAction, type CertificateDTO } from '../../duck';
|
||||
import { useDisclosure } from '@heroui/react';
|
||||
import { CertificateDetail } from './certificate-details';
|
||||
import { CertificateEdit } from './certificate-edit';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
|
||||
export interface CertificateViewProps {
|
||||
certificate: CertificateDTO;
|
||||
}
|
||||
|
||||
export const CertificateView: FC<CertificateViewProps> = ({ certificate }) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const detailDisclosure = useDisclosure();
|
||||
const editDisclosure = useDisclosure();
|
||||
|
||||
const handleDeleteCertificate = () => {
|
||||
if (confirm('Delete certificate?')) {
|
||||
dispatch(deleteCertificateAction(certificate.id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>{certificate.name}</td>
|
||||
<td>{certificate.domain}</td>
|
||||
<td>{certificate.cert_type}</td>
|
||||
<td>{new Date(certificate.expires_at).toLocaleDateString()}</td>
|
||||
<td>{certificate.auto_renew ? 'Yes' : 'No'}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={detailDisclosure.onOpenChange}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={editDisclosure.onOpenChange}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleDeleteCertificate}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<CertificateDetail cetificate={certificate} {...detailDisclosure} />
|
||||
<CertificateEdit {...editDisclosure} certificateId={certificate.id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './certificate-list'
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { FC } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { createCertificateAction, type CreateCertificateDTO } from '../../duck';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
|
||||
export const CreateCertificate: FC = () => {
|
||||
const { handleSubmit, register, reset } = useForm<CreateCertificateDTO>();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onSubmit = (values: CreateCertificateDTO) => {
|
||||
dispatch(createCertificateAction(values)).then(() => {
|
||||
reset();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form id="certificateForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" {...register('name', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Domain:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
{...register('domain', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Certificate Type:</label>
|
||||
<select id="certType" {...register('cert_type', { required: true })}>
|
||||
<option value="self_signed">Self-Signed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="certAutoRenew"
|
||||
{...register('auto_renew')}
|
||||
/>{' '}
|
||||
Auto Renew
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Generate Certificate
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './create-certificate'
|
||||
1
client/src/features/certificates/components/index.ts
Normal file
1
client/src/features/certificates/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './create-certificate'
|
||||
99
client/src/features/certificates/duck/actions.ts
Normal file
99
client/src/features/certificates/duck/actions.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { certificateSlice } from './slice';
|
||||
import { createCertificate, deleteCertificate, getCertificates, patchCertificate } from './api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { getCertificatesState } from './selectors';
|
||||
import type { RootState } from '../../../store';
|
||||
import { appNotificator } from '../../../utils/notification/app-notificator';
|
||||
import type { CreateCertificateDTO, EditCertificateDTO } from './dto';
|
||||
|
||||
const PREFFIX = 'certificates';
|
||||
|
||||
export const fetchCertificates = createAsyncThunk(
|
||||
`${PREFFIX}/fetchAll`,
|
||||
async (_, { dispatch, getState }) => {
|
||||
const { loading } = getCertificatesState(getState() as RootState);
|
||||
try {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(certificateSlice.actions.setLoading(true));
|
||||
const response = await getCertificates().then(({ data }) => data);
|
||||
dispatch(certificateSlice.actions.setUsers(response));
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||
dispatch(certificateSlice.actions.setError(message));
|
||||
} finally {
|
||||
dispatch(certificateSlice.actions.setLoading(false));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const createCertificateAction = createAsyncThunk(
|
||||
`${PREFFIX}/createCertificates`,
|
||||
async (params: CreateCertificateDTO, { dispatch }) => {
|
||||
try {
|
||||
await createCertificate(params);
|
||||
dispatch(fetchCertificates());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
message:
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: `Unknown error in ${PREFFIX}/createCertificates`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const updateCertificate = createAsyncThunk(
|
||||
`${PREFFIX}/updateCertificate`,
|
||||
async (
|
||||
params: {
|
||||
id: string;
|
||||
certificate: EditCertificateDTO;
|
||||
},
|
||||
{ dispatch },
|
||||
) => {
|
||||
try {
|
||||
await patchCertificate(params.id, params.certificate);
|
||||
dispatch(fetchCertificates());
|
||||
appNotificator.add({
|
||||
message: 'Template updated',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Error updating: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/updateTemplate`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteCertificateAction = createAsyncThunk(
|
||||
`${PREFFIX}/deleteCertificate`,
|
||||
async (id: string, { dispatch }) => {
|
||||
try {
|
||||
await deleteCertificate(id);
|
||||
appNotificator.add({
|
||||
message: 'Certificate deleted',
|
||||
type: 'success',
|
||||
});
|
||||
dispatch(fetchCertificates());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Delete error: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/deleteCertificate`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
20
client/src/features/certificates/duck/api.ts
Normal file
20
client/src/features/certificates/duck/api.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { api } from '../../../api/api';
|
||||
import type { CertificateDTO, CreateCertificateDTO, EditCertificateDTO } from './dto';
|
||||
|
||||
export const getCertificates = () =>
|
||||
api.get<never, AxiosResponse<CertificateDTO[]>>('/certificates');
|
||||
|
||||
export const createCertificate = (params: CreateCertificateDTO) =>
|
||||
api.post<AxiosResponse>('/certificates', params, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
export const getCertificate = (id: string) => api.get<never, AxiosResponse<CertificateDTO>>(`/certificates/${id}`)
|
||||
|
||||
export const patchCertificate = (id: string, certificate: EditCertificateDTO) =>
|
||||
api.put(`/certificates/${id}`, certificate, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
export const deleteCertificate = (id: string) => api.delete(`/certificates/${id}`);
|
||||
24
client/src/features/certificates/duck/dto.ts
Normal file
24
client/src/features/certificates/duck/dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface CertificateDTO {
|
||||
name: string;
|
||||
domain: string;
|
||||
cert_type: string;
|
||||
expires_at: string;
|
||||
auto_renew: boolean;
|
||||
id: string
|
||||
created_at: string
|
||||
certificate_pem: string
|
||||
has_private_key: boolean
|
||||
}
|
||||
|
||||
export interface CreateCertificateDTO {
|
||||
name: string;
|
||||
domain: string;
|
||||
cert_type: string;
|
||||
auto_renew: boolean;
|
||||
}
|
||||
|
||||
export interface EditCertificateDTO {
|
||||
name: string
|
||||
domain: string
|
||||
auto_renew: boolean
|
||||
}
|
||||
4
client/src/features/certificates/duck/index.ts
Normal file
4
client/src/features/certificates/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './actions'
|
||||
export * from './dto'
|
||||
export * from './slice'
|
||||
export * from './selectors'
|
||||
3
client/src/features/certificates/duck/selectors.ts
Normal file
3
client/src/features/certificates/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { RootState } from '../../../store';
|
||||
|
||||
export const getCertificatesState = (state: RootState) => state.certificates;
|
||||
32
client/src/features/certificates/duck/slice.ts
Normal file
32
client/src/features/certificates/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { CertificateDTO } from './dto';
|
||||
|
||||
export interface CertificatesState {
|
||||
loading: boolean;
|
||||
certificates: CertificateDTO[]
|
||||
error: null | string;
|
||||
}
|
||||
|
||||
const initialState: CertificatesState = {
|
||||
loading: false,
|
||||
certificates: [],
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const certificateSlice = createSlice({
|
||||
name: 'certificates',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
setUsers: (state, action: PayloadAction<CertificateDTO[]>) => {
|
||||
state.certificates = action.payload
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
state.error = action.payload
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
2
client/src/features/certificates/index.ts
Normal file
2
client/src/features/certificates/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './duck'
|
||||
export * from './components'
|
||||
4
client/src/features/index.ts
Normal file
4
client/src/features/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './servers'
|
||||
export * from './templates'
|
||||
export * from './users'
|
||||
export * from './certificates'
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useForm, type SubmitHandler } from 'react-hook-form';
|
||||
import { createServerAction } from '../../duck';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import type { CreateServerForm } from '../../types';
|
||||
|
||||
|
||||
|
||||
export const AddServer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { register, handleSubmit, reset } = useForm<CreateServerForm>();
|
||||
|
||||
const onSubmit: SubmitHandler<CreateServerForm> = (values) => {
|
||||
const data = {
|
||||
...values,
|
||||
grpc_port: parseInt(values.grpc_port)
|
||||
}
|
||||
dispatch(createServerAction(data)).then(() => {
|
||||
reset();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<h2>Add Server</h2>
|
||||
<form onSubmit={handleSubmit(onSubmit)} id="serverForm">
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input {...register('name', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Hostname:</label>
|
||||
<input {...register('hostname', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>gRPC Port:</label>
|
||||
<input {...register('grpc_port', { required: true })} />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Add Server
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './servers-list'
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useEffect, type FC } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@heroui/react';
|
||||
import type { CreateServerForm } from '../../types';
|
||||
import { getServer } from '../../duck/api';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import { updateServer } from '../../duck';
|
||||
|
||||
export interface ServerEditProps {
|
||||
serverId: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
}
|
||||
|
||||
export const ServerEdit: FC<ServerEditProps> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { serverId, isOpen, onOpenChange } = props;
|
||||
const { register, handleSubmit, reset } = useForm<CreateServerForm>();
|
||||
|
||||
useEffect(() => {
|
||||
getServer(serverId).then((response) => {
|
||||
const { data } = response;
|
||||
reset({
|
||||
...data,
|
||||
grpc_port: String(data.grpc_port),
|
||||
});
|
||||
});
|
||||
}, [serverId]);
|
||||
|
||||
const onSubmit = (values: CreateServerForm) => {
|
||||
const data = {
|
||||
...values,
|
||||
grpc_port: parseInt(values.grpc_port),
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateServer({
|
||||
id: serverId,
|
||||
server: data,
|
||||
}),
|
||||
).then(() => {
|
||||
|
||||
onOpenChange();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Modal Title
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input {...register('name', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Hostname:</label>
|
||||
<input {...register('hostname', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>gRPC Port:</label>
|
||||
<input
|
||||
type="number"
|
||||
{...register('grpc_port', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { FC } from 'react';
|
||||
import { useDisclosure } from '@heroui/react';
|
||||
import { deleteServerAction, type ServerDTO } from '../../duck';
|
||||
import { testServer } from '../../duck/api';
|
||||
import { appNotificator } from '../../../../utils/notification/app-notificator';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import { ServerEdit } from './server-edit';
|
||||
|
||||
export interface ServerViewProps {
|
||||
server: ServerDTO;
|
||||
}
|
||||
|
||||
export const ServerView: FC<ServerViewProps> = ({ server }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleTestServer = () => {
|
||||
testServer(server.id).then((result) => {
|
||||
const { connected } = result.data;
|
||||
appNotificator.add({
|
||||
message: connected ? 'Connection OK' : 'Connection failed',
|
||||
type: connected ? 'success' : 'error',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteServer = () => {
|
||||
if (confirm('Delete server?')) {
|
||||
dispatch(deleteServerAction(server.id));
|
||||
}
|
||||
};
|
||||
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>{server.name}</td>
|
||||
<td>{server.hostname}</td>
|
||||
<td>{server.grpc_port}</td>
|
||||
<td>{server.status}</td>
|
||||
<td>
|
||||
<button className="btn btn-success" onClick={handleTestServer}>
|
||||
Test
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={onOpen}>
|
||||
Edit
|
||||
</button>
|
||||
<button className="btn btn-danger" onClick={handleDeleteServer}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<ServerEdit
|
||||
serverId={server.id}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { FC } from 'react';
|
||||
import type { ServerDTO } from '../../duck';
|
||||
import { ServerView } from './server-view';
|
||||
|
||||
export interface ServersListProps {
|
||||
servers: ServerDTO[];
|
||||
}
|
||||
|
||||
export const ServersList: FC<ServersListProps> = (props) => {
|
||||
const { servers } = props;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Hostname</th>
|
||||
<th>Port</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{servers.map((server) => (
|
||||
<ServerView key={server.id} server={server} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
99
client/src/features/servers/duck/actions.ts
Normal file
99
client/src/features/servers/duck/actions.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { serversSlice } from './slice';
|
||||
import { createServer, deleteServer, getServers, patchServer } from './api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { getServersState } from './selectors';
|
||||
import type { RootState } from '../../../store';
|
||||
import type { CreateServerDTO } from './dto';
|
||||
import { appNotificator } from '../../../utils/notification/app-notificator';
|
||||
|
||||
const PREFFIX = 'servers';
|
||||
|
||||
export const fetchServers = createAsyncThunk(
|
||||
`${PREFFIX}/fetchAll`,
|
||||
async (_, { dispatch, getState }) => {
|
||||
const { loading } = getServersState(getState() as RootState);
|
||||
try {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(serversSlice.actions.setLoading(true));
|
||||
const response = await getServers().then(({ data }) => data);
|
||||
dispatch(serversSlice.actions.setServers(response));
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||
dispatch(serversSlice.actions.setError(message));
|
||||
} finally {
|
||||
dispatch(serversSlice.actions.setLoading(false));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const createServerAction = createAsyncThunk(
|
||||
`${PREFFIX}/createServer`,
|
||||
async (params: CreateServerDTO, { dispatch }) => {
|
||||
try {
|
||||
await createServer(params);
|
||||
dispatch(fetchServers());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
message:
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: `Unknown error in ${PREFFIX}/createServer`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteServerAction = createAsyncThunk(
|
||||
`${PREFFIX}/deleteServer`,
|
||||
async (id: string, { dispatch }) => {
|
||||
try {
|
||||
await deleteServer(id);
|
||||
appNotificator.add({
|
||||
message: 'Server deleted',
|
||||
type: 'success',
|
||||
});
|
||||
dispatch(fetchServers());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Delete error: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/deleteServer`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const updateServer = createAsyncThunk(
|
||||
`${PREFFIX}/updateServer`,
|
||||
async (
|
||||
params: {
|
||||
id: string;
|
||||
server: CreateServerDTO;
|
||||
},
|
||||
{ dispatch },
|
||||
) => {
|
||||
try {
|
||||
await patchServer(params.id, params.server);
|
||||
dispatch(fetchServers());
|
||||
appNotificator.add({
|
||||
message: 'Server updated',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Error updating: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/deleteServer`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
28
client/src/features/servers/duck/api.ts
Normal file
28
client/src/features/servers/duck/api.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { api } from '../../../api/api';
|
||||
import type { ServerDTO, CreateServerDTO, TestServerDTO } from './dto';
|
||||
|
||||
export const getServers = () =>
|
||||
api.get<never, AxiosResponse<ServerDTO[]>>('/servers');
|
||||
|
||||
export const createServer = (params: CreateServerDTO) =>
|
||||
api.post<AxiosResponse>('servers', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export const testServer = (id: string) =>
|
||||
api.post<TestServerDTO>(`/servers/${id}/test`);
|
||||
|
||||
export const deleteServer = (id: string) => api.delete(`/servers/${id}`);
|
||||
|
||||
export const getServer = (id: string) =>
|
||||
api.get<string, AxiosResponse<ServerDTO>>(`/servers/${id}`);
|
||||
|
||||
export const patchServer = (id: string, server: CreateServerDTO) =>
|
||||
api.put(`/servers/${id}`, server, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
|
||||
18
client/src/features/servers/duck/dto.ts
Normal file
18
client/src/features/servers/duck/dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface ServerDTO {
|
||||
id: string
|
||||
name: string
|
||||
hostname: string
|
||||
grpc_port: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface CreateServerDTO {
|
||||
name: string;
|
||||
hostname: string;
|
||||
grpc_port: number;
|
||||
}
|
||||
|
||||
export interface TestServerDTO {
|
||||
connected: boolean,
|
||||
endpoint: string
|
||||
}
|
||||
4
client/src/features/servers/duck/index.ts
Normal file
4
client/src/features/servers/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './actions'
|
||||
export * from './dto'
|
||||
export * from './slice'
|
||||
export * from './selectors'
|
||||
3
client/src/features/servers/duck/selectors.ts
Normal file
3
client/src/features/servers/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { RootState } from '../../../store';
|
||||
|
||||
export const getServersState = (state: RootState) => state.servers;
|
||||
32
client/src/features/servers/duck/slice.ts
Normal file
32
client/src/features/servers/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { ServerDTO } from './dto';
|
||||
|
||||
export interface ServersState {
|
||||
loading: boolean;
|
||||
servers: ServerDTO[];
|
||||
error: null | string;
|
||||
}
|
||||
|
||||
const initialState: ServersState = {
|
||||
loading: false,
|
||||
servers: [],
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const serversSlice = createSlice({
|
||||
name: 'servers',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
setServers: (state, action: PayloadAction<ServerDTO[]>) => {
|
||||
state.servers = action.payload
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
state.error = action.payload
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
1
client/src/features/servers/index.ts
Normal file
1
client/src/features/servers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './duck'
|
||||
5
client/src/features/servers/types/form.ts
Normal file
5
client/src/features/servers/types/form.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface CreateServerForm {
|
||||
name: string;
|
||||
hostname: string;
|
||||
grpc_port: string;
|
||||
}
|
||||
1
client/src/features/servers/types/index.ts
Normal file
1
client/src/features/servers/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './form'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useForm, type SubmitHandler } from 'react-hook-form'
|
||||
import type { CreateTemplateForm } from '../../types';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import { protocolOptions } from './util'
|
||||
import type { CreateTemplateDTO } from '../../duck/dto';
|
||||
import { createTemplateAction } from '../../duck';
|
||||
|
||||
|
||||
export const AddTemplate = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { register, handleSubmit, reset } = useForm<CreateTemplateForm>({
|
||||
defaultValues: {
|
||||
default_port: '443'
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<CreateTemplateForm> = (values) => {
|
||||
const data: CreateTemplateDTO = {
|
||||
...values,
|
||||
default_port: parseInt(values.default_port),
|
||||
config_template: ''
|
||||
}
|
||||
dispatch(createTemplateAction(data)).then(() => {
|
||||
reset();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} id="templateForm">
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" {...register('name', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Protocol:</label>
|
||||
<select {...register('protocol', {required: true})}>
|
||||
{Object.entries(protocolOptions).map((protocolTupple)=> (
|
||||
<option value={protocolTupple[0]}>{protocolTupple[1]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Default Port:</label>
|
||||
<input type="number" {...register('default_port', {required: true})} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input type="checkbox" {...register('requires_tls')}/> Requires TLS
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Add Template
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './add-template'
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Protocol } from '../../duck/dto'
|
||||
|
||||
export const protocolOptions: Record<Protocol, string> = {
|
||||
vless: 'VLESS',
|
||||
vmess: 'VMess',
|
||||
trojan: 'Trojan',
|
||||
shadowsocks: 'Shadowsocks'
|
||||
}
|
||||
|
||||
2
client/src/features/templates/components/index.ts
Normal file
2
client/src/features/templates/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './add-template'
|
||||
export * from './template-list'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './template-list'
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useEffect, type FC } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@heroui/react';
|
||||
import type { EditTemplateForm } from '../../types';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import { getTemplateById } from '../../duck/api';
|
||||
import { protocolOptions } from '../add-template/util';
|
||||
import { updateTemplate } from '../../duck';
|
||||
|
||||
export interface TemplateEditProps {
|
||||
templateId: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
}
|
||||
|
||||
export const TemplateEdit: FC<TemplateEditProps> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { templateId, isOpen, onOpenChange } = props;
|
||||
const { register, handleSubmit, reset } = useForm<EditTemplateForm>();
|
||||
|
||||
useEffect(() => {
|
||||
getTemplateById(templateId).then((response) => {
|
||||
const { data } = response;
|
||||
reset({
|
||||
...data,
|
||||
default_port: String(data.default_port),
|
||||
});
|
||||
});
|
||||
}, [templateId]);
|
||||
|
||||
const onSubmit = (values: EditTemplateForm) => {
|
||||
const data = {
|
||||
...values,
|
||||
default_port: parseInt(values.default_port),
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateTemplate({
|
||||
id: templateId,
|
||||
template: data,
|
||||
}),
|
||||
).then(() => {
|
||||
onOpenChange();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Modal Title
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('name', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Protocol:</label>
|
||||
<select
|
||||
id="editProtocol"
|
||||
{...register('protocol', { required: true })}
|
||||
>
|
||||
{Object.entries(protocolOptions).map((protocolTupple) => (
|
||||
<option value={protocolTupple[0]}>
|
||||
{protocolTupple[1]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Default Port:</label>
|
||||
<input
|
||||
type="number"
|
||||
{...register('default_port', {required: true})}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input type="checkbox" {...register('requires_tls')} />{' '}
|
||||
Requires TLS
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input type="checkbox" {...register('is_active')} />{' '}
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { FC } from 'react';
|
||||
import type { TemplateDTO } from '../../duck';
|
||||
import { TemplateView } from './template-view';
|
||||
|
||||
export interface TemplateListProps {
|
||||
templates: TemplateDTO[];
|
||||
}
|
||||
|
||||
export const TemplateList: FC<TemplateListProps> = ({ templates }) => {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Protocol</th>
|
||||
<th>Port</th>
|
||||
<th>TLS</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{templates.map((template) => (
|
||||
<TemplateView template={template} key={template.id}/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { FC } from 'react';
|
||||
import { deleteTemplateAction, type TemplateDTO } from '../../duck';
|
||||
import { useDisclosure } from '@heroui/react';
|
||||
import { TemplateEdit } from './template-edit';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
|
||||
export interface TemplateViewProps {
|
||||
template: TemplateDTO;
|
||||
}
|
||||
|
||||
export const TemplateView: FC<TemplateViewProps> = ({ template }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
|
||||
const handleDeleteTemplate = () => {
|
||||
if (confirm('Delete template?')) {
|
||||
dispatch(deleteTemplateAction(template.id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>{template.name}</td>
|
||||
<td>{template.protocol}</td>
|
||||
<td>{template.default_port}</td>
|
||||
<td>{template.requires_tls ? 'Yes' : 'No'}</td>
|
||||
<td>{template.is_active ? 'Yes' : 'No'}</td>
|
||||
<td>
|
||||
<button className="btn btn-primary" onClick={onOpen}>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleDeleteTemplate}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<TemplateEdit
|
||||
templateId={template.id}
|
||||
onOpenChange={onOpenChange}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
102
client/src/features/templates/duck/actions.ts
Normal file
102
client/src/features/templates/duck/actions.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { templatesSlice } from './slice';
|
||||
import { getTemplates, createTemplate, patchTemplate, deleteTemplate } from './api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { getTemplatesState } from './selectors';
|
||||
import type { RootState } from '../../../store';
|
||||
import type { CreateTemplateDTO, EditTemplateDTO } from './dto';
|
||||
import { appNotificator } from '../../../utils/notification/app-notificator';
|
||||
|
||||
const PREFFIX = 'templates';
|
||||
|
||||
export const fetchTemplates = createAsyncThunk(
|
||||
`${PREFFIX}/fetchAll`,
|
||||
async (_, { dispatch, getState }) => {
|
||||
const { loading } = getTemplatesState(getState() as RootState);
|
||||
try {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(templatesSlice.actions.setLoading(true));
|
||||
const response = await getTemplates().then(({ data }) => data);
|
||||
dispatch(templatesSlice.actions.setTemplates(response));
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||
dispatch(templatesSlice.actions.setError(message));
|
||||
} finally {
|
||||
dispatch(templatesSlice.actions.setLoading(false));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
export const createTemplateAction = createAsyncThunk(
|
||||
`${PREFFIX}/createTemplate`,
|
||||
async (params: CreateTemplateDTO, { dispatch }) => {
|
||||
try {
|
||||
await createTemplate(params);
|
||||
dispatch(fetchTemplates());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
message:
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: `Unknown error in ${PREFFIX}/createTemplate`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
|
||||
export const updateTemplate = createAsyncThunk(
|
||||
`${PREFFIX}/updateTemplate`,
|
||||
async (
|
||||
params: {
|
||||
id: string;
|
||||
template: EditTemplateDTO;
|
||||
},
|
||||
{ dispatch },
|
||||
) => {
|
||||
try {
|
||||
await patchTemplate(params.id, params.template);
|
||||
dispatch(fetchTemplates());
|
||||
appNotificator.add({
|
||||
message: 'Template updated',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Error updating: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/updateTemplate`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteTemplateAction = createAsyncThunk(
|
||||
`${PREFFIX}/deleteTemplate`,
|
||||
async (id: string, { dispatch }) => {
|
||||
try {
|
||||
await deleteTemplate(id);
|
||||
appNotificator.add({
|
||||
message: 'Template deleted',
|
||||
type: 'success',
|
||||
});
|
||||
dispatch(fetchTemplates());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Delete error: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/deleteTemplate`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
23
client/src/features/templates/duck/api.ts
Normal file
23
client/src/features/templates/duck/api.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { api } from '../../../api/api';
|
||||
import type { TemplateDTO, CreateTemplateDTO, EditTemplateDTO } from './dto';
|
||||
|
||||
export const getTemplates = () =>
|
||||
api.get<never, AxiosResponse<TemplateDTO[]>>('/templates');
|
||||
|
||||
export const createTemplate = (params: CreateTemplateDTO) =>
|
||||
api.post<AxiosResponse>('templates', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export const getTemplateById = (id: string) =>
|
||||
api.get<string, AxiosResponse<TemplateDTO>>(`/templates/${id}`);
|
||||
|
||||
export const patchTemplate = (id: string, template: EditTemplateDTO) =>
|
||||
api.put(`/templates/${id}`, template, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
export const deleteTemplate = (id: string) => api.delete(`/templates/${id}`);
|
||||
33
client/src/features/templates/duck/dto.ts
Normal file
33
client/src/features/templates/duck/dto.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type Protocol = 'vless' | 'vmess' | 'trojan' | 'shadowsocks';
|
||||
|
||||
export interface TemplateDTO {
|
||||
base_settings: Record<string, unknown>; // TODO define unknown
|
||||
created_at: string;
|
||||
default_port: number;
|
||||
description: string;
|
||||
id: string;
|
||||
is_active: boolean;
|
||||
name: string;
|
||||
protocol: Protocol;
|
||||
requires_domain: boolean;
|
||||
requires_tls: boolean;
|
||||
stream_settings: Record<string, unknown>; // TOD define unknown
|
||||
updated_at: string;
|
||||
variables: unknown[]; // TOD define unknown
|
||||
}
|
||||
|
||||
export interface CreateTemplateDTO {
|
||||
name: string;
|
||||
protocol: Protocol;
|
||||
default_port: number;
|
||||
requires_tls: boolean;
|
||||
config_template: '';
|
||||
}
|
||||
|
||||
export interface EditTemplateDTO {
|
||||
name: string,
|
||||
protocol: Protocol
|
||||
default_port: number
|
||||
requires_tls: boolean
|
||||
is_active: boolean
|
||||
}
|
||||
4
client/src/features/templates/duck/index.ts
Normal file
4
client/src/features/templates/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './actions'
|
||||
export * from './dto'
|
||||
export * from './slice'
|
||||
export * from './selectors'
|
||||
3
client/src/features/templates/duck/selectors.ts
Normal file
3
client/src/features/templates/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { RootState } from '../../../store';
|
||||
|
||||
export const getTemplatesState = (state: RootState) => state.templates;
|
||||
32
client/src/features/templates/duck/slice.ts
Normal file
32
client/src/features/templates/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { TemplateDTO } from './dto';
|
||||
|
||||
export interface TemplateState {
|
||||
loading: boolean;
|
||||
templates: TemplateDTO[];
|
||||
error: null | string;
|
||||
}
|
||||
|
||||
const initialState: TemplateState = {
|
||||
loading: false,
|
||||
templates: [],
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const templatesSlice = createSlice({
|
||||
name: 'templates',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
setTemplates: (state, action: PayloadAction<TemplateDTO[]>) => {
|
||||
state.templates = action.payload
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
state.error = action.payload
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
2
client/src/features/templates/index.ts
Normal file
2
client/src/features/templates/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './duck'
|
||||
export * from './components'
|
||||
16
client/src/features/templates/types/form.ts
Normal file
16
client/src/features/templates/types/form.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Protocol } from "../duck/dto"
|
||||
|
||||
export interface CreateTemplateForm {
|
||||
name: string,
|
||||
protocol: Protocol
|
||||
default_port: string
|
||||
requires_tls: boolean
|
||||
}
|
||||
|
||||
export interface EditTemplateForm {
|
||||
name: string,
|
||||
protocol: Protocol
|
||||
default_port: string
|
||||
requires_tls: boolean
|
||||
is_active: boolean
|
||||
}
|
||||
1
client/src/features/templates/types/index.ts
Normal file
1
client/src/features/templates/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './form'
|
||||
30
client/src/features/users/duck/actions.ts
Normal file
30
client/src/features/users/duck/actions.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { usersSlice } from './slice';
|
||||
import { getUsers } from './api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { getUsersState } from './selectors';
|
||||
import type { RootState } from '../../../store';
|
||||
|
||||
const PREFFIX = 'users'
|
||||
|
||||
export const fetchUsers = createAsyncThunk(
|
||||
`${PREFFIX}/fetchAll`,
|
||||
async (_, { dispatch, getState }) => {
|
||||
const { loading } = getUsersState(getState() as RootState);
|
||||
try {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(usersSlice.actions.setLoading(true));
|
||||
const response = await getUsers().then(({ data }) => data);
|
||||
dispatch(usersSlice.actions.setUsers(response));
|
||||
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||
dispatch(usersSlice.actions.setError(message));
|
||||
} finally {
|
||||
dispatch(usersSlice.actions.setLoading(false));
|
||||
}
|
||||
},
|
||||
);
|
||||
5
client/src/features/users/duck/api.ts
Normal file
5
client/src/features/users/duck/api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { api } from '../../../api/api';
|
||||
import type { UserDTO } from './dto';
|
||||
|
||||
export const getUsers = () => api.get<never, AxiosResponse<UserDTO[]>>('/users');
|
||||
8
client/src/features/users/duck/dto.ts
Normal file
8
client/src/features/users/duck/dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface User {}
|
||||
|
||||
export interface UserDTO {
|
||||
page: number
|
||||
per_page: number
|
||||
total: number
|
||||
users: User[]
|
||||
}
|
||||
4
client/src/features/users/duck/index.ts
Normal file
4
client/src/features/users/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './actions'
|
||||
export * from './dto'
|
||||
export * from './slice'
|
||||
export * from './selectors'
|
||||
3
client/src/features/users/duck/selectors.ts
Normal file
3
client/src/features/users/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { RootState } from '../../../store';
|
||||
|
||||
export const getUsersState = (state: RootState) => state.users;
|
||||
32
client/src/features/users/duck/slice.ts
Normal file
32
client/src/features/users/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { UserDTO, User } from './dto';
|
||||
|
||||
export interface UsersState {
|
||||
loading: boolean;
|
||||
users: User[]
|
||||
error: null | string;
|
||||
}
|
||||
|
||||
const initialState: UsersState = {
|
||||
loading: false,
|
||||
users: [],
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const usersSlice = createSlice({
|
||||
name: 'certificate',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
setUsers: (state, action: PayloadAction<UserDTO[]>) => {
|
||||
state.users = action.payload.users
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
state.error = action.payload
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
1
client/src/features/users/index.ts
Normal file
1
client/src/features/users/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './duck'
|
||||
2
client/src/hero.ts
Normal file
2
client/src/hero.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { heroui } from "@heroui/react";
|
||||
export default heroui();
|
||||
5
client/src/index.css
Normal file
5
client/src/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@plugin './hero.ts';
|
||||
|
||||
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
22
client/src/main.tsx
Normal file
22
client/src/main.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { store } from './store/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HeroUIProvider } from '@heroui/react';
|
||||
import {ToastProvider} from "@heroui/toast";
|
||||
import { router } from './router';
|
||||
import './index.css';
|
||||
import { ApplyNotificator } from './common/components/apply-notificator';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<HeroUIProvider>
|
||||
<RouterProvider router={router} />
|
||||
<ToastProvider/>
|
||||
<ApplyNotificator/>
|
||||
</HeroUIProvider>
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
);
|
||||
38
client/src/pages/certificates/certificates.tsx
Normal file
38
client/src/pages/certificates/certificates.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { RouteObject } from 'react-router';
|
||||
import { useAppDispatch, useAppSelector } from '../../common/hooks';
|
||||
import { useEffect } from 'react';
|
||||
import { fetchCertificates, getCertificatesState } from '../../features';
|
||||
import { CreateCertificate } from '../../features/certificates';
|
||||
import { CertificateList } from '../../features/certificates/components/certificate-list';
|
||||
|
||||
export const Certificates = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { loading, certificates } = useAppSelector(getCertificatesState)
|
||||
|
||||
useEffect(()=>{
|
||||
dispatch(fetchCertificates())
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<div id="certificates" className="tab-content active">
|
||||
<div className="section">
|
||||
<h2>Add Certificate</h2>
|
||||
<CreateCertificate/>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>Certificates List</h2>
|
||||
<div id="certificatesList" className="loading">
|
||||
{ loading && 'Loading...' }
|
||||
{ certificates.length ? <CertificateList certificates={certificates}/> : <p>No certificates found</p> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CertificatesRoute: RouteObject = {
|
||||
path: '/certificates',
|
||||
Component: Certificates,
|
||||
};
|
||||
1
client/src/pages/certificates/index.ts
Normal file
1
client/src/pages/certificates/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './certificates';
|
||||
72
client/src/pages/dashboard/dashboard.tsx
Normal file
72
client/src/pages/dashboard/dashboard.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { RouteObject } from 'react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../common/hooks';
|
||||
import {
|
||||
fetchServers,
|
||||
getServersState,
|
||||
fetchTemplates,
|
||||
getTemplatesState,
|
||||
fetchUsers,
|
||||
getUsersState,
|
||||
getCertificatesState,
|
||||
fetchCertificates,
|
||||
} from '../../features';
|
||||
|
||||
export const Dashboard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading: serverLoading, servers } = useAppSelector(getServersState);
|
||||
const { loading: usersLoading, users } = useAppSelector(getUsersState);
|
||||
const { loading: certificatesLoading, certificates } =
|
||||
useAppSelector(getCertificatesState);
|
||||
const { loading: templatesLoading, templates } =
|
||||
useAppSelector(getTemplatesState);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchServers());
|
||||
dispatch(fetchTemplates());
|
||||
dispatch(fetchUsers());
|
||||
dispatch(fetchCertificates());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div id="dashboard" className="tab-content active">
|
||||
<div className="section">
|
||||
<h2>Statistics</h2>
|
||||
<p>
|
||||
Servers:{' '}
|
||||
<span id="serverCount">
|
||||
{serverLoading === true && 'Loading...'}
|
||||
{servers && String(servers.length)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Templates:{' '}
|
||||
<span id="templateCount">
|
||||
{templatesLoading && 'Loading...'}
|
||||
{templates && String(templates.length)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Certificates:{' '}
|
||||
<span id="certCount">
|
||||
{certificatesLoading && 'Loading...'}
|
||||
{certificates && String(certificates.length)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Users:{' '}
|
||||
<span id="userCount">
|
||||
{usersLoading && 'Loading...'}
|
||||
{users && String(users.length)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DashboardRoute: RouteObject = {
|
||||
index: true,
|
||||
path: '/',
|
||||
Component: Dashboard,
|
||||
};
|
||||
1
client/src/pages/dashboard/index.ts
Normal file
1
client/src/pages/dashboard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dashboard';
|
||||
238
client/src/pages/home/home.css
Normal file
238
client/src/pages/home/home.css
Normal file
@@ -0,0 +1,238 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.section {
|
||||
background: white;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
h2 {
|
||||
color: #666;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
margin: 2px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.form-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 300px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
/* Toast notifications */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
margin-bottom: 10px;
|
||||
padding: 16px 20px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
transform: translateX(100%);
|
||||
transition:
|
||||
transform 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out;
|
||||
opacity: 0;
|
||||
pointer-events: auto;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.toast.show {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
.toast.success {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
.toast.error {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
.toast-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.toast-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.toast-close:hover {
|
||||
color: #666;
|
||||
}
|
||||
.toast-body {
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.toast.success .toast-title {
|
||||
color: #155724;
|
||||
}
|
||||
.toast.error .toast-title {
|
||||
color: #721c24;
|
||||
}
|
||||
.tabs {
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tab {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.tab.active {
|
||||
border-bottom-color: #007bff;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.modal.show {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-close:hover {
|
||||
color: #666;
|
||||
}
|
||||
.modal-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
}
|
||||
.cert-details {
|
||||
font-family: monospace;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
82
client/src/pages/home/home.tsx
Normal file
82
client/src/pages/home/home.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Outlet } from 'react-router';
|
||||
import './home.css';
|
||||
import { NavMenu } from '../../components/nav-menu/nav-menu';
|
||||
import { navItems } from './utils';
|
||||
|
||||
export const Home = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="container">
|
||||
<h1 className="text-3xl font-bold underline">Xray Admin Panel - Test Interface</h1>
|
||||
|
||||
{/* <!-- Toast notifications container --> */}
|
||||
<div className="toast-container" id="toastContainer"></div>
|
||||
|
||||
<NavMenu items={navItems} />
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* <!-- Modal dialogs --> */}
|
||||
<div id="editModal" className="modal">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<div className="modal-title" id="editModalTitle">
|
||||
Edit Item
|
||||
</div>
|
||||
<button
|
||||
className="modal-close"
|
||||
// onClick="closeModal('editModal')"
|
||||
>
|
||||
×
|
||||
</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
client/src/pages/home/index.ts
Normal file
1
client/src/pages/home/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './home';
|
||||
26
client/src/pages/home/utils.ts
Normal file
26
client/src/pages/home/utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const navItems = [
|
||||
{
|
||||
href: '/',
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
href: '/servers',
|
||||
label: 'Servers',
|
||||
},
|
||||
{
|
||||
href: '/inbound-templates',
|
||||
label: 'Inbound Templates',
|
||||
},
|
||||
{
|
||||
href: '/certificates',
|
||||
label: 'Certificates',
|
||||
},
|
||||
{
|
||||
href: '/inbound-binding',
|
||||
label: 'Inbound Binding',
|
||||
},
|
||||
{
|
||||
href: '/users',
|
||||
label: 'Users',
|
||||
},
|
||||
];
|
||||
55
client/src/pages/inbound-binding/inbound-binding.tsx
Normal file
55
client/src/pages/inbound-binding/inbound-binding.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { RouteObject } from 'react-router';
|
||||
|
||||
export const InboundBinding = () => {
|
||||
return (
|
||||
<div id="inbounds" className="tab-content active">
|
||||
<div className="section">
|
||||
<h2>Bind Template to Server</h2>
|
||||
<form id="inboundForm">
|
||||
<div className="form-group">
|
||||
<label>Server:</label>
|
||||
<select id="inboundServer" required>
|
||||
<option value="">Select Server...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Template:</label>
|
||||
<select id="inboundTemplate" required>
|
||||
<option value="">Select Template...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Port:</label>
|
||||
<input type="number" id="inboundPort" value="443" required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Certificate:</label>
|
||||
<select id="inboundCertificate">
|
||||
<option value="">No Certificate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="inboundActive" checked /> Active
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Bind Template
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>Server Inbounds</h2>
|
||||
<div id="inboundsList" className="loading">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InboundBindingRoute: RouteObject = {
|
||||
path: '/inbound-binding',
|
||||
Component: InboundBinding,
|
||||
};
|
||||
1
client/src/pages/inbound-binding/index.ts
Normal file
1
client/src/pages/inbound-binding/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './inbound-binding';
|
||||
39
client/src/pages/inbound-templates/inboud-templates.tsx
Normal file
39
client/src/pages/inbound-templates/inboud-templates.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
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
client/src/pages/inbound-templates/index.ts
Normal file
1
client/src/pages/inbound-templates/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './inboud-templates';
|
||||
7
client/src/pages/index.ts
Normal file
7
client/src/pages/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './home';
|
||||
export * from './certificates';
|
||||
export * from './dashboard';
|
||||
export * from './inbound-binding';
|
||||
export * from './inbound-templates';
|
||||
export * from './servers';
|
||||
export * from './users';
|
||||
1
client/src/pages/servers/index.ts
Normal file
1
client/src/pages/servers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './servers';
|
||||
39
client/src/pages/servers/servers.tsx
Normal file
39
client/src/pages/servers/servers.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
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
client/src/pages/users/index.ts
Normal file
1
client/src/pages/users/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
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