Commit 0170d19f authored by song's avatar song
Browse files

merge upstream main

parent 7ade9baa
...@@ -62,6 +62,7 @@ services: ...@@ -62,6 +62,7 @@ services:
- REDIS_PORT=6379 - REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0} - REDIS_DB=${REDIS_DB:-0}
- REDIS_ENABLE_TLS=${REDIS_ENABLE_TLS:-false}
# ======================================================================= # =======================================================================
# Admin Account (auto-created on first run) # Admin Account (auto-created on first run)
...@@ -79,6 +80,16 @@ services: ...@@ -79,6 +80,16 @@ services:
- JWT_SECRET=${JWT_SECRET:-} - JWT_SECRET=${JWT_SECRET:-}
- JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24} - JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
# =======================================================================
# TOTP (2FA) Configuration
# =======================================================================
# IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty,
# a random key will be generated on each startup, causing all existing
# TOTP configurations to become invalid (users won't be able to login
# with 2FA).
# Generate a secure key: openssl rand -hex 32
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:-}
# ======================================================================= # =======================================================================
# Timezone Configuration # Timezone Configuration
# This affects ALL time operations in the application: # This affects ALL time operations in the application:
......
#!/bin/bash
# =============================================================================
# Sub2API Docker Deployment Preparation Script
# =============================================================================
# This script prepares deployment files for Sub2API:
# - Downloads docker-compose.local.yml and .env.example
# - Generates secure secrets (JWT_SECRET, TOTP_ENCRYPTION_KEY, POSTGRES_PASSWORD)
# - Creates necessary data directories
#
# After running this script, you can start services with:
# docker-compose -f docker-compose.local.yml up -d
# =============================================================================
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# GitHub raw content base URL
GITHUB_RAW_URL="https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy"
# Print colored message
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Generate random secret
generate_secret() {
openssl rand -hex 32
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Main installation function
main() {
echo ""
echo "=========================================="
echo " Sub2API Deployment Preparation"
echo "=========================================="
echo ""
# Check if openssl is available
if ! command_exists openssl; then
print_error "openssl is not installed. Please install openssl first."
exit 1
fi
# Check if deployment already exists
if [ -f "docker-compose.local.yml" ] && [ -f ".env" ]; then
print_warning "Deployment files already exist in current directory."
read -p "Overwrite existing files? (y/N): " -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Cancelled."
exit 0
fi
fi
# Download docker-compose.local.yml
print_info "Downloading docker-compose.local.yml..."
if command_exists curl; then
curl -sSL "${GITHUB_RAW_URL}/docker-compose.local.yml" -o docker-compose.local.yml
elif command_exists wget; then
wget -q "${GITHUB_RAW_URL}/docker-compose.local.yml" -O docker-compose.local.yml
else
print_error "Neither curl nor wget is installed. Please install one of them."
exit 1
fi
print_success "Downloaded docker-compose.local.yml"
# Download .env.example
print_info "Downloading .env.example..."
if command_exists curl; then
curl -sSL "${GITHUB_RAW_URL}/.env.example" -o .env.example
else
wget -q "${GITHUB_RAW_URL}/.env.example" -O .env.example
fi
print_success "Downloaded .env.example"
# Generate .env file with auto-generated secrets
print_info "Generating secure secrets..."
echo ""
# Generate secrets
JWT_SECRET=$(generate_secret)
TOTP_ENCRYPTION_KEY=$(generate_secret)
POSTGRES_PASSWORD=$(generate_secret)
# Create .env from .env.example
cp .env.example .env
# Update .env with generated secrets (cross-platform compatible)
if sed --version >/dev/null 2>&1; then
# GNU sed (Linux)
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env
else
# BSD sed (macOS)
sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i '' "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env
sed -i '' "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env
fi
# Create data directories
print_info "Creating data directories..."
mkdir -p data postgres_data redis_data
print_success "Created data directories"
# Set secure permissions for .env file (readable/writable only by owner)
chmod 600 .env
echo ""
# Display completion message
echo "=========================================="
echo " Preparation Complete!"
echo "=========================================="
echo ""
echo "Generated secure credentials:"
echo " POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}"
echo " JWT_SECRET: ${JWT_SECRET}"
echo " TOTP_ENCRYPTION_KEY: ${TOTP_ENCRYPTION_KEY}"
echo ""
print_warning "These credentials have been saved to .env file."
print_warning "Please keep them secure and do not share publicly!"
echo ""
echo "Directory structure:"
echo " docker-compose.local.yml - Docker Compose configuration"
echo " .env - Environment variables (generated secrets)"
echo " .env.example - Example template (for reference)"
echo " data/ - Application data (will be created on first run)"
echo " postgres_data/ - PostgreSQL data"
echo " redis_data/ - Redis data"
echo ""
echo "Next steps:"
echo " 1. (Optional) Edit .env to customize configuration"
echo " 2. Start services:"
echo " docker-compose -f docker-compose.local.yml up -d"
echo ""
echo " 3. View logs:"
echo " docker-compose -f docker-compose.local.yml logs -f sub2api"
echo ""
echo " 4. Access Web UI:"
echo " http://localhost:8080"
echo ""
print_info "If admin password is not set in .env, it will be auto-generated."
print_info "Check logs for the generated admin password on first startup."
echo ""
}
# Run main function
main "$@"
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -19,9 +19,12 @@ ...@@ -19,9 +19,12 @@
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.7.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"chart.js": "^4.4.1", "chart.js": "^4.4.1",
"dompurify": "^3.3.1",
"driver.js": "^1.4.0", "driver.js": "^1.4.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"marked": "^17.0.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode": "^1.5.4",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-chartjs": "^5.3.0", "vue-chartjs": "^5.3.0",
"vue-i18n": "^9.14.5", "vue-i18n": "^9.14.5",
...@@ -29,9 +32,11 @@ ...@@ -29,9 +32,11 @@
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^20.10.5", "@types/node": "^20.10.5",
"@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
......
...@@ -20,15 +20,24 @@ importers: ...@@ -20,15 +20,24 @@ importers:
chart.js: chart.js:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.5.1 version: 4.5.1
dompurify:
specifier: ^3.3.1
version: 3.3.1
driver.js: driver.js:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
file-saver: file-saver:
specifier: ^2.0.5 specifier: ^2.0.5
version: 2.0.5 version: 2.0.5
marked:
specifier: ^17.0.1
version: 17.0.1
pinia: pinia:
specifier: ^2.1.7 specifier: ^2.1.7
version: 2.3.1(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3)) version: 2.3.1(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
qrcode:
specifier: ^1.5.4
version: 1.5.4
vue: vue:
specifier: ^3.4.0 specifier: ^3.4.0
version: 3.5.26(typescript@5.6.3) version: 3.5.26(typescript@5.6.3)
...@@ -45,6 +54,9 @@ importers: ...@@ -45,6 +54,9 @@ importers:
specifier: ^0.18.5 specifier: ^0.18.5
version: 0.18.5 version: 0.18.5
devDependencies: devDependencies:
'@types/dompurify':
specifier: ^3.0.5
version: 3.2.0
'@types/file-saver': '@types/file-saver':
specifier: ^2.0.7 specifier: ^2.0.7
version: 2.0.7 version: 2.0.7
...@@ -54,6 +66,9 @@ importers: ...@@ -54,6 +66,9 @@ importers:
'@types/node': '@types/node':
specifier: ^20.10.5 specifier: ^20.10.5
version: 20.19.27 version: 20.19.27
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^7.18.0 specifier: ^7.18.0
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)
...@@ -1239,56 +1254,67 @@ packages: ...@@ -1239,56 +1254,67 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0': '@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0': '@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0': '@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0': '@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.54.0': '@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.54.0': '@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0': '@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0': '@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0': '@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0': '@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.54.0': '@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
...@@ -1443,6 +1469,10 @@ packages: ...@@ -1443,6 +1469,10 @@ packages:
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/dompurify@3.2.0':
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
'@types/estree-jsx@1.0.5': '@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
...@@ -1479,6 +1509,9 @@ packages: ...@@ -1479,6 +1509,9 @@ packages:
'@types/parse-json@4.0.2': '@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
'@types/react@19.2.7': '@types/react@19.2.7':
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
...@@ -1832,6 +1865,10 @@ packages: ...@@ -1832,6 +1865,10 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001761: caniuse-lite@1.0.30001761:
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
...@@ -1895,6 +1932,9 @@ packages: ...@@ -1895,6 +1932,9 @@ packages:
classnames@2.5.1: classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clsx@1.2.1: clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'} engines: {node: '>=6'}
...@@ -2164,6 +2204,10 @@ packages: ...@@ -2164,6 +2204,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decimal.js@10.6.0: decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
...@@ -2198,6 +2242,9 @@ packages: ...@@ -2198,6 +2242,9 @@ packages:
didyoumean@1.2.2: didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dir-glob@3.0.1: dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -2424,6 +2471,10 @@ packages: ...@@ -2424,6 +2471,10 @@ packages:
find-root@1.1.0: find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
find-up@5.0.0: find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -2488,6 +2539,10 @@ packages: ...@@ -2488,6 +2539,10 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.4.0: get-east-asian-width@1.4.0:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
...@@ -2856,6 +2911,10 @@ packages: ...@@ -2856,6 +2911,10 @@ packages:
lit@3.3.2: lit@3.3.2:
resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -3239,14 +3298,26 @@ packages: ...@@ -3239,14 +3298,26 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-locate@5.0.0: p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
...@@ -3341,6 +3412,10 @@ packages: ...@@ -3341,6 +3412,10 @@ packages:
pkg-types@1.3.1: pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
points-on-curve@0.2.0: points-on-curve@0.2.0:
resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
...@@ -3421,6 +3496,11 @@ packages: ...@@ -3421,6 +3496,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
query-string@9.3.1: query-string@9.3.1:
resolution: {integrity: sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==} resolution: {integrity: sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==}
engines: {node: '>=18'} engines: {node: '>=18'}
...@@ -3664,6 +3744,13 @@ packages: ...@@ -3664,6 +3744,13 @@ packages:
remark-stringify@11.0.0: remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
requires-port@1.0.0: requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
...@@ -3739,6 +3826,9 @@ packages: ...@@ -3739,6 +3826,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-value@2.0.1: set-value@2.0.1:
resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -4263,6 +4353,9 @@ packages: ...@@ -4263,6 +4353,9 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'} engines: {node: '>=18'}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
...@@ -4285,6 +4378,10 @@ packages: ...@@ -4285,6 +4378,10 @@ packages:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -4324,10 +4421,21 @@ packages: ...@@ -4324,10 +4421,21 @@ packages:
xmlchars@2.2.0: xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yaml@1.10.2: yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -5806,6 +5914,10 @@ snapshots: ...@@ -5806,6 +5914,10 @@ snapshots:
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/dompurify@3.2.0':
dependencies:
dompurify: 3.3.1
'@types/estree-jsx@1.0.5': '@types/estree-jsx@1.0.5':
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
...@@ -5838,6 +5950,10 @@ snapshots: ...@@ -5838,6 +5950,10 @@ snapshots:
'@types/parse-json@4.0.2': {} '@types/parse-json@4.0.2': {}
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 20.19.27
'@types/react@19.2.7': '@types/react@19.2.7':
dependencies: dependencies:
csstype: 3.2.3 csstype: 3.2.3
...@@ -6321,6 +6437,8 @@ snapshots: ...@@ -6321,6 +6437,8 @@ snapshots:
camelcase-css@2.0.1: {} camelcase-css@2.0.1: {}
camelcase@5.3.1: {}
caniuse-lite@1.0.30001761: {} caniuse-lite@1.0.30001761: {}
ccount@2.0.1: {} ccount@2.0.1: {}
...@@ -6395,6 +6513,12 @@ snapshots: ...@@ -6395,6 +6513,12 @@ snapshots:
classnames@2.5.1: {} classnames@2.5.1: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
clsx@1.2.1: {} clsx@1.2.1: {}
clsx@2.1.1: {} clsx@2.1.1: {}
...@@ -6668,6 +6792,8 @@ snapshots: ...@@ -6668,6 +6792,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
decimal.js@10.6.0: {} decimal.js@10.6.0: {}
decode-named-character-reference@1.2.0: decode-named-character-reference@1.2.0:
...@@ -6694,6 +6820,8 @@ snapshots: ...@@ -6694,6 +6820,8 @@ snapshots:
didyoumean@1.2.2: {} didyoumean@1.2.2: {}
dijkstrajs@1.0.3: {}
dir-glob@3.0.1: dir-glob@3.0.1:
dependencies: dependencies:
path-type: 4.0.0 path-type: 4.0.0
...@@ -6978,6 +7106,11 @@ snapshots: ...@@ -6978,6 +7106,11 @@ snapshots:
find-root@1.1.0: {} find-root@1.1.0: {}
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
find-up@5.0.0: find-up@5.0.0:
dependencies: dependencies:
locate-path: 6.0.0 locate-path: 6.0.0
...@@ -7029,6 +7162,8 @@ snapshots: ...@@ -7029,6 +7162,8 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.4.0: {} get-east-asian-width@1.4.0: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
...@@ -7521,6 +7656,10 @@ snapshots: ...@@ -7521,6 +7656,10 @@ snapshots:
lit-element: 4.2.2 lit-element: 4.2.2
lit-html: 3.3.2 lit-html: 3.3.2
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
...@@ -8194,14 +8333,24 @@ snapshots: ...@@ -8194,14 +8333,24 @@ snapshots:
type-check: 0.4.0 type-check: 0.4.0
word-wrap: 1.2.5 word-wrap: 1.2.5
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-locate@5.0.0: p-locate@5.0.0:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
package-manager-detector@1.6.0: {} package-manager-detector@1.6.0: {}
...@@ -8284,6 +8433,8 @@ snapshots: ...@@ -8284,6 +8433,8 @@ snapshots:
mlly: 1.8.0 mlly: 1.8.0
pathe: 2.0.3 pathe: 2.0.3
pngjs@5.0.0: {}
points-on-curve@0.2.0: {} points-on-curve@0.2.0: {}
points-on-path@0.2.1: points-on-path@0.2.1:
...@@ -8352,6 +8503,12 @@ snapshots: ...@@ -8352,6 +8503,12 @@ snapshots:
punycode@2.3.1: {} punycode@2.3.1: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
query-string@9.3.1: query-string@9.3.1:
dependencies: dependencies:
decode-uri-component: 0.4.1 decode-uri-component: 0.4.1
...@@ -8703,6 +8860,10 @@ snapshots: ...@@ -8703,6 +8860,10 @@ snapshots:
mdast-util-to-markdown: 2.1.2 mdast-util-to-markdown: 2.1.2
unified: 11.0.5 unified: 11.0.5
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
requires-port@1.0.0: {} requires-port@1.0.0: {}
reselect@5.1.1: {} reselect@5.1.1: {}
...@@ -8788,6 +8949,8 @@ snapshots: ...@@ -8788,6 +8949,8 @@ snapshots:
semver@7.7.3: {} semver@7.7.3: {}
set-blocking@2.0.0: {}
set-value@2.0.1: set-value@2.0.1:
dependencies: dependencies:
extend-shallow: 2.0.1 extend-shallow: 2.0.1
...@@ -9298,6 +9461,8 @@ snapshots: ...@@ -9298,6 +9461,8 @@ snapshots:
tr46: 5.1.1 tr46: 5.1.1
webidl-conversions: 7.0.0 webidl-conversions: 7.0.0
which-module@2.0.1: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
...@@ -9313,6 +9478,12 @@ snapshots: ...@@ -9313,6 +9478,12 @@ snapshots:
word@0.3.0: {} word@0.3.0: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
...@@ -9345,8 +9516,29 @@ snapshots: ...@@ -9345,8 +9516,29 @@ snapshots:
xmlchars@2.2.0: {} xmlchars@2.2.0: {}
y18n@4.0.3: {}
yaml@1.10.2: {} yaml@1.10.2: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zustand@3.7.2(react@19.2.3): zustand@3.7.2(react@19.2.3):
......
/**
* Admin Announcements API endpoints
*/
import { apiClient } from '../client'
import type {
Announcement,
AnnouncementUserReadStatus,
BasePaginationResponse,
CreateAnnouncementRequest,
UpdateAnnouncementRequest
} from '@/types'
export async function list(
page: number = 1,
pageSize: number = 20,
filters?: {
status?: string
search?: string
}
): Promise<BasePaginationResponse<Announcement>> {
const { data } = await apiClient.get<BasePaginationResponse<Announcement>>('/admin/announcements', {
params: { page, page_size: pageSize, ...filters }
})
return data
}
export async function getById(id: number): Promise<Announcement> {
const { data } = await apiClient.get<Announcement>(`/admin/announcements/${id}`)
return data
}
export async function create(request: CreateAnnouncementRequest): Promise<Announcement> {
const { data } = await apiClient.post<Announcement>('/admin/announcements', request)
return data
}
export async function update(id: number, request: UpdateAnnouncementRequest): Promise<Announcement> {
const { data } = await apiClient.put<Announcement>(`/admin/announcements/${id}`, request)
return data
}
export async function deleteAnnouncement(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/admin/announcements/${id}`)
return data
}
export async function getReadStatus(
id: number,
page: number = 1,
pageSize: number = 20,
search: string = ''
): Promise<BasePaginationResponse<AnnouncementUserReadStatus>> {
const { data } = await apiClient.get<BasePaginationResponse<AnnouncementUserReadStatus>>(
`/admin/announcements/${id}/read-status`,
{ params: { page, page_size: pageSize, search } }
)
return data
}
const announcementsAPI = {
list,
getById,
create,
update,
delete: deleteAnnouncement,
getReadStatus
}
export default announcementsAPI
...@@ -50,6 +50,7 @@ export interface TrendParams { ...@@ -50,6 +50,7 @@ export interface TrendParams {
account_id?: number account_id?: number
group_id?: number group_id?: number
stream?: boolean stream?: boolean
billing_type?: number | null
} }
export interface TrendResponse { export interface TrendResponse {
...@@ -78,6 +79,7 @@ export interface ModelStatsParams { ...@@ -78,6 +79,7 @@ export interface ModelStatsParams {
account_id?: number account_id?: number
group_id?: number group_id?: number
stream?: boolean stream?: boolean
billing_type?: number | null
} }
export interface ModelStatsResponse { export interface ModelStatsResponse {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import { apiClient } from '../client' import { apiClient } from '../client'
import type { import type {
Group, AdminGroup,
GroupPlatform, GroupPlatform,
CreateGroupRequest, CreateGroupRequest,
UpdateGroupRequest, UpdateGroupRequest,
...@@ -31,8 +31,8 @@ export async function list( ...@@ -31,8 +31,8 @@ export async function list(
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
} }
): Promise<PaginatedResponse<Group>> { ): Promise<PaginatedResponse<AdminGroup>> {
const { data } = await apiClient.get<PaginatedResponse<Group>>('/admin/groups', { const { data } = await apiClient.get<PaginatedResponse<AdminGroup>>('/admin/groups', {
params: { params: {
page, page,
page_size: pageSize, page_size: pageSize,
...@@ -48,8 +48,8 @@ export async function list( ...@@ -48,8 +48,8 @@ export async function list(
* @param platform - Optional platform filter * @param platform - Optional platform filter
* @returns List of all active groups * @returns List of all active groups
*/ */
export async function getAll(platform?: GroupPlatform): Promise<Group[]> { export async function getAll(platform?: GroupPlatform): Promise<AdminGroup[]> {
const { data } = await apiClient.get<Group[]>('/admin/groups/all', { const { data } = await apiClient.get<AdminGroup[]>('/admin/groups/all', {
params: platform ? { platform } : undefined params: platform ? { platform } : undefined
}) })
return data return data
...@@ -60,7 +60,7 @@ export async function getAll(platform?: GroupPlatform): Promise<Group[]> { ...@@ -60,7 +60,7 @@ export async function getAll(platform?: GroupPlatform): Promise<Group[]> {
* @param platform - Platform to filter by * @param platform - Platform to filter by
* @returns List of groups for the specified platform * @returns List of groups for the specified platform
*/ */
export async function getByPlatform(platform: GroupPlatform): Promise<Group[]> { export async function getByPlatform(platform: GroupPlatform): Promise<AdminGroup[]> {
return getAll(platform) return getAll(platform)
} }
...@@ -69,8 +69,8 @@ export async function getByPlatform(platform: GroupPlatform): Promise<Group[]> { ...@@ -69,8 +69,8 @@ export async function getByPlatform(platform: GroupPlatform): Promise<Group[]> {
* @param id - Group ID * @param id - Group ID
* @returns Group details * @returns Group details
*/ */
export async function getById(id: number): Promise<Group> { export async function getById(id: number): Promise<AdminGroup> {
const { data } = await apiClient.get<Group>(`/admin/groups/${id}`) const { data } = await apiClient.get<AdminGroup>(`/admin/groups/${id}`)
return data return data
} }
...@@ -79,8 +79,8 @@ export async function getById(id: number): Promise<Group> { ...@@ -79,8 +79,8 @@ export async function getById(id: number): Promise<Group> {
* @param groupData - Group data * @param groupData - Group data
* @returns Created group * @returns Created group
*/ */
export async function create(groupData: CreateGroupRequest): Promise<Group> { export async function create(groupData: CreateGroupRequest): Promise<AdminGroup> {
const { data } = await apiClient.post<Group>('/admin/groups', groupData) const { data } = await apiClient.post<AdminGroup>('/admin/groups', groupData)
return data return data
} }
...@@ -90,8 +90,8 @@ export async function create(groupData: CreateGroupRequest): Promise<Group> { ...@@ -90,8 +90,8 @@ export async function create(groupData: CreateGroupRequest): Promise<Group> {
* @param updates - Fields to update * @param updates - Fields to update
* @returns Updated group * @returns Updated group
*/ */
export async function update(id: number, updates: UpdateGroupRequest): Promise<Group> { export async function update(id: number, updates: UpdateGroupRequest): Promise<AdminGroup> {
const { data } = await apiClient.put<Group>(`/admin/groups/${id}`, updates) const { data } = await apiClient.put<AdminGroup>(`/admin/groups/${id}`, updates)
return data return data
} }
...@@ -111,7 +111,7 @@ export async function deleteGroup(id: number): Promise<{ message: string }> { ...@@ -111,7 +111,7 @@ export async function deleteGroup(id: number): Promise<{ message: string }> {
* @param status - New status * @param status - New status
* @returns Updated group * @returns Updated group
*/ */
export async function toggleStatus(id: number, status: 'active' | 'inactive'): Promise<Group> { export async function toggleStatus(id: number, status: 'active' | 'inactive'): Promise<AdminGroup> {
return update(id, { status }) return update(id, { status })
} }
......
...@@ -10,6 +10,7 @@ import accountsAPI from './accounts' ...@@ -10,6 +10,7 @@ import accountsAPI from './accounts'
import proxiesAPI from './proxies' import proxiesAPI from './proxies'
import redeemAPI from './redeem' import redeemAPI from './redeem'
import promoAPI from './promo' import promoAPI from './promo'
import announcementsAPI from './announcements'
import settingsAPI from './settings' import settingsAPI from './settings'
import systemAPI from './system' import systemAPI from './system'
import subscriptionsAPI from './subscriptions' import subscriptionsAPI from './subscriptions'
...@@ -30,6 +31,7 @@ export const adminAPI = { ...@@ -30,6 +31,7 @@ export const adminAPI = {
proxies: proxiesAPI, proxies: proxiesAPI,
redeem: redeemAPI, redeem: redeemAPI,
promo: promoAPI, promo: promoAPI,
announcements: announcementsAPI,
settings: settingsAPI, settings: settingsAPI,
system: systemAPI, system: systemAPI,
subscriptions: subscriptionsAPI, subscriptions: subscriptionsAPI,
...@@ -48,6 +50,7 @@ export { ...@@ -48,6 +50,7 @@ export {
proxiesAPI, proxiesAPI,
redeemAPI, redeemAPI,
promoAPI, promoAPI,
announcementsAPI,
settingsAPI, settingsAPI,
systemAPI, systemAPI,
subscriptionsAPI, subscriptionsAPI,
......
...@@ -781,6 +781,7 @@ export interface OpsAdvancedSettings { ...@@ -781,6 +781,7 @@ export interface OpsAdvancedSettings {
ignore_count_tokens_errors: boolean ignore_count_tokens_errors: boolean
ignore_context_canceled: boolean ignore_context_canceled: boolean
ignore_no_available_accounts: boolean ignore_no_available_accounts: boolean
ignore_invalid_api_key_errors: boolean
auto_refresh_enabled: boolean auto_refresh_enabled: boolean
auto_refresh_interval_seconds: number auto_refresh_interval_seconds: number
} }
......
...@@ -12,6 +12,10 @@ export interface SystemSettings { ...@@ -12,6 +12,10 @@ export interface SystemSettings {
// Registration settings // Registration settings
registration_enabled: boolean registration_enabled: boolean
email_verify_enabled: boolean email_verify_enabled: boolean
promo_code_enabled: boolean
password_reset_enabled: boolean
totp_enabled: boolean // TOTP 双因素认证
totp_encryption_key_configured: boolean // TOTP 加密密钥是否已配置
// Default settings // Default settings
default_balance: number default_balance: number
default_concurrency: number default_concurrency: number
...@@ -23,6 +27,9 @@ export interface SystemSettings { ...@@ -23,6 +27,9 @@ export interface SystemSettings {
contact_info: string contact_info: string
doc_url: string doc_url: string
home_content: string home_content: string
hide_ccs_import_button: boolean
purchase_subscription_enabled: boolean
purchase_subscription_url: string
// SMTP settings // SMTP settings
smtp_host: string smtp_host: string
smtp_port: number smtp_port: number
...@@ -63,6 +70,9 @@ export interface SystemSettings { ...@@ -63,6 +70,9 @@ export interface SystemSettings {
export interface UpdateSettingsRequest { export interface UpdateSettingsRequest {
registration_enabled?: boolean registration_enabled?: boolean
email_verify_enabled?: boolean email_verify_enabled?: boolean
promo_code_enabled?: boolean
password_reset_enabled?: boolean
totp_enabled?: boolean // TOTP 双因素认证
default_balance?: number default_balance?: number
default_concurrency?: number default_concurrency?: number
site_name?: string site_name?: string
...@@ -72,6 +82,9 @@ export interface UpdateSettingsRequest { ...@@ -72,6 +82,9 @@ export interface UpdateSettingsRequest {
contact_info?: string contact_info?: string
doc_url?: string doc_url?: string
home_content?: string home_content?: string
hide_ccs_import_button?: boolean
purchase_subscription_enabled?: boolean
purchase_subscription_url?: string
smtp_host?: string smtp_host?: string
smtp_port?: number smtp_port?: number
smtp_username?: string smtp_username?: string
......
...@@ -17,7 +17,7 @@ import type { ...@@ -17,7 +17,7 @@ import type {
* List all subscriptions with pagination * List all subscriptions with pagination
* @param page - Page number (default: 1) * @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 20) * @param pageSize - Items per page (default: 20)
* @param filters - Optional filters (status, user_id, group_id) * @param filters - Optional filters (status, user_id, group_id, sort_by, sort_order)
* @returns Paginated list of subscriptions * @returns Paginated list of subscriptions
*/ */
export async function list( export async function list(
...@@ -27,6 +27,8 @@ export async function list( ...@@ -27,6 +27,8 @@ export async function list(
status?: 'active' | 'expired' | 'revoked' status?: 'active' | 'expired' | 'revoked'
user_id?: number user_id?: number
group_id?: number group_id?: number
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
*/ */
import { apiClient } from '../client' import { apiClient } from '../client'
import type { UsageLog, UsageQueryParams, PaginatedResponse } from '@/types' import type { AdminUsageLog, UsageQueryParams, PaginatedResponse } from '@/types'
// ==================== Types ==================== // ==================== Types ====================
...@@ -31,6 +31,46 @@ export interface SimpleApiKey { ...@@ -31,6 +31,46 @@ export interface SimpleApiKey {
user_id: number user_id: number
} }
export interface UsageCleanupFilters {
start_time: string
end_time: string
user_id?: number
api_key_id?: number
account_id?: number
group_id?: number
model?: string | null
stream?: boolean | null
billing_type?: number | null
}
export interface UsageCleanupTask {
id: number
status: string
filters: UsageCleanupFilters
created_by: number
deleted_rows: number
error_message?: string | null
canceled_by?: number | null
canceled_at?: string | null
started_at?: string | null
finished_at?: string | null
created_at: string
updated_at: string
}
export interface CreateUsageCleanupTaskRequest {
start_date: string
end_date: string
user_id?: number
api_key_id?: number
account_id?: number
group_id?: number
model?: string | null
stream?: boolean | null
billing_type?: number | null
timezone?: string
}
export interface AdminUsageQueryParams extends UsageQueryParams { export interface AdminUsageQueryParams extends UsageQueryParams {
user_id?: number user_id?: number
} }
...@@ -45,8 +85,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams { ...@@ -45,8 +85,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
export async function list( export async function list(
params: AdminUsageQueryParams, params: AdminUsageQueryParams,
options?: { signal?: AbortSignal } options?: { signal?: AbortSignal }
): Promise<PaginatedResponse<UsageLog>> { ): Promise<PaginatedResponse<AdminUsageLog>> {
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/admin/usage', { const { data } = await apiClient.get<PaginatedResponse<AdminUsageLog>>('/admin/usage', {
params, params,
signal: options?.signal signal: options?.signal
}) })
...@@ -108,11 +148,51 @@ export async function searchApiKeys(userId?: number, keyword?: string): Promise< ...@@ -108,11 +148,51 @@ export async function searchApiKeys(userId?: number, keyword?: string): Promise<
return data return data
} }
/**
* List usage cleanup tasks (admin only)
* @param params - Query parameters for pagination
* @returns Paginated list of cleanup tasks
*/
export async function listCleanupTasks(
params: { page?: number; page_size?: number },
options?: { signal?: AbortSignal }
): Promise<PaginatedResponse<UsageCleanupTask>> {
const { data } = await apiClient.get<PaginatedResponse<UsageCleanupTask>>('/admin/usage/cleanup-tasks', {
params,
signal: options?.signal
})
return data
}
/**
* Create a usage cleanup task (admin only)
* @param payload - Cleanup task parameters
* @returns Created cleanup task
*/
export async function createCleanupTask(payload: CreateUsageCleanupTaskRequest): Promise<UsageCleanupTask> {
const { data } = await apiClient.post<UsageCleanupTask>('/admin/usage/cleanup-tasks', payload)
return data
}
/**
* Cancel a usage cleanup task (admin only)
* @param taskId - Task ID to cancel
*/
export async function cancelCleanupTask(taskId: number): Promise<{ id: number; status: string }> {
const { data } = await apiClient.post<{ id: number; status: string }>(
`/admin/usage/cleanup-tasks/${taskId}/cancel`
)
return data
}
export const adminUsageAPI = { export const adminUsageAPI = {
list, list,
getStats, getStats,
searchUsers, searchUsers,
searchApiKeys searchApiKeys,
listCleanupTasks,
createCleanupTask,
cancelCleanupTask
} }
export default adminUsageAPI export default adminUsageAPI
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
*/ */
import { apiClient } from '../client' import { apiClient } from '../client'
import type { User, UpdateUserRequest, PaginatedResponse } from '@/types' import type { AdminUser, UpdateUserRequest, PaginatedResponse } from '@/types'
/** /**
* List all users with pagination * List all users with pagination
...@@ -26,7 +26,7 @@ export async function list( ...@@ -26,7 +26,7 @@ export async function list(
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
} }
): Promise<PaginatedResponse<User>> { ): Promise<PaginatedResponse<AdminUser>> {
// Build params with attribute filters in attr[id]=value format // Build params with attribute filters in attr[id]=value format
const params: Record<string, any> = { const params: Record<string, any> = {
page, page,
...@@ -44,8 +44,7 @@ export async function list( ...@@ -44,8 +44,7 @@ export async function list(
} }
} }
} }
const { data } = await apiClient.get<PaginatedResponse<AdminUser>>('/admin/users', {
const { data } = await apiClient.get<PaginatedResponse<User>>('/admin/users', {
params, params,
signal: options?.signal signal: options?.signal
}) })
...@@ -57,8 +56,8 @@ export async function list( ...@@ -57,8 +56,8 @@ export async function list(
* @param id - User ID * @param id - User ID
* @returns User details * @returns User details
*/ */
export async function getById(id: number): Promise<User> { export async function getById(id: number): Promise<AdminUser> {
const { data } = await apiClient.get<User>(`/admin/users/${id}`) const { data } = await apiClient.get<AdminUser>(`/admin/users/${id}`)
return data return data
} }
...@@ -73,8 +72,8 @@ export async function create(userData: { ...@@ -73,8 +72,8 @@ export async function create(userData: {
balance?: number balance?: number
concurrency?: number concurrency?: number
allowed_groups?: number[] | null allowed_groups?: number[] | null
}): Promise<User> { }): Promise<AdminUser> {
const { data } = await apiClient.post<User>('/admin/users', userData) const { data } = await apiClient.post<AdminUser>('/admin/users', userData)
return data return data
} }
...@@ -84,8 +83,8 @@ export async function create(userData: { ...@@ -84,8 +83,8 @@ export async function create(userData: {
* @param updates - Fields to update * @param updates - Fields to update
* @returns Updated user * @returns Updated user
*/ */
export async function update(id: number, updates: UpdateUserRequest): Promise<User> { export async function update(id: number, updates: UpdateUserRequest): Promise<AdminUser> {
const { data } = await apiClient.put<User>(`/admin/users/${id}`, updates) const { data } = await apiClient.put<AdminUser>(`/admin/users/${id}`, updates)
return data return data
} }
...@@ -112,8 +111,8 @@ export async function updateBalance( ...@@ -112,8 +111,8 @@ export async function updateBalance(
balance: number, balance: number,
operation: 'set' | 'add' | 'subtract' = 'set', operation: 'set' | 'add' | 'subtract' = 'set',
notes?: string notes?: string
): Promise<User> { ): Promise<AdminUser> {
const { data } = await apiClient.post<User>(`/admin/users/${id}/balance`, { const { data } = await apiClient.post<AdminUser>(`/admin/users/${id}/balance`, {
balance, balance,
operation, operation,
notes: notes || '' notes: notes || ''
...@@ -127,7 +126,7 @@ export async function updateBalance( ...@@ -127,7 +126,7 @@ export async function updateBalance(
* @param concurrency - New concurrency limit * @param concurrency - New concurrency limit
* @returns Updated user * @returns Updated user
*/ */
export async function updateConcurrency(id: number, concurrency: number): Promise<User> { export async function updateConcurrency(id: number, concurrency: number): Promise<AdminUser> {
return update(id, { concurrency }) return update(id, { concurrency })
} }
...@@ -137,7 +136,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis ...@@ -137,7 +136,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis
* @param status - New status * @param status - New status
* @returns Updated user * @returns Updated user
*/ */
export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<User> { export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<AdminUser> {
return update(id, { status }) return update(id, { status })
} }
......
/**
* User Announcements API endpoints
*/
import { apiClient } from './client'
import type { UserAnnouncement } from '@/types'
export async function list(unreadOnly: boolean = false): Promise<UserAnnouncement[]> {
const { data } = await apiClient.get<UserAnnouncement[]>('/announcements', {
params: unreadOnly ? { unread_only: 1 } : {}
})
return data
}
export async function markRead(id: number): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>(`/announcements/${id}/read`)
return data
}
const announcementsAPI = {
list,
markRead
}
export default announcementsAPI
...@@ -11,9 +11,23 @@ import type { ...@@ -11,9 +11,23 @@ import type {
CurrentUserResponse, CurrentUserResponse,
SendVerifyCodeRequest, SendVerifyCodeRequest,
SendVerifyCodeResponse, SendVerifyCodeResponse,
PublicSettings PublicSettings,
TotpLoginResponse,
TotpLogin2FARequest
} from '@/types' } from '@/types'
/**
* Login response type - can be either full auth or 2FA required
*/
export type LoginResponse = AuthResponse | TotpLoginResponse
/**
* Type guard to check if login response requires 2FA
*/
export function isTotp2FARequired(response: LoginResponse): response is TotpLoginResponse {
return 'requires_2fa' in response && response.requires_2fa === true
}
/** /**
* Store authentication token in localStorage * Store authentication token in localStorage
*/ */
...@@ -38,11 +52,28 @@ export function clearAuthToken(): void { ...@@ -38,11 +52,28 @@ export function clearAuthToken(): void {
/** /**
* User login * User login
* @param credentials - Username and password * @param credentials - Email and password
* @returns Authentication response with token and user data, or 2FA required response
*/
export async function login(credentials: LoginRequest): Promise<LoginResponse> {
const { data } = await apiClient.post<LoginResponse>('/auth/login', credentials)
// Only store token if 2FA is not required
if (!isTotp2FARequired(data)) {
setAuthToken(data.access_token)
localStorage.setItem('auth_user', JSON.stringify(data.user))
}
return data
}
/**
* Complete login with 2FA code
* @param request - Temp token and TOTP code
* @returns Authentication response with token and user data * @returns Authentication response with token and user data
*/ */
export async function login(credentials: LoginRequest): Promise<AuthResponse> { export async function login2FA(request: TotpLogin2FARequest): Promise<AuthResponse> {
const { data } = await apiClient.post<AuthResponse>('/auth/login', credentials) const { data } = await apiClient.post<AuthResponse>('/auth/login/2fa', request)
// Store token and user data // Store token and user data
setAuthToken(data.access_token) setAuthToken(data.access_token)
...@@ -133,8 +164,61 @@ export async function validatePromoCode(code: string): Promise<ValidatePromoCode ...@@ -133,8 +164,61 @@ export async function validatePromoCode(code: string): Promise<ValidatePromoCode
return data return data
} }
/**
* Forgot password request
*/
export interface ForgotPasswordRequest {
email: string
turnstile_token?: string
}
/**
* Forgot password response
*/
export interface ForgotPasswordResponse {
message: string
}
/**
* Request password reset link
* @param request - Email and optional Turnstile token
* @returns Response with message
*/
export async function forgotPassword(request: ForgotPasswordRequest): Promise<ForgotPasswordResponse> {
const { data } = await apiClient.post<ForgotPasswordResponse>('/auth/forgot-password', request)
return data
}
/**
* Reset password request
*/
export interface ResetPasswordRequest {
email: string
token: string
new_password: string
}
/**
* Reset password response
*/
export interface ResetPasswordResponse {
message: string
}
/**
* Reset password with token
* @param request - Email, token, and new password
* @returns Response with message
*/
export async function resetPassword(request: ResetPasswordRequest): Promise<ResetPasswordResponse> {
const { data } = await apiClient.post<ResetPasswordResponse>('/auth/reset-password', request)
return data
}
export const authAPI = { export const authAPI = {
login, login,
login2FA,
isTotp2FARequired,
register, register,
getCurrentUser, getCurrentUser,
logout, logout,
...@@ -144,7 +228,9 @@ export const authAPI = { ...@@ -144,7 +228,9 @@ export const authAPI = {
clearAuthToken, clearAuthToken,
getPublicSettings, getPublicSettings,
sendVerifyCode, sendVerifyCode,
validatePromoCode validatePromoCode,
forgotPassword,
resetPassword
} }
export default authAPI export default authAPI
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
export { apiClient } from './client' export { apiClient } from './client'
// Auth API // Auth API
export { authAPI } from './auth' export { authAPI, isTotp2FARequired, type LoginResponse } from './auth'
// User APIs // User APIs
export { keysAPI } from './keys' export { keysAPI } from './keys'
...@@ -15,6 +15,8 @@ export { usageAPI } from './usage' ...@@ -15,6 +15,8 @@ export { usageAPI } from './usage'
export { userAPI } from './user' export { userAPI } from './user'
export { redeemAPI, type RedeemHistoryItem } from './redeem' export { redeemAPI, type RedeemHistoryItem } from './redeem'
export { userGroupsAPI } from './groups' export { userGroupsAPI } from './groups'
export { totpAPI } from './totp'
export { default as announcementsAPI } from './announcements'
// Admin APIs // Admin APIs
export { adminAPI } from './admin' export { adminAPI } from './admin'
......
...@@ -14,7 +14,9 @@ export interface RedeemHistoryItem { ...@@ -14,7 +14,9 @@ export interface RedeemHistoryItem {
status: string status: string
used_at: string used_at: string
created_at: string created_at: string
// 订阅类型专用字段 // Notes from admin for admin_balance/admin_concurrency types
notes?: string
// Subscription-specific fields
group_id?: number group_id?: number
validity_days?: number validity_days?: number
group?: { group?: {
......
...@@ -31,6 +31,7 @@ export interface RedisConfig { ...@@ -31,6 +31,7 @@ export interface RedisConfig {
port: number port: number
password: string password: string
db: number db: number
enable_tls: boolean
} }
export interface AdminConfig { export interface AdminConfig {
......
/**
* TOTP (2FA) API endpoints
* Handles Two-Factor Authentication with Google Authenticator
*/
import { apiClient } from './client'
import type {
TotpStatus,
TotpSetupRequest,
TotpSetupResponse,
TotpEnableRequest,
TotpEnableResponse,
TotpDisableRequest,
TotpVerificationMethod
} from '@/types'
/**
* Get TOTP status for current user
* @returns TOTP status including enabled state and feature availability
*/
export async function getStatus(): Promise<TotpStatus> {
const { data } = await apiClient.get<TotpStatus>('/user/totp/status')
return data
}
/**
* Get verification method for TOTP operations
* @returns Method ('email' or 'password') required for setup/disable
*/
export async function getVerificationMethod(): Promise<TotpVerificationMethod> {
const { data } = await apiClient.get<TotpVerificationMethod>('/user/totp/verification-method')
return data
}
/**
* Send email verification code for TOTP operations
* @returns Success response
*/
export async function sendVerifyCode(): Promise<{ success: boolean }> {
const { data } = await apiClient.post<{ success: boolean }>('/user/totp/send-code')
return data
}
/**
* Initiate TOTP setup - generates secret and QR code
* @param request - Email code or password depending on verification method
* @returns Setup response with secret, QR code URL, and setup token
*/
export async function initiateSetup(request?: TotpSetupRequest): Promise<TotpSetupResponse> {
const { data } = await apiClient.post<TotpSetupResponse>('/user/totp/setup', request || {})
return data
}
/**
* Complete TOTP setup by verifying the code
* @param request - TOTP code and setup token
* @returns Enable response with success status and enabled timestamp
*/
export async function enable(request: TotpEnableRequest): Promise<TotpEnableResponse> {
const { data } = await apiClient.post<TotpEnableResponse>('/user/totp/enable', request)
return data
}
/**
* Disable TOTP for current user
* @param request - Email code or password depending on verification method
* @returns Success response
*/
export async function disable(request: TotpDisableRequest): Promise<{ success: boolean }> {
const { data } = await apiClient.post<{ success: boolean }>('/user/totp/disable', request)
return data
}
export const totpAPI = {
getStatus,
getVerificationMethod,
sendVerifyCode,
initiateSetup,
enable,
disable
}
export default totpAPI
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment