update
Browse files- .clinerules +146 -0
- .env +12 -0
- .gitignore +3 -0
- Dockerfile +16 -0
- app.py +67 -0
- core/config.py +30 -0
- core/dependencies.py +141 -0
- core/models.py +64 -0
- core/utils.py +123 -0
- memory-bank/activeContext.md +62 -0
- memory-bank/productContext.md +15 -0
- memory-bank/progress.md +45 -0
- memory-bank/projectBrief.md +17 -0
- memory-bank/systemPatterns.md +39 -0
- memory-bank/techContext.md +35 -0
- push.sh +3 -0
- requirements.txt +10 -0
- routes/admin.py +125 -0
- routes/api_keys.py +56 -0
- routes/auth.py +369 -0
- routes/proxies.py +21 -0
- static/admin.html +224 -0
- static/app.js +28 -0
- static/index.html +125 -0
- static/js/admin.js +218 -0
- static/js/auth.js +409 -0
- static/js/proxy.js +35 -0
- static/js/store.js +23 -0
- static/login.html +129 -0
- static/reset-password.html +130 -0
- static/signup.html +130 -0
- static/style.css +463 -0
- test_auth_proxy.py +86 -0
- test_email.py +95 -0
- test_supabase.py +87 -0
.clinerules
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 自定义指令
|
| 2 |
+
CUSTOM_INSTRUCTIONS = """
|
| 3 |
+
# 自动激活conda环境
|
| 4 |
+
# 在每次会话开始时,自动检查当前conda环境。如果不是 'airs',则激活它。
|
| 5 |
+
# 这确保了所有后续的Python相关命令都在正确的环境中执行。
|
| 6 |
+
if [ -z "$CONDA_DEFAULT_ENV" ] || [ "$CONDA_DEFAULT_ENV" != "airs" ]; then
|
| 7 |
+
conda activate airs
|
| 8 |
+
fi
|
| 9 |
+
# ~/airs/superproxy/solutions目录用于存储项目下所有解决方案,其中 ~ 表示当前系统用户的主目录。
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
description: Describes Cline's Memory Bank system, its structure, and workflows for maintaining project knowledge across sessions.
|
| 15 |
+
author: https://github.com/nickbaumann98
|
| 16 |
+
version: 1.0
|
| 17 |
+
tags: ["memory-bank", "knowledge-base", "core-behavior", "documentation-protocol"]
|
| 18 |
+
globs: ["memory-bank/**/*.md", "*"]
|
| 19 |
+
---
|
| 20 |
+
# Cline's Memory Bank
|
| 21 |
+
|
| 22 |
+
My memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
| 23 |
+
|
| 24 |
+
## Memory Bank Structure
|
| 25 |
+
|
| 26 |
+
The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
| 27 |
+
|
| 28 |
+
```mermaid
|
| 29 |
+
flowchart TD
|
| 30 |
+
PB[projectBrief.md] --> PC[productContext.md]
|
| 31 |
+
PB --> SP[systemPatterns.md]
|
| 32 |
+
PB --> TC[techContext.md]
|
| 33 |
+
|
| 34 |
+
PC --> AC[activeContext.md]
|
| 35 |
+
SP --> AC
|
| 36 |
+
TC --> AC
|
| 37 |
+
|
| 38 |
+
AC --> P[progress.md]
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### Core Files (Required)
|
| 42 |
+
1. `projectBrief.md`
|
| 43 |
+
- Foundation document that shapes all other files
|
| 44 |
+
- Created at project start if it doesn't exist
|
| 45 |
+
- Defines core requirements and goals
|
| 46 |
+
- Source of truth for project scope
|
| 47 |
+
|
| 48 |
+
2. `productContext.md`
|
| 49 |
+
- Why this project exists
|
| 50 |
+
- Problems it solves
|
| 51 |
+
- How it should work
|
| 52 |
+
- User experience goals
|
| 53 |
+
|
| 54 |
+
3. `activeContext.md`
|
| 55 |
+
- Current work focus
|
| 56 |
+
- Recent changes
|
| 57 |
+
- Next steps
|
| 58 |
+
- Active decisions and considerations
|
| 59 |
+
- Important patterns and preferences
|
| 60 |
+
- Learnings and project insights
|
| 61 |
+
|
| 62 |
+
4. `systemPatterns.md`
|
| 63 |
+
- System architecture
|
| 64 |
+
- Key technical decisions
|
| 65 |
+
- Design patterns in use
|
| 66 |
+
- Component relationships
|
| 67 |
+
- Critical implementation paths
|
| 68 |
+
|
| 69 |
+
5. `techContext.md`
|
| 70 |
+
- Technologies used
|
| 71 |
+
- Development setup
|
| 72 |
+
- Technical constraints
|
| 73 |
+
- Dependencies
|
| 74 |
+
- Tool usage patterns
|
| 75 |
+
|
| 76 |
+
6. `progress.md`
|
| 77 |
+
- What works
|
| 78 |
+
- What's left to build
|
| 79 |
+
- Current status
|
| 80 |
+
- Known issues
|
| 81 |
+
- Evolution of project decisions
|
| 82 |
+
|
| 83 |
+
### Additional Context
|
| 84 |
+
Create additional files/folders within memory-bank/ when they help organize:
|
| 85 |
+
- Complex feature documentation
|
| 86 |
+
- Integration specifications
|
| 87 |
+
- API documentation
|
| 88 |
+
- Testing strategies
|
| 89 |
+
- Deployment procedures
|
| 90 |
+
|
| 91 |
+
## Core Workflows
|
| 92 |
+
|
| 93 |
+
### Plan Mode
|
| 94 |
+
```mermaid
|
| 95 |
+
flowchart TD
|
| 96 |
+
Start[Start] --> ReadFiles[Read Memory Bank]
|
| 97 |
+
ReadFiles --> CheckFiles{Files Complete?}
|
| 98 |
+
|
| 99 |
+
CheckFiles -->|No| Plan[Create Plan]
|
| 100 |
+
Plan --> Document[Document in Chat]
|
| 101 |
+
|
| 102 |
+
CheckFiles -->|Yes| Verify[Verify Context]
|
| 103 |
+
Verify --> Strategy[Develop Strategy]
|
| 104 |
+
Strategy --> Present[Present Approach]
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### Act Mode
|
| 108 |
+
```mermaid
|
| 109 |
+
flowchart TD
|
| 110 |
+
Start[Start] --> Context[Check Memory Bank]
|
| 111 |
+
Context --> Update[Update Documentation]
|
| 112 |
+
Update --> Execute[Execute Task]
|
| 113 |
+
Execute --> Document[Document Changes]
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## Documentation Updates
|
| 117 |
+
|
| 118 |
+
Memory Bank updates occur when:
|
| 119 |
+
1. Discovering new project patterns
|
| 120 |
+
2. After implementing significant changes
|
| 121 |
+
3. When user requests with **update memory bank** (MUST review ALL files)
|
| 122 |
+
4. When context needs clarification
|
| 123 |
+
|
| 124 |
+
```mermaid
|
| 125 |
+
flowchart TD
|
| 126 |
+
Start[Update Process]
|
| 127 |
+
|
| 128 |
+
subgraph Process
|
| 129 |
+
P1[Review ALL Files]
|
| 130 |
+
P2[Document Current State]
|
| 131 |
+
P3[Clarify Next Steps]
|
| 132 |
+
P4[Document Insights & Patterns]
|
| 133 |
+
|
| 134 |
+
P1 --> P2 --> P3 --> P4
|
| 135 |
+
end
|
| 136 |
+
|
| 137 |
+
Start --> Process
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
| 141 |
+
|
| 142 |
+
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# 自动批准规则
|
| 146 |
+
AUTO_APPROVE = true
|
.env
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SUPABASE_URL="https://fmipexqcxsopbffjdfur.supabase.co"
|
| 2 |
+
SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZtaXBleHFjeHNvcGJmZmpkZnVyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDcyMzczNjQsImV4cCI6MjA2MjgxMzM2NH0.2RthX7FX6BnU90N3GOlvIR94dFViwqFo27kKeTO7NBc"
|
| 3 |
+
SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZtaXBleHFjeHNvcGJmZmpkZnVyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0NzIzNzM2NCwiZXhwIjoyMDYyODEzMzY0fQ.AHj-C3YH4jQytRd1-ikLOeSrDZErvqMZPYLCHfyUGS4"
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
JWT_SECRET_KEY="asdlfjnljflasdjf8796dfad8c7a9dsc"
|
| 7 |
+
SMTP_SERVER="smtp.qq.com"
|
| 8 |
+
SMTP_PORT=465 # 或 465,取决于您的SMTP服务
|
| 9 |
+
SMTP_USERNAME="airs.ltd@qq.com"
|
| 10 |
+
SMTP_PASSWORD="wxrowbdsaqyebhcd"
|
| 11 |
+
SENDER_EMAIL="airs.ltd@qq.com"
|
| 12 |
+
SENDER_NAME="SuperProxy Support" # 可选,发件人名称
|
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**/__pycache__/
|
| 2 |
+
**/__images__/
|
| 3 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 2 |
+
# you will also find guides on how best to write your Dockerfile
|
| 3 |
+
|
| 4 |
+
FROM python:3.12
|
| 5 |
+
|
| 6 |
+
RUN useradd -m -u 1000 user
|
| 7 |
+
USER user
|
| 8 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 9 |
+
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 13 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 14 |
+
|
| 15 |
+
COPY --chown=user . /app
|
| 16 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# uvicorn app:app --host 0.0.0.0 --port 7860 --reload
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import sys # Import sys
|
| 5 |
+
from fastapi import FastAPI
|
| 6 |
+
from fastapi.responses import HTMLResponse
|
| 7 |
+
from fastapi.staticfiles import StaticFiles
|
| 8 |
+
|
| 9 |
+
# Optional: Import python-multipart to ensure it's loaded
|
| 10 |
+
try:
|
| 11 |
+
import python_multipart
|
| 12 |
+
except ImportError:
|
| 13 |
+
print("Warning: python-multipart is not installed. File uploads may fail.")
|
| 14 |
+
|
| 15 |
+
from routes import auth, api_keys, proxies, admin # Import admin router
|
| 16 |
+
|
| 17 |
+
# Environment check: Ensure we are running in the correct Conda environment
|
| 18 |
+
# This helps catch issues where uvicorn might be using a different python interpreter
|
| 19 |
+
conda_prefix = os.environ.get("CONDA_PREFIX")
|
| 20 |
+
if conda_prefix:
|
| 21 |
+
expected_python_path = os.path.join(conda_prefix, "bin", "python")
|
| 22 |
+
if sys.executable != expected_python_path:
|
| 23 |
+
print(f"Warning: Python interpreter is not from the expected Conda environment 'airs'.")
|
| 24 |
+
print(f"Expected: {expected_python_path}")
|
| 25 |
+
print(f"Actual: {sys.executable}")
|
| 26 |
+
# Optionally, you could raise an exception here to prevent startup in wrong environment
|
| 27 |
+
# raise RuntimeError("Incorrect Python environment detected. Please activate 'airs' conda environment.")
|
| 28 |
+
else:
|
| 29 |
+
print("Warning: CONDA_PREFIX environment variable not set. Cannot verify Conda environment.")
|
| 30 |
+
|
| 31 |
+
# Initialize FastAPI app
|
| 32 |
+
app = FastAPI()
|
| 33 |
+
|
| 34 |
+
# Mount static files
|
| 35 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 36 |
+
|
| 37 |
+
# Include routers
|
| 38 |
+
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
| 39 |
+
app.include_router(api_keys.router, prefix="/api/user", tags=["api_keys"])
|
| 40 |
+
app.include_router(proxies.router, prefix="/api/proxies", tags=["proxies"])
|
| 41 |
+
app.include_router(admin.router, prefix="/api/admin", tags=["admin"]) # Include admin router
|
| 42 |
+
|
| 43 |
+
# Root and HTML routes
|
| 44 |
+
@app.get("/", response_class=HTMLResponse)
|
| 45 |
+
async def read_root():
|
| 46 |
+
with open("static/index.html", "r") as f:
|
| 47 |
+
return f.read()
|
| 48 |
+
|
| 49 |
+
@app.get("/login", response_class=HTMLResponse)
|
| 50 |
+
async def read_login():
|
| 51 |
+
with open("static/login.html", "r") as f:
|
| 52 |
+
return f.read()
|
| 53 |
+
|
| 54 |
+
@app.get("/reset-password", response_class=HTMLResponse)
|
| 55 |
+
async def read_reset_password():
|
| 56 |
+
with open("static/reset-password.html", "r") as f:
|
| 57 |
+
return f.read()
|
| 58 |
+
|
| 59 |
+
@app.get("/signup", response_class=HTMLResponse)
|
| 60 |
+
async def read_signup():
|
| 61 |
+
with open("static/signup.html", "r") as f:
|
| 62 |
+
return f.read()
|
| 63 |
+
|
| 64 |
+
@app.get("/admin", response_class=HTMLResponse)
|
| 65 |
+
async def read_admin():
|
| 66 |
+
with open("static/admin.html", "r") as f:
|
| 67 |
+
return f.read()
|
core/config.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import secrets
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv() # 加载 .env 文件中的环境变量
|
| 6 |
+
|
| 7 |
+
# Supabase Configuration
|
| 8 |
+
SUPABASE_URL = os.environ.get("SUPABASE_URL")
|
| 9 |
+
SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY")
|
| 10 |
+
SUPABASE_SERVICE_ROLE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
| 11 |
+
|
| 12 |
+
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
|
| 13 |
+
raise ValueError("Supabase URL and Anon Key must be set as environment variables.")
|
| 14 |
+
|
| 15 |
+
# JWT Configuration
|
| 16 |
+
SECRET_KEY = os.environ.get("JWT_SECRET_KEY", secrets.token_urlsafe(32))
|
| 17 |
+
ALGORITHM = "HS256"
|
| 18 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
| 19 |
+
VERIFICATION_CODE_EXPIRE_MINUTES = 5 # 邮箱验证码有效期,单位:分钟
|
| 20 |
+
|
| 21 |
+
# Email Configuration (for custom password reset)
|
| 22 |
+
SMTP_SERVER = os.environ.get("SMTP_SERVER")
|
| 23 |
+
SMTP_PORT = int(os.environ.get("SMTP_PORT", 465)) # Changed default to 465 for SMTP_SSL
|
| 24 |
+
SMTP_USERNAME = os.environ.get("SMTP_USERNAME")
|
| 25 |
+
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD")
|
| 26 |
+
SENDER_EMAIL = os.environ.get("SENDER_EMAIL")
|
| 27 |
+
SENDER_NAME = os.environ.get("SENDER_NAME", "SuperProxy Support")
|
| 28 |
+
|
| 29 |
+
if not all([SMTP_SERVER, SMTP_USERNAME, SMTP_PASSWORD, SENDER_EMAIL]):
|
| 30 |
+
print("Warning: SMTP server details not fully configured. Password reset emails may not work.")
|
core/dependencies.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from fastapi import HTTPException, Depends, status
|
| 3 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
|
| 4 |
+
from supabase import create_client, Client
|
| 5 |
+
import jwt
|
| 6 |
+
|
| 7 |
+
from typing import Optional
|
| 8 |
+
from fastapi import HTTPException, Depends, status
|
| 9 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
|
| 10 |
+
from supabase import create_client, Client
|
| 11 |
+
import jwt
|
| 12 |
+
|
| 13 |
+
from core.config import SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SECRET_KEY, ALGORITHM
|
| 14 |
+
from core.models import User, TokenData # Import TokenData
|
| 15 |
+
|
| 16 |
+
# Dependency to get Supabase client
|
| 17 |
+
def get_supabase_client() -> Client:
|
| 18 |
+
try:
|
| 19 |
+
return create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"Error initializing Supabase client: {e}")
|
| 22 |
+
raise HTTPException(
|
| 23 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 24 |
+
detail="Failed to connect to Supabase. Please check server configuration."
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Authentication schemes
|
| 28 |
+
oauth2_scheme = HTTPBearer(auto_error=False)
|
| 29 |
+
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
| 30 |
+
|
| 31 |
+
# Dependency to get current user from JWT token
|
| 32 |
+
async def get_current_user_from_token(
|
| 33 |
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(oauth2_scheme),
|
| 34 |
+
supabase_client: Client = Depends(get_supabase_client)
|
| 35 |
+
) -> Optional[User]:
|
| 36 |
+
if not credentials:
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
token = credentials.credentials
|
| 40 |
+
try:
|
| 41 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 42 |
+
user_id: str = payload.get("sub")
|
| 43 |
+
if user_id is None:
|
| 44 |
+
raise HTTPException(
|
| 45 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 46 |
+
detail="Invalid authentication credentials (token payload)",
|
| 47 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Fetch user from sp_users table, including is_admin
|
| 51 |
+
res = supabase_client.table('sp_users').select('id, email, is_admin, email_verified').eq('id', user_id).single().execute()
|
| 52 |
+
if res.data:
|
| 53 |
+
return User(
|
| 54 |
+
id=res.data['id'],
|
| 55 |
+
email=res.data['email'],
|
| 56 |
+
is_admin=res.data.get('is_admin', False), # Default to False if not present
|
| 57 |
+
email_verified=res.data.get('email_verified', False)
|
| 58 |
+
)
|
| 59 |
+
else:
|
| 60 |
+
raise HTTPException(
|
| 61 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 62 |
+
detail="Invalid authentication credentials (user not found)",
|
| 63 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 64 |
+
)
|
| 65 |
+
except jwt.PyJWTError:
|
| 66 |
+
raise HTTPException(
|
| 67 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 68 |
+
detail="Invalid authentication credentials (token invalid)",
|
| 69 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 70 |
+
)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"Error in get_current_user_from_token: {e}")
|
| 73 |
+
raise HTTPException(
|
| 74 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 75 |
+
detail=f"Internal server error during token authentication: {e}",
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Dependency to get current user from API Key
|
| 79 |
+
async def get_current_user_from_api_key(
|
| 80 |
+
api_key: Optional[str] = Depends(api_key_header),
|
| 81 |
+
supabase_client: Client = Depends(get_supabase_client)
|
| 82 |
+
) -> Optional[User]:
|
| 83 |
+
if not api_key:
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
res = supabase_client.table('sp_user_api_keys').select('user_id').eq('api_key', api_key).single().execute()
|
| 88 |
+
if res.data and res.data['user_id']:
|
| 89 |
+
user_id = res.data['user_id']
|
| 90 |
+
# Fetch user details from sp_users table, including is_admin
|
| 91 |
+
user_res = supabase_client.table('sp_users').select('id, email, is_admin, email_verified').eq('id', user_id).single().execute()
|
| 92 |
+
if user_res.data:
|
| 93 |
+
return User(
|
| 94 |
+
id=user_res.data['id'],
|
| 95 |
+
email=user_res.data['email'],
|
| 96 |
+
is_admin=user_res.data.get('is_admin', False), # Default to False if not present
|
| 97 |
+
email_verified=user_res.data.get('email_verified', False)
|
| 98 |
+
)
|
| 99 |
+
else:
|
| 100 |
+
raise HTTPException(
|
| 101 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 102 |
+
detail="Invalid API Key or user not found",
|
| 103 |
+
headers={"X-API-Key": "Invalid"},
|
| 104 |
+
)
|
| 105 |
+
else:
|
| 106 |
+
raise HTTPException(
|
| 107 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 108 |
+
detail="Invalid API Key",
|
| 109 |
+
headers={"X-API-Key": "Invalid"},
|
| 110 |
+
)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
print(f"Error in get_current_user_from_api_key: {e}")
|
| 113 |
+
raise HTTPException(
|
| 114 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 115 |
+
detail=f"Internal server error during API Key authentication: {e}",
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Combined dependency for authentication
|
| 119 |
+
async def get_current_active_user(
|
| 120 |
+
user_from_token: Optional[User] = Depends(get_current_user_from_token),
|
| 121 |
+
user_from_api_key: Optional[User] = Depends(get_current_user_from_api_key)
|
| 122 |
+
) -> User:
|
| 123 |
+
if user_from_token:
|
| 124 |
+
return user_from_token
|
| 125 |
+
if user_from_api_key:
|
| 126 |
+
return user_from_api_key
|
| 127 |
+
|
| 128 |
+
raise HTTPException(
|
| 129 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 130 |
+
detail="Not authenticated",
|
| 131 |
+
headers={"WWW-Authenticate": "Bearer or X-API-Key"},
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# New dependency to check for admin user
|
| 135 |
+
async def get_current_admin_user(current_user: User = Depends(get_current_active_user)) -> User:
|
| 136 |
+
if not current_user.is_admin:
|
| 137 |
+
raise HTTPException(
|
| 138 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 139 |
+
detail="Operation forbidden: Not an administrator."
|
| 140 |
+
)
|
| 141 |
+
return current_user
|
core/models.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
class UserCredentials(BaseModel):
|
| 6 |
+
email: str
|
| 7 |
+
password: str
|
| 8 |
+
verification_code: Optional[str] = Field(None, alias="verificationCode") # Make verification code optional
|
| 9 |
+
|
| 10 |
+
class Config:
|
| 11 |
+
allow_population_by_field_name = True
|
| 12 |
+
populate_by_name = True
|
| 13 |
+
|
| 14 |
+
class ForgotPasswordRequest(BaseModel):
|
| 15 |
+
email: str
|
| 16 |
+
|
| 17 |
+
class Token(BaseModel):
|
| 18 |
+
access_token: str
|
| 19 |
+
token_type: str = "bearer"
|
| 20 |
+
|
| 21 |
+
class TokenData(BaseModel): # New model for JWT payload
|
| 22 |
+
user_id: Optional[str] = None
|
| 23 |
+
|
| 24 |
+
class User(BaseModel):
|
| 25 |
+
id: str
|
| 26 |
+
email: str
|
| 27 |
+
email_verified: bool = False
|
| 28 |
+
is_admin: bool = False
|
| 29 |
+
disabled: bool = True # Add disabled field, default to True
|
| 30 |
+
|
| 31 |
+
class ApiKeyCreateResponse(BaseModel):
|
| 32 |
+
api_key: str
|
| 33 |
+
|
| 34 |
+
class ApiKeyItem(BaseModel):
|
| 35 |
+
api_key: str
|
| 36 |
+
created_at: datetime
|
| 37 |
+
|
| 38 |
+
class ChangePasswordRequest(BaseModel):
|
| 39 |
+
new_password: str
|
| 40 |
+
reset_token: Optional[str] = None # For password reset flow
|
| 41 |
+
|
| 42 |
+
class ResetPasswordWithCodeRequest(BaseModel):
|
| 43 |
+
email: str
|
| 44 |
+
verification_code: str = Field(..., alias="verificationCode")
|
| 45 |
+
new_password: str
|
| 46 |
+
|
| 47 |
+
class Config:
|
| 48 |
+
allow_population_by_field_name = True
|
| 49 |
+
populate_by_name = True
|
| 50 |
+
|
| 51 |
+
class AdminUser(BaseModel): # New model for admin view
|
| 52 |
+
id: str
|
| 53 |
+
email: str
|
| 54 |
+
email_verified: bool = False
|
| 55 |
+
created_at: datetime
|
| 56 |
+
is_admin: bool = False
|
| 57 |
+
disabled: bool = True # Add disabled field
|
| 58 |
+
|
| 59 |
+
class UserUpdate(BaseModel): # New model for updating user info
|
| 60 |
+
email: Optional[str] = None
|
| 61 |
+
password: Optional[str] = None
|
| 62 |
+
email_verified: Optional[bool] = None
|
| 63 |
+
is_admin: Optional[bool] = None
|
| 64 |
+
disabled: Optional[bool] = None # Add optional disabled field
|
core/utils.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import jwt
|
| 2 |
+
import smtplib
|
| 3 |
+
import secrets
|
| 4 |
+
import ssl # Import ssl
|
| 5 |
+
import random # Import random for verification code generation
|
| 6 |
+
import time # Import time for verification code expiration
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from email.mime.text import MIMEText
|
| 9 |
+
from email.header import Header
|
| 10 |
+
from passlib.context import CryptContext
|
| 11 |
+
from typing import Optional # Import Optional
|
| 12 |
+
|
| 13 |
+
from core.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, \
|
| 14 |
+
SMTP_SERVER, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SENDER_EMAIL, SENDER_NAME, \
|
| 15 |
+
VERIFICATION_CODE_EXPIRE_MINUTES
|
| 16 |
+
|
| 17 |
+
# In-memory storage for verification codes (for demonstration purposes)
|
| 18 |
+
# In a production environment, consider using Redis or a database for persistence and scalability
|
| 19 |
+
verification_codes = {} # {email: {"code": "123456", "expires_at": timestamp}}
|
| 20 |
+
|
| 21 |
+
# Password hashing context
|
| 22 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 23 |
+
|
| 24 |
+
# Helper functions for password hashing
|
| 25 |
+
def verify_password(plain_password, hashed_password):
|
| 26 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 27 |
+
|
| 28 |
+
def get_password_hash(password):
|
| 29 |
+
# Truncate password to 72 bytes as bcrypt has a limit
|
| 30 |
+
# This handles cases where users might input very long passwords
|
| 31 |
+
truncated_password = password.encode('utf-8')[:72].decode('utf-8', 'ignore')
|
| 32 |
+
return pwd_context.hash(truncated_password)
|
| 33 |
+
|
| 34 |
+
# Helper function for JWT
|
| 35 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
| 36 |
+
to_encode = data.copy()
|
| 37 |
+
if expires_delta:
|
| 38 |
+
expire = datetime.utcnow() + expires_delta
|
| 39 |
+
else:
|
| 40 |
+
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 41 |
+
to_encode.update({"exp": expire})
|
| 42 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 43 |
+
return encoded_jwt
|
| 44 |
+
|
| 45 |
+
def generate_verification_code():
|
| 46 |
+
"""Generates a 6-digit numeric verification code."""
|
| 47 |
+
return str(random.randint(100000, 999999))
|
| 48 |
+
|
| 49 |
+
def store_verification_code(email: str, code: str, prefix: str = ""):
|
| 50 |
+
"""Stores the verification code with an expiration time, using an optional prefix for the key."""
|
| 51 |
+
key = prefix + email
|
| 52 |
+
expires_at = time.time() + VERIFICATION_CODE_EXPIRE_MINUTES * 60
|
| 53 |
+
verification_codes[key] = {"code": code, "expires_at": expires_at}
|
| 54 |
+
print(f"Stored verification code for {key}: {code}, expires at {datetime.fromtimestamp(expires_at)}")
|
| 55 |
+
|
| 56 |
+
def verify_stored_code(email: str, code: str, prefix: str = "") -> bool:
|
| 57 |
+
"""Verifies the provided code against the stored one and checks for expiration, using an optional prefix for the key."""
|
| 58 |
+
key = prefix + email
|
| 59 |
+
stored_data = verification_codes.get(key)
|
| 60 |
+
if not stored_data:
|
| 61 |
+
return False # No code stored for this email
|
| 62 |
+
|
| 63 |
+
if time.time() > stored_data["expires_at"]:
|
| 64 |
+
del verification_codes[key] # Remove expired code
|
| 65 |
+
return False # Code expired
|
| 66 |
+
|
| 67 |
+
return stored_data["code"] == code
|
| 68 |
+
|
| 69 |
+
# Helper function for sending emails
|
| 70 |
+
def send_email(to_email: str, subject: str, body: str):
|
| 71 |
+
if not all([SMTP_SERVER, SMTP_USERNAME, SMTP_PASSWORD, SENDER_EMAIL]):
|
| 72 |
+
print(f"错误: SMTP配置不完整。邮件未发送至 {to_email}。请检查 .env 文件中的 SMTP_SERVER, SMTP_USERNAME, SMTP_PASSWORD, SENDER_EMAIL。")
|
| 73 |
+
return False
|
| 74 |
+
|
| 75 |
+
msg = MIMEText(body, 'plain', 'utf-8')
|
| 76 |
+
msg['From'] = f"{SENDER_NAME} <{SENDER_EMAIL}>" # 调整From头部构建方式
|
| 77 |
+
msg['To'] = Header(to_email, 'utf-8')
|
| 78 |
+
msg['Subject'] = Header(subject, 'utf-8')
|
| 79 |
+
|
| 80 |
+
email_sent_successfully = False # 新增标志变量
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
if SMTP_PORT == 587:
|
| 84 |
+
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
|
| 85 |
+
# server.set_debuglevel(1) # 生产环境不启用调试级别
|
| 86 |
+
server.starttls(context=ssl.create_default_context())
|
| 87 |
+
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
| 88 |
+
server.sendmail(SENDER_EMAIL, to_email, msg.as_string())
|
| 89 |
+
print(f"邮件发送成功至 {to_email} (端口: {SMTP_PORT})。")
|
| 90 |
+
email_sent_successfully = True
|
| 91 |
+
|
| 92 |
+
elif SMTP_PORT == 465:
|
| 93 |
+
context = ssl.create_default_context()
|
| 94 |
+
context.check_hostname = False
|
| 95 |
+
context.verify_mode = ssl.CERT_NONE
|
| 96 |
+
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context, timeout=10) as server:
|
| 97 |
+
# server.set_debuglevel(1) # 生产环境不启用调试级别
|
| 98 |
+
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
| 99 |
+
server.sendmail(SENDER_EMAIL, to_email, msg.as_string())
|
| 100 |
+
server.quit() # 明确关闭连接
|
| 101 |
+
print(f"邮件发送成功至 {to_email} (端口: {SMTP_PORT})。")
|
| 102 |
+
email_sent_successfully = True
|
| 103 |
+
else:
|
| 104 |
+
print(f"错误:不支持的端口 {SMTP_PORT}。邮件未发送至 {to_email}。目前只支持 465 (SSL) 和 587 (STARTTLS)。")
|
| 105 |
+
|
| 106 |
+
except smtplib.SMTPAuthenticationError:
|
| 107 |
+
print(f"认证失败:��件未发送至 {to_email}。请检查 .env 文件中的 SMTP_USERNAME 和 SMTP_PASSWORD。对于 QQ 邮箱,请确保使用的是授权码而非登录密码。")
|
| 108 |
+
except smtplib.SMTPConnectError as e:
|
| 109 |
+
print(f"连接失败:邮件未发送至 {to_email}。请检查 SMTP_SERVER 地址、SMTP_PORT 端口是否正确,以及网络防火墙设置。错误详情: {e}")
|
| 110 |
+
except smtplib.SMTPServerDisconnected as e:
|
| 111 |
+
print(f"SMTP 服务器意外断开连接:邮件未发送至 {to_email}。错误详情: {e}")
|
| 112 |
+
except smtplib.SMTPRecipientsRefused as e: # 添加更具体的异常捕获
|
| 113 |
+
print(f"收件人被拒绝:邮件未发送至 {to_email}。错误详情: {e}")
|
| 114 |
+
except smtplib.SMTPSenderRefused as e: # 添加更具体的异常捕获
|
| 115 |
+
print(f"发件人被拒绝:邮件未发送至 {to_email}。错误详情: {e}")
|
| 116 |
+
except smtplib.SMTPDataError as e: # 添加更具体的异常捕获
|
| 117 |
+
print(f"SMTP 数据错误:邮件未发送至 {to_email}。错误详情: {e}")
|
| 118 |
+
except smtplib.SMTPException as e:
|
| 119 |
+
print(f"SMTP 协议错误:邮件未发送至 {to_email}。错误详情: {e}")
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"发生未知错误:邮件未发送至 {to_email}。错误详情: {e}")
|
| 122 |
+
|
| 123 |
+
return email_sent_successfully
|
memory-bank/activeContext.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 活跃上下文 (Active Context)
|
| 2 |
+
|
| 3 |
+
## 当前工作重点
|
| 4 |
+
将 Vue.js/Bootstrap 前端和 Python/FastAPI/Supabase 后端集成到现有的 Docker 化 `spwebsite` 项目中,并确保其可运行。
|
| 5 |
+
|
| 6 |
+
## 最近的更改
|
| 7 |
+
1. **`requirements.txt`**: 添加了 `supabase` 和 `python-dotenv` 依赖。
|
| 8 |
+
2. **`static/` 目录**: 创建了 `static` 目录,用于存放前端静态文件。
|
| 9 |
+
3. **`static/index.html`**: 更新了前端入口文件,添加了用户注册和登录表单,并修正了静态文件引用路径。
|
| 10 |
+
4. **`static/style.css`**: 创建了自定义样式文件。
|
| 11 |
+
5. **`static/app.js`**: 更新了 Vue.js 应用逻辑文件,添加了用户认证状态管理、注册/登录/退出方法,以及在认证后获取代理数据的方法。在成功登录后,将用户认证凭证(API Key)存储在本地存储中。
|
| 12 |
+
6. **`app.py`**:
|
| 13 |
+
* 配置了 FastAPI 以提供静态文件服务。
|
| 14 |
+
* 集成了 `python-dotenv` 来加载 `.env` 文件中的环境变量。
|
| 15 |
+
* 集成了 Supabase 客户端初始化逻辑。
|
| 16 |
+
* 添加了 `/api/auth/signup` 和 `/api/auth/login` API 端点,用于用户注册和登录。
|
| 17 |
+
* 添加了 `/api/user/generate-api-key` 端点,用于已登录用户生成持久的 API Key。
|
| 18 |
+
* 添加了 `/api/user/api-keys` 端点,用于获取用户已生成的 API Key 列表。
|
| 19 |
+
* 添加了 `/api/user/me` 端点,用于获取当前登录用户的电子邮件。
|
| 20 |
+
* 添加了 `/api/auth/change-password` 端点,用于用户修改密码。
|
| 21 |
+
* 修改了 `/api/proxies` 示例 API 端点,使其需要用户认证,并将表名修改为 `sp_proxies`。
|
| 22 |
+
* 添加了 `/api/auth/forgot-password` 端点,用于处理忘记密码请求。
|
| 23 |
+
* 修改了根路由 `/` 以返回 `index.html`。
|
| 24 |
+
* 添加了 `/login` 路由,用于返回 `static/login.html`。
|
| 25 |
+
7. **`README.md`**: 更新了项目概述、技术栈、本地运行指南(包括 `conda` 和 `uvicorn` 命令、`python-dotenv` 安装、`.env` 文件创建说明和 Supabase 用户信息)和部署说明。
|
| 26 |
+
8. **`app.py`**: 恢复了 `signup` 端点,使其使用 `supabase.auth.sign_up` 进行用户注册,而不是管理员创建用户。
|
| 27 |
+
9. **`.env` 文件**: 生成了包含 Supabase 凭证占位符的 `.env` 文件。
|
| 28 |
+
10. **`../solutions/supabase_solution.md`**: 更新了数据表命名规范和示例代码中的表名,并添加了 `python-dotenv` 的使用说明。
|
| 29 |
+
11. **前端 DOM 警告修复**: 解决了 `static/login.html` 和 `static/reset-password.html` 页面中所有 `autocomplete` 属性相关的 DOM 警告。
|
| 30 |
+
12. **重置密码流程更新**:
|
| 31 |
+
* 修改了 `static/reset-password.html` 的前端逻辑,使其不再依赖 `reset_token`,而是通过邮箱和验证码重置密码。
|
| 32 |
+
* 在 `static/js/auth.js` 中添加了 `sendResetVerificationCode` 和 `resetPasswordWithCode` 方法,并更新了 `authMounted` 逻辑。
|
| 33 |
+
* 在后端 `routes/auth.py` 中实现了 `/api/auth/send-reset-password-code` 和 `/api/auth/reset-password-with-code` 两个新接口。
|
| 34 |
+
* 在 `core/models.py` 中定义了 `ResetPasswordWithCodeRequest` 模型。
|
| 35 |
+
* 修复了 `routes/auth.py` 中重复的 `signup` 路由定义。
|
| 36 |
+
* 修改了 `core/utils.py` 中的 `store_verification_code` 和 `verify_stored_code` 函数,使其支持 `prefix` 参数,解决了后端报错。
|
| 37 |
+
|
| 38 |
+
## 下一步计划
|
| 39 |
+
1. 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
| 40 |
+
2. 确保 `.env` 文件中的 Supabase 凭证已正确配置。
|
| 41 |
+
3. 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 42 |
+
4. 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
| 43 |
+
5. 根据实际需求,扩展前端界面和后端 API,实现更多功能。
|
| 44 |
+
|
| 45 |
+
## 下一步计划
|
| 46 |
+
1. 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
| 47 |
+
2. 确保 `.env` 文件中的 Supabase 凭证已正确配置。
|
| 48 |
+
3. 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 49 |
+
4. 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
| 50 |
+
5. 根据实际需求,扩展前端界面和后端 API,实现更多功能。
|
| 51 |
+
|
| 52 |
+
## 活跃的决策和考虑事项
|
| 53 |
+
* **前后端一体**: 项目被确认为前后端一体的 Docker 化应用。
|
| 54 |
+
* **部署环境**: Hugging Face Spaces 通过 `Dockerfile` 自动生成镜像,无需手动 `docker build`。
|
| 55 |
+
* **本地运行**: 使用 `conda activate airs && uvicorn app:app --host 0.0.0.0 --port 7860 --reload` 命令在本地运行。
|
| 56 |
+
* **Supabase 凭证**: 需通过环境变量安全配置。
|
| 57 |
+
|
| 58 |
+
## 学习和项目洞察
|
| 59 |
+
* 理解了 Hugging Face Spaces 的 Docker 部署机制。
|
| 60 |
+
* 确认了 `spwebsite` 项目的集成方式和运行环境。
|
| 61 |
+
* 调试了 Supabase 邮件验证问题,并确认了后端 API 的功能。
|
| 62 |
+
* 成功将认证逻辑分离到独立的登录页面。
|
memory-bank/productContext.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 产品背景 (Product Context)
|
| 2 |
+
|
| 3 |
+
## 项目存在的目的
|
| 4 |
+
`spwebsite` 项目旨在为 `superproxy` 服务提供一个用户友好的管理界面和展示平台。它将允许用户查看代理信息、管理数据,并可能在未来扩展更多功能。
|
| 5 |
+
|
| 6 |
+
## 解决的问题
|
| 7 |
+
1. **缺乏统一界面**: 为 `superproxy` 后端服务提供一个集中的、可视化的管理界面,方便用户操作和监控。
|
| 8 |
+
2. **数据展示**: 能够清晰地展示 Supabase 中存储的代理数据,提高数据可读性。
|
| 9 |
+
3. **简化交互**: 简化用户与后端服务的交互过程,无需直接操作 API 或数据库。
|
| 10 |
+
|
| 11 |
+
## 用户体验目标
|
| 12 |
+
1. **直观易用**: 界面设计应简洁直观,用户能够轻松理解和操作。
|
| 13 |
+
2. **响应迅速**: 前端应用应具有良好的响应性,提供流畅的用户体验。
|
| 14 |
+
3. **信息清晰**: 关键信息(如代理状态、数据)应清晰地呈现给用户。
|
| 15 |
+
4. **可靠稳定**: 确保前后端集成稳定可靠,数据交互准确无误。
|
memory-bank/progress.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 进度 (Progress)
|
| 2 |
+
|
| 3 |
+
## 已完成的工作
|
| 4 |
+
1. **项目初始化**: 根据 `sp_website_solution.md` 和 `supabase_solution.md` 进行了项目结构调整。
|
| 5 |
+
2. **前端集成**:
|
| 6 |
+
* 创建了 `static/` 目录。
|
| 7 |
+
* 更新了 `static/index.html`,添加了用户注册和登录表单,并修正了静态文件引用路径。
|
| 8 |
+
* 创建了 `static/style.css`。
|
| 9 |
+
* 更新了 `static/app.js`,添加了用户认证状态管理、注册/登录/退出方法,以及在认证后获取代理数据的方法。在成功登录后,将用户认证凭证(API Key)存储在本地存储中。同时,添加了获取用户电子邮件和修改密码的逻辑。
|
| 10 |
+
* 解决了 `static/login.html` 和 `static/reset-password.html` 页面中所有 `autocomplete` 属性相关的 DOM 警告。
|
| 11 |
+
* 修改了 `static/reset-password.html` 的前端逻辑,使其不再依赖 `reset_token`,而是通过邮箱和验证码重置密码。
|
| 12 |
+
* 在 `static/js/auth.js` 中添加了 `sendResetVerificationCode` 和 `resetPasswordWithCode` 方法,并更新了 `authMounted` 逻辑。
|
| 13 |
+
3. **后端集成**:
|
| 14 |
+
* 更新了 `requirements.txt`,添加了 `supabase` 和 `python-dotenv` 依赖。
|
| 15 |
+
* 修改了 `app.py`,使其能够提供静态文件服务,集成了 `python-dotenv` 来加载环境变量,集成了 Supabase 客户端初始化,添加了 `/api/auth/signup` 和 `/api/auth/login` API 端点,用于用户注册和登录,添加了 `/api/user/generate-api-key` 端点用于生成 API Key,添加了 `/api/user/api-keys` 端点用于获取 API Key 列表,添加了 `/api/user/me` 端点用于获取当前登录用户的电子邮件,添加了 `/api/auth/change-password` 端点用于用户修改密码,并修改了 `/api/proxies` 示例 API 端点使其需要用户认证,并将表名修改为 `sp_proxies`。
|
| 16 |
+
* 恢复了 `app.py` 中的 `signup` 端点,使其使用 `supabase.auth.sign_up` 进行用户注册,而不是管理员创建用户。
|
| 17 |
+
* 创建了 `static/login.html`,用于独立的登录/注册页面。
|
| 18 |
+
* 修改了 `static/index.html`,移除了认证表单,并添加了指向 `/login` 的链接。
|
| 19 |
+
* 修改了 `static/app.js`,调整了 Vue.js 逻辑,使其在 `index.html` 和 `login.html` 中都能正确工作,并添加了重定向逻辑。
|
| 20 |
+
* 修改了 `app.py`,添加了 `/login` 路由,用于返回 `static/login.html`。
|
| 21 |
+
* 在 `routes/auth.py` 中实现了 `/api/auth/send-reset-password-code` 和 `/api/auth/reset-password-with-code` 两个新接口。
|
| 22 |
+
* 在 `core/models.py` 中定义了 `ResetPasswordWithCodeRequest` 模型。
|
| 23 |
+
* 修复了 `routes/auth.py` 中重复的 `signup` 路由定义。
|
| 24 |
+
* 修改了 `core/utils.py` 中的 `store_verification_code` 和 `verify_stored_code` 函数,使其支持 `prefix` 参数,解决了后端报错。
|
| 25 |
+
4. **文档更新**: 更新了 `README.md`,反映了项目的新功能、技术栈和正确的本地运行指南。
|
| 26 |
+
5. **Memory Bank 更新**: 更新了所有核心 Memory Bank 文件以反映项目最新状态。
|
| 27 |
+
6. **Supabase 解决方案文件更新**: 更新了 `../solutions/supabase_solution.md`。
|
| 28 |
+
7. **`.env` 文件生成**: 生成了包含 Supabase 凭证占位符的 `.env` 文件。
|
| 29 |
+
|
| 30 |
+
## 剩余的工作
|
| 31 |
+
1. **Supabase 数据库设置**: 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
| 32 |
+
2. **前端数据展示**: 在 `static/app.js` 中实现 `fetchProxies` 方法,使其能够调用后端 `/api/proxies` 端点并显示从 Supabase 获取的数据。
|
| 33 |
+
3. **功能扩展**: 根据实际需求,进一步开发前端界面和后端 API,实现更多功能。
|
| 34 |
+
4. **Supabase 认证配置**: 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 35 |
+
5. **测试**: 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
| 36 |
+
|
| 37 |
+
## 当前状态
|
| 38 |
+
项目已完成前后端一体框架的搭建,前端和后端的基本集成已完成,并实现了用户注册、登录、API Key 申请、获取用户电子邮件、修改密码和忘记密码功能。认证逻辑已分离到独立的登录页面。项目现在可以在本地通过 `conda` 和 `uvicorn` 命令运行,并准备好与 Supabase 数据库进行实际的数据交互。
|
| 39 |
+
|
| 40 |
+
## 已知问题
|
| 41 |
+
* `static/app.js` 中的 `fetchProxies` 方法目前是注释掉的,需要手动启用并根据 Supabase 实际数据结构进行调整。
|
| 42 |
+
* Supabase 数据库的 `sp_proxies` 表和 `sp_user_api_keys` 表尚未创建,需要手动在 Supabase 控制台完成。
|
| 43 |
+
* 环境变量 `SUPABASE_URL` 和 `SUPABASE_ANON_KEY` 需要在本地 `.env` 文件或部署环境中配置。
|
| 44 |
+
* `SUPABASE_SERVICE_ROLE_KEY` 需要在 `.env` 文件中配置,以便后端能够修改用户密码。
|
| 45 |
+
* Supabase 的邮件认证可能需要额外配置,否则用户注册后需要手动确认邮件。
|
memory-bank/projectBrief.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 项目简报 (Project Brief)
|
| 2 |
+
|
| 3 |
+
## 项目名称
|
| 4 |
+
SP Website
|
| 5 |
+
|
| 6 |
+
## 项目目标
|
| 7 |
+
构建一个前后端一体的 Docker 化 Web 应用,提供基于 Vue.js 和 Bootstrap 的前端界面,并与使用 Python (FastAPI) 和 Supabase 数据库的后端服务进行交互。
|
| 8 |
+
|
| 9 |
+
## 核心需求
|
| 10 |
+
1. **前端界面**: 基于 Vue.js (CDN) 和 Bootstrap 5.3 (CDN) 的响应式用户界面。
|
| 11 |
+
2. **后端服务**: 使用 Python (FastAPI) 提供 RESTful API,处理业务逻辑。
|
| 12 |
+
3. **数据存储**: 集成 Supabase (PostgreSQL) 作为主要数据库。
|
| 13 |
+
4. **容器化**: 项目应完全 Docker 化,支持在 Docker 环境中构建、运行和部署。
|
| 14 |
+
5. **部署**: 兼容 Hugging Face Spaces 的 Docker 部署流程。
|
| 15 |
+
|
| 16 |
+
## 范围
|
| 17 |
+
本项目涵盖前端界面开发、后端 API 实现、Supabase 数据库集成和 Docker 化部署。
|
memory-bank/systemPatterns.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 系统模式 (System Patterns)
|
| 2 |
+
|
| 3 |
+
## 系统架构
|
| 4 |
+
`spwebsite` 采用前后端一体的单体应用架构,通过 Docker 进行容器化部署。
|
| 5 |
+
|
| 6 |
+
* **前端**: 静态文件 (HTML, CSS, JavaScript) 由后端 FastAPI 应用提供服务。
|
| 7 |
+
* **后端**: Python FastAPI 应用处理业务逻辑、API 请求,并与 Supabase 数据库交互。
|
| 8 |
+
* **数据库**: 外部托管的 Supabase (PostgreSQL) 数据库。
|
| 9 |
+
|
| 10 |
+
```mermaid
|
| 11 |
+
graph TD
|
| 12 |
+
Browser --> FastAPI_App[FastAPI 应用 (Python)]
|
| 13 |
+
FastAPI_App --> StaticFiles[提供静态文件 (Vue.js/Bootstrap)]
|
| 14 |
+
FastAPI_App --> Supabase_DB[Supabase 数据库 (PostgreSQL)]
|
| 15 |
+
StaticFiles --> Browser
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
## 关键技术决策
|
| 19 |
+
1. **前后端一体**: 简化部署和开发流程,尤其适用于 Hugging Face Spaces 的 Docker 部署。
|
| 20 |
+
2. **FastAPI**: 选择 FastAPI 作为后端框架,因为它性能高、易于使用,并内置 OpenAPI/Swagger 文档。
|
| 21 |
+
3. **Vue.js (CDN)**: 采用 CDN 方式引入 Vue.js,避免了复杂的构建工具链,简化了前端开发和部署。
|
| 22 |
+
4. **Bootstrap 5.3 (CDN)**: 同样通过 CDN 引入,提供快速响应式 UI 开发能力。
|
| 23 |
+
5. **Supabase**: 作为后端即服务 (BaaS) 解决方案,提供托管的 PostgreSQL 数据库、认证和 API,大大减少了后端基础设施的维护工作。
|
| 24 |
+
6. **Docker**: 容器化确保了开发、测试和生产环境的一致性。
|
| 25 |
+
|
| 26 |
+
## 设计模式
|
| 27 |
+
* **MVC/MVVM (前端)**: Vue.js 自然地支持 MVVM 模式,通过数据绑定和组件化管理 UI 状态和行为。
|
| 28 |
+
* **RESTful API (后端)**: 后端通过 RESTful 风格的 API 端点与前端通信。
|
| 29 |
+
* **依赖注入 (后端)**: FastAPI 通过其依赖注入系统简化了请求处理和资源管理。
|
| 30 |
+
|
| 31 |
+
## 组件关系
|
| 32 |
+
* **`index.html`**: 前端应用的入口,加载所有静态资源和 Vue 应用。
|
| 33 |
+
* **`style.css`**: 定义全局和组件级样式。
|
| 34 |
+
* **`app.js`**: 包含 Vue 根实例,管理应用状态和行为,通过 `fetch` API 调用后端。
|
| 35 |
+
* **`app.py`**:
|
| 36 |
+
* 使用 `StaticFiles` 挂载 `static/` 目录,提供前端文件。
|
| 37 |
+
* 定义 API 路由 (`/api/proxies`),处理前端请求。
|
| 38 |
+
* 通过 `supabase` 客户端与 Supabase 数据库进行 CRUD 操作。
|
| 39 |
+
* **Supabase 数据库**: 存储 `sp_proxies` 等应用数据(表名前缀为 `sp_`),并通过 RLS 提供数据安全。
|
memory-bank/techContext.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 技术背景 (Tech Context)
|
| 2 |
+
|
| 3 |
+
## 使用的技术
|
| 4 |
+
* **前端**:
|
| 5 |
+
* HTML5, CSS3, JavaScript
|
| 6 |
+
* Vue.js 3 (通过 CDN 引入)
|
| 7 |
+
* Bootstrap 5.3 (通过 CDN 引入)
|
| 8 |
+
* **后端**:
|
| 9 |
+
* Python 3.x
|
| 10 |
+
* FastAPI (Web 框架)
|
| 11 |
+
* Uvicorn (ASGI 服务器)
|
| 12 |
+
* Supabase Python SDK (数据库客户端)
|
| 13 |
+
* **数据库**: Supabase (托管 PostgreSQL)
|
| 14 |
+
* **容器化**: Docker
|
| 15 |
+
|
| 16 |
+
## 开发设置
|
| 17 |
+
* **Python 环境**: 建议使用 Conda 管理 Python 环境,并激活 `airs` 环境。
|
| 18 |
+
* **依赖管理**: `requirements.txt` 文件用于管理 Python 依赖。
|
| 19 |
+
* **环境变量**: Supabase 凭证 (`SUPABASE_URL`, `SUPABASE_ANON_KEY`) 通过环境变量配置。
|
| 20 |
+
* **本地运行**: 使用 `conda activate airs && uvicorn app:app --host 0.0.0.0 --port 7860 --reload` 命令。
|
| 21 |
+
|
| 22 |
+
## 技术约束
|
| 23 |
+
* **CDN 依赖**: 前端 Vue.js 和 Bootstrap 依赖于 CDN,需要网络连接才能加载。
|
| 24 |
+
* **Supabase 外部依赖**: 数据库服务由 Supabase 托管,需要稳定的网络连接。
|
| 25 |
+
* **Hugging Face Spaces 限制**: 部署到 Hugging Face Spaces 时,需要遵循其平台规范和资源限制。
|
| 26 |
+
|
| 27 |
+
## 依赖
|
| 28 |
+
* **Python 依赖**: `fastapi`, `uvicorn[standard]`, `supabase` (列于 `requirements.txt`)。
|
| 29 |
+
* **前端依赖**: Vue.js 3, Bootstrap 5.3 (通过 CDN 引入)。
|
| 30 |
+
|
| 31 |
+
## 工具使用模式
|
| 32 |
+
* **Git**: 用于版本控制和代码部署到 Hugging Face Spaces。
|
| 33 |
+
* **Docker**: 用于容器化应用,确保环境一致性。
|
| 34 |
+
* **Conda**: 用于管理 Python 虚拟环境。
|
| 35 |
+
* **uvicorn**: 用于在本地运行 FastAPI 应用。
|
push.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
git add .
|
| 2 |
+
git commit -m "update"
|
| 3 |
+
git push
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
supabase
|
| 4 |
+
python-dotenv
|
| 5 |
+
passlib==1.7.4
|
| 6 |
+
bcrypt==4.0.1
|
| 7 |
+
PyJWT
|
| 8 |
+
gotrue
|
| 9 |
+
pytest
|
| 10 |
+
httpx
|
routes/admin.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, File, UploadFile, HTTPException, status, Depends, Query
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
import os
|
| 4 |
+
from supabase import create_client, Client
|
| 5 |
+
from gotrue.errors import AuthApiError
|
| 6 |
+
from pydantic import BaseModel # Import BaseModel for UserListResponse
|
| 7 |
+
|
| 8 |
+
from core.config import SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
|
| 9 |
+
from core.dependencies import get_current_admin_user, get_supabase_client
|
| 10 |
+
from core.models import User, AdminUser, UserUpdate
|
| 11 |
+
from core.utils import get_password_hash
|
| 12 |
+
|
| 13 |
+
router = APIRouter()
|
| 14 |
+
|
| 15 |
+
UPLOAD_DIRECTORY = "static/images"
|
| 16 |
+
|
| 17 |
+
@router.post("/upload-image")
|
| 18 |
+
async def upload_image(file: UploadFile = File(...), current_user: User = Depends(get_current_admin_user)):
|
| 19 |
+
"""
|
| 20 |
+
上传图片到 /static/images 目录。
|
| 21 |
+
"""
|
| 22 |
+
if not os.path.exists(UPLOAD_DIRECTORY):
|
| 23 |
+
os.makedirs(UPLOAD_DIRECTORY)
|
| 24 |
+
|
| 25 |
+
file_location = os.path.join(UPLOAD_DIRECTORY, file.filename)
|
| 26 |
+
try:
|
| 27 |
+
with open(file_location, "wb+") as file_object:
|
| 28 |
+
file_object.write(await file.read())
|
| 29 |
+
return {"filename": file.filename, "path": f"/{UPLOAD_DIRECTORY}/{file.filename}", "message": "图片上传成功"}
|
| 30 |
+
except Exception as e:
|
| 31 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"图片上传失败: {e}")
|
| 32 |
+
|
| 33 |
+
class UserListResponse(BaseModel):
|
| 34 |
+
users: List[AdminUser]
|
| 35 |
+
total_count: int
|
| 36 |
+
|
| 37 |
+
@router.get("/users", response_model=UserListResponse) # Update response_model
|
| 38 |
+
async def get_all_users(
|
| 39 |
+
current_user: User = Depends(get_current_admin_user),
|
| 40 |
+
supabase_client: Client = Depends(get_supabase_client),
|
| 41 |
+
page: int = Query(1, ge=1),
|
| 42 |
+
page_size: int = Query(10, ge=1, le=100),
|
| 43 |
+
search: Optional[str] = Query(None)
|
| 44 |
+
):
|
| 45 |
+
"""
|
| 46 |
+
获取所有用户列表(仅限管理员)。
|
| 47 |
+
"""
|
| 48 |
+
offset = (page - 1) * page_size
|
| 49 |
+
query = supabase_client.table('sp_users').select('id, email, email_verified, created_at, is_admin, disabled', count='exact') # Include disabled field
|
| 50 |
+
|
| 51 |
+
if search:
|
| 52 |
+
query = query.ilike('email', f"%{search}%") # Case-insensitive search by email
|
| 53 |
+
|
| 54 |
+
res = query.order('created_at', desc=True).range(offset, offset + page_size - 1).execute()
|
| 55 |
+
|
| 56 |
+
users = [AdminUser(**user) for user in res.data]
|
| 57 |
+
total_count = res.count
|
| 58 |
+
|
| 59 |
+
return {"users": users, "total_count": total_count}
|
| 60 |
+
|
| 61 |
+
@router.get("/users/{user_id}", response_model=AdminUser)
|
| 62 |
+
async def get_user_by_id(
|
| 63 |
+
user_id: str,
|
| 64 |
+
current_user: User = Depends(get_current_admin_user),
|
| 65 |
+
supabase_client: Client = Depends(get_supabase_client)
|
| 66 |
+
):
|
| 67 |
+
"""
|
| 68 |
+
根据用户ID获取单个用户信息(仅限管理员)。
|
| 69 |
+
"""
|
| 70 |
+
res = supabase_client.table('sp_users').select('id, email, email_verified, created_at, is_admin, disabled').eq('id', user_id).single().execute() # Include disabled field
|
| 71 |
+
if not res.data:
|
| 72 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户未找到")
|
| 73 |
+
return AdminUser(**res.data)
|
| 74 |
+
|
| 75 |
+
@router.put("/users/{user_id}", response_model=AdminUser)
|
| 76 |
+
async def update_user(
|
| 77 |
+
user_id: str,
|
| 78 |
+
user_update: UserUpdate,
|
| 79 |
+
current_user: User = Depends(get_current_admin_user),
|
| 80 |
+
supabase_client: Client = Depends(get_supabase_client)
|
| 81 |
+
):
|
| 82 |
+
"""
|
| 83 |
+
更新用户信息(仅限管理员)。
|
| 84 |
+
"""
|
| 85 |
+
update_data = user_update.dict(exclude_unset=True)
|
| 86 |
+
|
| 87 |
+
if 'password' in update_data and update_data['password']:
|
| 88 |
+
update_data['password_hash'] = get_password_hash(update_data['password'])
|
| 89 |
+
del update_data['password'] # Remove plain password from update_data
|
| 90 |
+
|
| 91 |
+
# Handle disabled field update
|
| 92 |
+
if 'disabled' in update_data and update_data['disabled'] is not None:
|
| 93 |
+
# Supabase update for 'disabled' field
|
| 94 |
+
pass # The update_data dictionary already contains 'disabled' if it was set
|
| 95 |
+
|
| 96 |
+
if not update_data:
|
| 97 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="没有提供更新数据")
|
| 98 |
+
|
| 99 |
+
res = supabase_client.table('sp_users').update(update_data).eq('id', user_id).execute()
|
| 100 |
+
|
| 101 |
+
if not res.data:
|
| 102 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户未找到或更新失败")
|
| 103 |
+
|
| 104 |
+
# Fetch the updated user to return
|
| 105 |
+
updated_user_res = supabase_client.table('sp_users').select('id, email, email_verified, created_at, is_admin, disabled').eq('id', user_id).single().execute() # Include disabled field
|
| 106 |
+
return AdminUser(**updated_user_res.data)
|
| 107 |
+
|
| 108 |
+
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 109 |
+
async def delete_user(
|
| 110 |
+
user_id: str,
|
| 111 |
+
current_user: User = Depends(get_current_admin_user),
|
| 112 |
+
supabase_client: Client = Depends(get_supabase_client)
|
| 113 |
+
):
|
| 114 |
+
"""
|
| 115 |
+
删除用户(仅限管理员)。
|
| 116 |
+
"""
|
| 117 |
+
# First, delete associated API keys
|
| 118 |
+
supabase_client.table('sp_user_api_keys').delete().eq('user_id', user_id).execute()
|
| 119 |
+
|
| 120 |
+
# Then delete the user from sp_users table
|
| 121 |
+
res = supabase_client.table('sp_users').delete().eq('id', user_id).execute()
|
| 122 |
+
|
| 123 |
+
if not res.data:
|
| 124 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户未找到或删除失败")
|
| 125 |
+
return
|
routes/api_keys.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import secrets
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import secrets
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from fastapi import APIRouter, HTTPException, Depends, status
|
| 6 |
+
from supabase import Client
|
| 7 |
+
|
| 8 |
+
from core.config import SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
|
| 9 |
+
from core.models import User, ApiKeyCreateResponse, ApiKeyItem
|
| 10 |
+
from core.dependencies import get_current_active_user, get_supabase_client # Import get_supabase_client
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
|
| 14 |
+
@router.post("/generate-api-key", response_model=ApiKeyCreateResponse)
|
| 15 |
+
async def generate_api_key(
|
| 16 |
+
current_user: User = Depends(get_current_active_user),
|
| 17 |
+
supabase_client: Client = Depends(get_supabase_client) # Inject Supabase client
|
| 18 |
+
):
|
| 19 |
+
try:
|
| 20 |
+
# Generate a secure random API Key
|
| 21 |
+
new_api_key = secrets.token_urlsafe(32) # 32 bytes = 43 characters
|
| 22 |
+
|
| 23 |
+
# Store the API Key in the database
|
| 24 |
+
data, count = supabase_client.table('sp_user_api_keys').insert({
|
| 25 |
+
"user_id": current_user.id,
|
| 26 |
+
"api_key": new_api_key,
|
| 27 |
+
"created_at": datetime.now().isoformat() # Store creation time
|
| 28 |
+
}).execute()
|
| 29 |
+
|
| 30 |
+
if data:
|
| 31 |
+
return ApiKeyCreateResponse(api_key=new_api_key)
|
| 32 |
+
else:
|
| 33 |
+
raise HTTPException(
|
| 34 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 35 |
+
detail="Failed to store API Key."
|
| 36 |
+
)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 39 |
+
|
| 40 |
+
@router.get("/api-keys", response_model=list[ApiKeyItem])
|
| 41 |
+
async def get_user_api_keys(
|
| 42 |
+
current_user: User = Depends(get_current_active_user),
|
| 43 |
+
supabase_client: Client = Depends(get_supabase_client) # Inject Supabase client
|
| 44 |
+
):
|
| 45 |
+
try:
|
| 46 |
+
res = supabase_client.table('sp_user_api_keys').select('api_key, created_at').eq('user_id', current_user.id).execute()
|
| 47 |
+
return [ApiKeyItem(**item) for item in res.data]
|
| 48 |
+
except Exception as e:
|
| 49 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 50 |
+
|
| 51 |
+
@router.get("/me", response_model=User)
|
| 52 |
+
async def get_me(current_user: User = Depends(get_current_active_user)):
|
| 53 |
+
"""
|
| 54 |
+
获取当前登录用户的详细信息。
|
| 55 |
+
"""
|
| 56 |
+
return current_user
|
routes/auth.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from fastapi import APIRouter, HTTPException, Depends, status, Request
|
| 4 |
+
from pydantic import BaseModel # Import BaseModel for EmailRequest
|
| 5 |
+
from supabase import create_client, Client # Import create_client
|
| 6 |
+
from gotrue.errors import AuthApiError
|
| 7 |
+
|
| 8 |
+
from core.config import SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SECRET_KEY, ALGORITHM, VERIFICATION_CODE_EXPIRE_MINUTES
|
| 9 |
+
from core.models import UserCredentials, ForgotPasswordRequest, Token, User, ChangePasswordRequest, ResetPasswordWithCodeRequest # Import new model
|
| 10 |
+
from core.utils import verify_password, get_password_hash, create_access_token, send_email, \
|
| 11 |
+
generate_verification_code, store_verification_code, verify_stored_code # Import verification code utilities
|
| 12 |
+
from core.dependencies import get_current_active_user, get_current_user_from_token
|
| 13 |
+
import jwt # Import jwt for change_password
|
| 14 |
+
|
| 15 |
+
class EmailRequest(BaseModel):
|
| 16 |
+
email: str
|
| 17 |
+
|
| 18 |
+
router = APIRouter()
|
| 19 |
+
|
| 20 |
+
# Supabase Client (service role for custom user management)
|
| 21 |
+
service_supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
| 22 |
+
|
| 23 |
+
@router.post("/send-verification-code")
|
| 24 |
+
async def send_verification_code(email_request: EmailRequest):
|
| 25 |
+
"""
|
| 26 |
+
生成并发送邮箱验证码。
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
# Check if user already exists
|
| 30 |
+
res = service_supabase.table('sp_users').select('id').eq('email', email_request.email).execute()
|
| 31 |
+
if res.data and len(res.data) > 0:
|
| 32 |
+
# 如果邮箱已注册,返回一个不同的消息,而不是抛出错误
|
| 33 |
+
return {"message": "此邮箱已被注册,请直接登录或尝试找回密码。"}
|
| 34 |
+
|
| 35 |
+
code = generate_verification_code()
|
| 36 |
+
store_verification_code(email_request.email, code)
|
| 37 |
+
|
| 38 |
+
email_body = f"您好,\n\n您的验证码是:{code}\n\n此验证码将在 {VERIFICATION_CODE_EXPIRE_MINUTES} 分钟后失效。\n\n如果您没有请求此验证码,请忽略此邮件。\n\n此致,\nSuperProxy Support"
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
print(f"Attempting to send verification email to {email_request.email}...")
|
| 42 |
+
email_sent = send_email(email_request.email, "您的验证码", email_body)
|
| 43 |
+
if email_sent:
|
| 44 |
+
print(f"Verification email successfully sent to {email_request.email}.")
|
| 45 |
+
return {"message": "验证码已发送,请检查您的邮箱。"}
|
| 46 |
+
else:
|
| 47 |
+
print(f"Failed to send verification email to {email_request.email}. send_email returned False.")
|
| 48 |
+
raise HTTPException(
|
| 49 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 50 |
+
detail="发送验证码失败。请联系管理员或检查邮箱配置。"
|
| 51 |
+
)
|
| 52 |
+
except Exception as email_error:
|
| 53 |
+
print(f"Error sending verification email to {email_request.email}: {email_error}")
|
| 54 |
+
raise HTTPException(
|
| 55 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 56 |
+
detail="发送验证码失败。请检查邮箱配置或联系管理员。"
|
| 57 |
+
)
|
| 58 |
+
except HTTPException:
|
| 59 |
+
raise
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"Error in send_verification_code: {e}")
|
| 62 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 63 |
+
|
| 64 |
+
@router.post("/send-reset-password-code")
|
| 65 |
+
async def send_reset_password_code(email_request: EmailRequest):
|
| 66 |
+
"""
|
| 67 |
+
生成并发送用于重置密码的邮箱验证码。
|
| 68 |
+
"""
|
| 69 |
+
try:
|
| 70 |
+
# Check if user exists
|
| 71 |
+
res = service_supabase.table('sp_users').select('id').eq('email', email_request.email).execute()
|
| 72 |
+
if not res.data or len(res.data) == 0:
|
| 73 |
+
raise HTTPException(
|
| 74 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 75 |
+
detail="此邮箱未注册。"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
code = generate_verification_code()
|
| 79 |
+
store_verification_code(email_request.email, code, prefix="reset_") # Use a different prefix for reset codes
|
| 80 |
+
|
| 81 |
+
email_body = f"您好,\n\n您请求重置密码。您的验证码是:{code}\n\n此验证码将在 {VERIFICATION_CODE_EXPIRE_MINUTES} 分钟后失效。\n\n如果您没有请求此操作,请忽略此邮件。\n\n此致,\nSuperProxy Support"
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
print(f"Attempting to send reset password verification email to {email_request.email}...")
|
| 85 |
+
email_sent = send_email(email_request.email, "您的重置密码验证码", email_body)
|
| 86 |
+
if email_sent:
|
| 87 |
+
print(f"Reset password verification email successfully sent to {email_request.email}.")
|
| 88 |
+
return {"message": "重置密码验证码已发送,请检查您的邮箱。"}
|
| 89 |
+
else:
|
| 90 |
+
print(f"Failed to send reset password verification email to {email_request.email}. send_email returned False.")
|
| 91 |
+
raise HTTPException(
|
| 92 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 93 |
+
detail="发送重置密码验证码��败。请联系管理员或检查邮箱配置。"
|
| 94 |
+
)
|
| 95 |
+
except Exception as email_error:
|
| 96 |
+
print(f"Error sending reset password verification email to {email_request.email}: {email_error}")
|
| 97 |
+
raise HTTPException(
|
| 98 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 99 |
+
detail="发送重置密码验证码失败。请检查邮箱配置或联系管理员。"
|
| 100 |
+
)
|
| 101 |
+
except HTTPException:
|
| 102 |
+
raise
|
| 103 |
+
except Exception as e:
|
| 104 |
+
print(f"Error in send_reset_password_code: {e}")
|
| 105 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@router.post("/signup")
|
| 109 |
+
async def signup(user_credentials: UserCredentials, request: Request):
|
| 110 |
+
try:
|
| 111 |
+
# Check if user already exists in sp_users table
|
| 112 |
+
res = service_supabase.table('sp_users').select('id').eq('email', user_credentials.email).execute()
|
| 113 |
+
if res.data and len(res.data) > 0:
|
| 114 |
+
raise HTTPException(
|
| 115 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 116 |
+
detail="此邮箱已被注册。"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Verify the provided verification code
|
| 120 |
+
if not verify_stored_code(user_credentials.email, user_credentials.verification_code):
|
| 121 |
+
raise HTTPException(
|
| 122 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 123 |
+
detail="验证码不正确或已过期。"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
hashed_password = get_password_hash(user_credentials.password)
|
| 127 |
+
|
| 128 |
+
# Insert user into sp_users table after successful email verification
|
| 129 |
+
data, count = service_supabase.table('sp_users').insert({
|
| 130 |
+
"email": user_credentials.email,
|
| 131 |
+
"password_hash": hashed_password,
|
| 132 |
+
"created_at": datetime.now().isoformat(),
|
| 133 |
+
"email_verified": True, # Mark email as verified
|
| 134 |
+
"is_admin": False # New users are not admins by default
|
| 135 |
+
}).execute()
|
| 136 |
+
|
| 137 |
+
if data:
|
| 138 |
+
return {"message": "注册成功!您现在可以登录。"}
|
| 139 |
+
else:
|
| 140 |
+
raise HTTPException(
|
| 141 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 142 |
+
detail="注册失败,无法创建用户。请联系管理员。"
|
| 143 |
+
)
|
| 144 |
+
except HTTPException:
|
| 145 |
+
raise
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"Error in signup: {e}")
|
| 148 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 149 |
+
|
| 150 |
+
@router.get("/verify-email")
|
| 151 |
+
async def verify_email(token: str):
|
| 152 |
+
try:
|
| 153 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 154 |
+
email: str = payload.get("sub")
|
| 155 |
+
token_type: str = payload.get("type")
|
| 156 |
+
password_hash: str = payload.get("password_hash")
|
| 157 |
+
|
| 158 |
+
if email is None or token_type != "email_verification" or password_hash is None:
|
| 159 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired verification token.")
|
| 160 |
+
|
| 161 |
+
# Check if user already exists (e.g., if verification link was clicked multiple times)
|
| 162 |
+
res = service_supabase.table('sp_users').select('id').eq('email', email).execute()
|
| 163 |
+
if res.data and len(res.data) > 0:
|
| 164 |
+
return {"message": "邮箱已验证,您现在可以登录。"}
|
| 165 |
+
|
| 166 |
+
# Insert user into sp_users table after successful email verification
|
| 167 |
+
data, count = service_supabase.table('sp_users').insert({
|
| 168 |
+
"email": email,
|
| 169 |
+
"password_hash": password_hash,
|
| 170 |
+
"created_at": datetime.now().isoformat(),
|
| 171 |
+
"email_verified": True, # Mark email as verified
|
| 172 |
+
"is_admin": False # New users are not admins by default
|
| 173 |
+
}).execute()
|
| 174 |
+
|
| 175 |
+
if data:
|
| 176 |
+
return {"message": "邮箱验证成功!您现在可以登录。"}
|
| 177 |
+
else:
|
| 178 |
+
raise HTTPException(
|
| 179 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 180 |
+
detail="邮箱验证失败,无法创建用户。请联系管理员。"
|
| 181 |
+
)
|
| 182 |
+
except jwt.PyJWTError:
|
| 183 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired verification token.")
|
| 184 |
+
except Exception as e:
|
| 185 |
+
print(f"Error in verify_email: {e}")
|
| 186 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 187 |
+
|
| 188 |
+
@router.post("/login", response_model=Token)
|
| 189 |
+
async def login(user_credentials: UserCredentials):
|
| 190 |
+
try:
|
| 191 |
+
try:
|
| 192 |
+
res = service_supabase.table('sp_users').select('id, email, password_hash').eq('email', user_credentials.email).single().execute()
|
| 193 |
+
user_data = res.data
|
| 194 |
+
except AuthApiError as e: # Catch Supabase specific errors
|
| 195 |
+
if "The result contains 0 rows" in e.message:
|
| 196 |
+
raise HTTPException(
|
| 197 |
+
status_code=status.HTTP_404_NOT_FOUND, # 404 for user not found
|
| 198 |
+
detail="用户名不存在。"
|
| 199 |
+
)
|
| 200 |
+
else:
|
| 201 |
+
print(f"AuthApiError fetching user in login: {e}")
|
| 202 |
+
raise HTTPException(
|
| 203 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 204 |
+
detail="用户名或密码不正确,请重新输入。"
|
| 205 |
+
)
|
| 206 |
+
except Exception as e:
|
| 207 |
+
print(f"Unexpected error fetching user in login: {e}")
|
| 208 |
+
raise HTTPException(
|
| 209 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 210 |
+
detail="邮箱或密码错误。请重新输入!"
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
if not verify_password(user_credentials.password, user_data['password_hash']):
|
| 214 |
+
raise HTTPException(
|
| 215 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 216 |
+
detail="用户名或密码不正确,请重新输入。"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
access_token_expires = timedelta(minutes=30) # ACCESS_TOKEN_EXPIRE_MINUTES from config
|
| 220 |
+
access_token = create_access_token(
|
| 221 |
+
data={"sub": user_data['id']}, expires_delta=access_token_expires
|
| 222 |
+
)
|
| 223 |
+
return Token(access_token=access_token)
|
| 224 |
+
except HTTPException:
|
| 225 |
+
raise
|
| 226 |
+
except Exception as e:
|
| 227 |
+
print(f"Unexpected error in login: {e}")
|
| 228 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="邮箱或密码错误。请重新输入!")
|
| 229 |
+
|
| 230 |
+
@router.post("/forgot-password")
|
| 231 |
+
async def forgot_password(request: Request, forgot_password_request: ForgotPasswordRequest):
|
| 232 |
+
"""
|
| 233 |
+
发送密码重置邮件。
|
| 234 |
+
"""
|
| 235 |
+
try:
|
| 236 |
+
try:
|
| 237 |
+
res = service_supabase.table('sp_users').select('id').eq('email', forgot_password_request.email).single().execute()
|
| 238 |
+
user_data = res.data
|
| 239 |
+
except AuthApiError as e:
|
| 240 |
+
if "The result contains 0 rows" in e.message:
|
| 241 |
+
raise HTTPException(
|
| 242 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 243 |
+
detail="邮箱不存在!"
|
| 244 |
+
)
|
| 245 |
+
else:
|
| 246 |
+
print(f"AuthApiError fetching user in forgot_password: {e}")
|
| 247 |
+
raise HTTPException(
|
| 248 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 249 |
+
detail="发送密码重置邮件失败,请稍后再试。"
|
| 250 |
+
)
|
| 251 |
+
except Exception as e:
|
| 252 |
+
print(f"Unexpected error fetching user in forgot_password: {e}")
|
| 253 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="邮箱不存在!")
|
| 254 |
+
|
| 255 |
+
user_id = user_data['id']
|
| 256 |
+
|
| 257 |
+
# Generate a password reset token
|
| 258 |
+
reset_token_expires = timedelta(hours=1) # Token valid for 1 hour
|
| 259 |
+
reset_token = create_access_token(
|
| 260 |
+
data={"sub": user_id, "type": "password_reset"}, expires_delta=reset_token_expires
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
# Construct reset link
|
| 264 |
+
# Assuming the frontend reset-password page is at /reset-password
|
| 265 |
+
# And it expects a 'token' query parameter
|
| 266 |
+
reset_link = f"{request.url.scheme}://{request.url.netloc}/reset-password?token={reset_token}"
|
| 267 |
+
|
| 268 |
+
email_body = f"您好,\n\n您请求重置密码。请点击以下链接重置您的密码:\n\n{reset_link}\n\n此链接将在1小时后失效。\n\n如果您没有请求此操作,请忽略此邮件。\n\n此致,\nSuperProxy Support"
|
| 269 |
+
|
| 270 |
+
print(f"Attempting to send password reset email to {forgot_password_request.email}...")
|
| 271 |
+
email_sent = send_email(forgot_password_request.email, "密码重置请求", email_body)
|
| 272 |
+
if email_sent:
|
| 273 |
+
print(f"Password reset email successfully sent to {forgot_password_request.email}.")
|
| 274 |
+
return {"message": "密码重置邮件已发送,请检查您的邮箱。"}
|
| 275 |
+
else:
|
| 276 |
+
print(f"Failed to send password reset email to {forgot_password_request.email}. send_email returned False.")
|
| 277 |
+
raise HTTPException(
|
| 278 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 279 |
+
detail="发送密码重置邮件失败,请检查服务器日志或邮箱配置。"
|
| 280 |
+
)
|
| 281 |
+
except HTTPException:
|
| 282 |
+
raise
|
| 283 |
+
except Exception as e:
|
| 284 |
+
print(f"Unexpected error in forgot_password: {e}")
|
| 285 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="邮箱不存在!")
|
| 286 |
+
|
| 287 |
+
@router.post("/change-password")
|
| 288 |
+
async def change_password(
|
| 289 |
+
password_request: ChangePasswordRequest,
|
| 290 |
+
current_user: Optional[User] = Depends(get_current_user_from_token) # Only allow token auth for this
|
| 291 |
+
):
|
| 292 |
+
"""
|
| 293 |
+
允许已登录用户修改其密码,或通过重置令牌修改密码。
|
| 294 |
+
"""
|
| 295 |
+
try:
|
| 296 |
+
user_id_to_update = None
|
| 297 |
+
if password_request.reset_token:
|
| 298 |
+
# Verify reset token
|
| 299 |
+
try:
|
| 300 |
+
payload = jwt.decode(password_request.reset_token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 301 |
+
token_type = payload.get("type")
|
| 302 |
+
user_id_to_update = payload.get("sub")
|
| 303 |
+
if token_type != "password_reset" or user_id_to_update is None:
|
| 304 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid reset token type.")
|
| 305 |
+
except jwt.PyJWTError:
|
| 306 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired reset token.")
|
| 307 |
+
elif current_user:
|
| 308 |
+
user_id_to_update = current_user.id
|
| 309 |
+
else:
|
| 310 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated or no reset token provided.")
|
| 311 |
+
|
| 312 |
+
if not user_id_to_update:
|
| 313 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Could not determine user to update.")
|
| 314 |
+
|
| 315 |
+
hashed_password = get_password_hash(password_request.new_password)
|
| 316 |
+
|
| 317 |
+
data, count = service_supabase.table('sp_users').update({'password_hash': hashed_password}).eq('id', user_id_to_update).execute()
|
| 318 |
+
|
| 319 |
+
if data:
|
| 320 |
+
return {"message": "Password updated successfully. Please log in again with your new password."}
|
| 321 |
+
else:
|
| 322 |
+
raise HTTPException(
|
| 323 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 324 |
+
detail="Failed to update password."
|
| 325 |
+
)
|
| 326 |
+
except Exception as e:
|
| 327 |
+
print(f"Error in change_password: {e}")
|
| 328 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 329 |
+
|
| 330 |
+
@router.post("/reset-password-with-code")
|
| 331 |
+
async def reset_password_with_code(reset_request: ResetPasswordWithCodeRequest):
|
| 332 |
+
"""
|
| 333 |
+
通过邮箱、验证码和新密码重置用户密码。
|
| 334 |
+
"""
|
| 335 |
+
try:
|
| 336 |
+
# 1. Check if user exists
|
| 337 |
+
res = service_supabase.table('sp_users').select('id').eq('email', reset_request.email).execute()
|
| 338 |
+
if not res.data or len(res.data) == 0:
|
| 339 |
+
raise HTTPException(
|
| 340 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 341 |
+
detail="此邮箱未注册。"
|
| 342 |
+
)
|
| 343 |
+
user_id = res.data[0]['id']
|
| 344 |
+
|
| 345 |
+
# 2. Verify the provided verification code
|
| 346 |
+
if not verify_stored_code(reset_request.email, reset_request.verification_code, prefix="reset_"):
|
| 347 |
+
raise HTTPException(
|
| 348 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 349 |
+
detail="验证码不正确或已过期。"
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
# 3. Hash the new password
|
| 353 |
+
hashed_password = get_password_hash(reset_request.new_password)
|
| 354 |
+
|
| 355 |
+
# 4. Update user's password in sp_users table
|
| 356 |
+
data, count = service_supabase.table('sp_users').update({'password_hash': hashed_password}).eq('id', user_id).execute()
|
| 357 |
+
|
| 358 |
+
if data:
|
| 359 |
+
return {"message": "密码重置成功!请使用新密码登录。"}
|
| 360 |
+
else:
|
| 361 |
+
raise HTTPException(
|
| 362 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 363 |
+
detail="密码重置失败,无法更新密码。请联系管理员。"
|
| 364 |
+
)
|
| 365 |
+
except HTTPException:
|
| 366 |
+
raise
|
| 367 |
+
except Exception as e:
|
| 368 |
+
print(f"Error in reset_password_with_code: {e}")
|
| 369 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
routes/proxies.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Depends, status
|
| 2 |
+
from supabase import Client
|
| 3 |
+
|
| 4 |
+
from core.config import SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
|
| 5 |
+
from core.models import User
|
| 6 |
+
from core.dependencies import get_current_active_user, get_supabase_client # Import get_supabase_client
|
| 7 |
+
|
| 8 |
+
router = APIRouter()
|
| 9 |
+
|
| 10 |
+
@router.get("/")
|
| 11 |
+
async def get_proxies(
|
| 12 |
+
current_user: User = Depends(get_current_active_user),
|
| 13 |
+
supabase_client: Client = Depends(get_supabase_client) # Inject Supabase client
|
| 14 |
+
):
|
| 15 |
+
try:
|
| 16 |
+
# Example: Fetch active proxies from Supabase for the authenticated user
|
| 17 |
+
# You might filter by user_id if your 'sp_proxies' table has a user_id column
|
| 18 |
+
res = supabase_client.table('sp_proxies').select('*').eq('is_active', True).execute()
|
| 19 |
+
return res.data
|
| 20 |
+
except Exception as e:
|
| 21 |
+
raise HTTPException(status_code=500, detail=str(e))
|
static/admin.html
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>SP Website - 后台管理</title>
|
| 7 |
+
<!-- Bootstrap 5.3 CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Font Awesome CDN for icons -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app">
|
| 16 |
+
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
| 17 |
+
<div class="container-fluid">
|
| 18 |
+
<a class="navbar-brand d-flex align-items-center" href="/">
|
| 19 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="30" height="30" class="d-inline-block align-text-top me-2">
|
| 20 |
+
<span class="fw-bold fs-5">API Router</span>
|
| 21 |
+
</a>
|
| 22 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 23 |
+
<span class="navbar-toggler-icon"></span>
|
| 24 |
+
</button>
|
| 25 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 26 |
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 27 |
+
<li class="nav-item">
|
| 28 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 29 |
+
<i class="fas fa-comment-dots me-1"></i> 聊天
|
| 30 |
+
</a>
|
| 31 |
+
</li>
|
| 32 |
+
<li class="nav-item">
|
| 33 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 34 |
+
<i class="fas fa-key me-1"></i> 令牌
|
| 35 |
+
</a>
|
| 36 |
+
</li>
|
| 37 |
+
<li class="nav-item">
|
| 38 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 39 |
+
<i class="fas fa-shopping-cart me-1"></i> 充值
|
| 40 |
+
</a>
|
| 41 |
+
</li>
|
| 42 |
+
<li class="nav-item">
|
| 43 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 44 |
+
<i class="fas fa-chart-bar me-1"></i> 总览
|
| 45 |
+
</a>
|
| 46 |
+
</li>
|
| 47 |
+
<li class="nav-item">
|
| 48 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 49 |
+
<i class="fas fa-file-alt me-1"></i> 日志
|
| 50 |
+
</a>
|
| 51 |
+
</li>
|
| 52 |
+
<li class="nav-item">
|
| 53 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 54 |
+
<i class="fas fa-cog me-1"></i> 设置
|
| 55 |
+
</a>
|
| 56 |
+
</li>
|
| 57 |
+
<li class="nav-item">
|
| 58 |
+
<a class="nav-link d-flex align-items-center me-3" href="#" @click="showUserManagement = true; showImageUpload = false;">
|
| 59 |
+
<i class="fas fa-users-cog me-1"></i> 用户管理
|
| 60 |
+
</a>
|
| 61 |
+
</li>
|
| 62 |
+
<li class="nav-item">
|
| 63 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 64 |
+
<i class="fas fa-info-circle me-1"></i> 关于
|
| 65 |
+
</a>
|
| 66 |
+
</li>
|
| 67 |
+
</ul>
|
| 68 |
+
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
| 69 |
+
<li class="nav-item dropdown">
|
| 70 |
+
<a class="nav-link dropdown-toggle d-flex align-items-center me-3" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 71 |
+
<i class="fas fa-font me-1"></i> A|文
|
| 72 |
+
</a>
|
| 73 |
+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
| 74 |
+
<li><a class="dropdown-item" href="#">中文</a></li>
|
| 75 |
+
<li><a class="dropdown-item" href="#">English</a></li>
|
| 76 |
+
</ul>
|
| 77 |
+
</li>
|
| 78 |
+
<li class="nav-item">
|
| 79 |
+
<a class="nav-link d-flex align-items-center" href="/login">
|
| 80 |
+
<i class="fas fa-user me-1"></i> 登录
|
| 81 |
+
</a>
|
| 82 |
+
</li>
|
| 83 |
+
</ul>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</nav>
|
| 87 |
+
|
| 88 |
+
<div class="container mt-5 pt-5">
|
| 89 |
+
<h2 class="mb-4">后台管理</h2>
|
| 90 |
+
|
| 91 |
+
<div class="card shadow-sm p-4 mb-4" v-if="showImageUpload">
|
| 92 |
+
<h4 class="mb-3">图片上传</h4>
|
| 93 |
+
<form @submit.prevent="uploadImage" enctype="multipart/form-data">
|
| 94 |
+
<div class="mb-3">
|
| 95 |
+
<label for="imageUpload" class="form-label">选择图片文件</label>
|
| 96 |
+
<input class="form-control" type="file" id="imageUpload" @change="handleFileUpload" required>
|
| 97 |
+
</div>
|
| 98 |
+
<button type="submit" class="btn btn-primary" :disabled="isUploading">
|
| 99 |
+
<span v-if="isUploading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 100 |
+
{{ isUploading ? '上传中...' : '上传图片' }}
|
| 101 |
+
</button>
|
| 102 |
+
</form>
|
| 103 |
+
<p v-if="uploadMessage" class="mt-3 text-info">{{ uploadMessage }}</p>
|
| 104 |
+
<div v-if="uploadedImageUrl" class="mt-3">
|
| 105 |
+
<h5>上传的图片:</h5>
|
| 106 |
+
<img :src="uploadedImageUrl" alt="Uploaded Image" class="img-fluid" style="max-width: 300px;">
|
| 107 |
+
<p class="mt-2">URL: <a :href="uploadedImageUrl" target="_blank">{{ uploadedImageUrl }}</a></p>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<!-- 用户管理模块 -->
|
| 112 |
+
<div class="card shadow-sm p-4 mb-4" v-if="showUserManagement">
|
| 113 |
+
<h4 class="mb-3">用户管理</h4>
|
| 114 |
+
<div class="input-group mb-3">
|
| 115 |
+
<input type="text" class="form-control" placeholder="按邮箱搜索用户" v-model="userSearchQuery" @keyup.enter="fetchUsers">
|
| 116 |
+
<button class="btn btn-outline-secondary" type="button" @click="fetchUsers">搜索</button>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="table-responsive">
|
| 119 |
+
<table class="table table-striped table-hover">
|
| 120 |
+
<thead>
|
| 121 |
+
<tr>
|
| 122 |
+
<th>ID</th>
|
| 123 |
+
<th>邮箱</th>
|
| 124 |
+
<th>已验证</th>
|
| 125 |
+
<th>管理员</th>
|
| 126 |
+
<th>禁用</th> <!-- Add new column header -->
|
| 127 |
+
<th>创建时间</th>
|
| 128 |
+
<th>操作</th>
|
| 129 |
+
</tr>
|
| 130 |
+
</thead>
|
| 131 |
+
<tbody>
|
| 132 |
+
<tr v-for="user in users" :key="user.id">
|
| 133 |
+
<td>{{ user.id }}</td>
|
| 134 |
+
<td>{{ user.email }}</td>
|
| 135 |
+
<td>
|
| 136 |
+
<span v-if="user.email_verified" class="badge bg-success">是</span>
|
| 137 |
+
<span v-else class="badge bg-danger">否</span>
|
| 138 |
+
</td>
|
| 139 |
+
<td>
|
| 140 |
+
<span v-if="user.is_admin" class="badge bg-primary">是</span>
|
| 141 |
+
<span v-else class="badge bg-secondary">否</span>
|
| 142 |
+
</td>
|
| 143 |
+
<td>
|
| 144 |
+
<span v-if="user.disabled" class="badge bg-warning">是</span> <!-- Display disabled status -->
|
| 145 |
+
<span v-else class="badge bg-success">否</span>
|
| 146 |
+
</td>
|
| 147 |
+
<td>{{ new Date(user.created_at).toLocaleString() }}</td>
|
| 148 |
+
<td>
|
| 149 |
+
<button class="btn btn-sm btn-info me-2" @click="editUser(user)">编辑</button>
|
| 150 |
+
<button class="btn btn-sm btn-danger" @click="deleteUser(user.id)">删除</button>
|
| 151 |
+
</td>
|
| 152 |
+
</tr>
|
| 153 |
+
</tbody>
|
| 154 |
+
</table>
|
| 155 |
+
</div>
|
| 156 |
+
<nav aria-label="User pagination" v-if="totalPages > 1">
|
| 157 |
+
<ul class="pagination justify-content-center">
|
| 158 |
+
<li class="page-item" :class="{ disabled: currentPage === 1 }">
|
| 159 |
+
<a class="page-link" href="#" @click.prevent="changePage(currentPage - 1)">上一页</a>
|
| 160 |
+
</li>
|
| 161 |
+
<li class="page-item" v-for="pageNumber in totalPages" :key="pageNumber" :class="{ active: pageNumber === currentPage }">
|
| 162 |
+
<a class="page-link" href="#" @click.prevent="changePage(pageNumber)">{{ pageNumber }}</a>
|
| 163 |
+
</li>
|
| 164 |
+
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
|
| 165 |
+
<a class="page-link" href="#" @click.prevent="changePage(currentPage + 1)">下一页</a>
|
| 166 |
+
</li>
|
| 167 |
+
</ul>
|
| 168 |
+
</nav>
|
| 169 |
+
<p v-if="userMessage" class="mt-3 text-info">{{ userMessage }}</p>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<!-- 编辑用户模态框 -->
|
| 173 |
+
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
|
| 174 |
+
<div class="modal-dialog">
|
| 175 |
+
<div class="modal-content">
|
| 176 |
+
<div class="modal-header">
|
| 177 |
+
<h5 class="modal-title" id="editUserModalLabel">编辑用户</h5>
|
| 178 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="modal-body">
|
| 181 |
+
<form @submit.prevent="saveUserChanges">
|
| 182 |
+
<div class="mb-3">
|
| 183 |
+
<label for="editUserEmail" class="form-label">邮箱</label>
|
| 184 |
+
<input type="email" class="form-control" id="editUserEmail" v-model="editingUser.email" required>
|
| 185 |
+
</div>
|
| 186 |
+
<div class="mb-3">
|
| 187 |
+
<label for="editUserPassword" class="form-label">新密码 (留空则不修改)</label>
|
| 188 |
+
<input type="password" class="form-control" id="editUserPassword" v-model="editingUser.password">
|
| 189 |
+
</div>
|
| 190 |
+
<div class="mb-3 form-check">
|
| 191 |
+
<input type="checkbox" class="form-check-input" id="editEmailVerified" v-model="editingUser.email_verified">
|
| 192 |
+
<label class="form-check-label" for="editEmailVerified">邮箱已验证</label>
|
| 193 |
+
</div>
|
| 194 |
+
<div class="mb-3 form-check">
|
| 195 |
+
<input type="checkbox" class="form-check-input" id="editIsAdmin" v-model="editingUser.is_admin">
|
| 196 |
+
<label class="form-check-label" for="editIsAdmin">管理员</label>
|
| 197 |
+
</div>
|
| 198 |
+
<div class="mb-3 form-check">
|
| 199 |
+
<input type="checkbox" class="form-check-input" id="editIsDisabled" v-model="editingUser.disabled">
|
| 200 |
+
<label class="form-check-label" for="editIsDisabled">禁用</label>
|
| 201 |
+
</div>
|
| 202 |
+
<button type="submit" class="btn btn-primary" :disabled="isSavingUser">
|
| 203 |
+
<span v-if="isSavingUser" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 204 |
+
{{ isSavingUser ? '保存中...' : '保存更改' }}
|
| 205 |
+
</button>
|
| 206 |
+
</form>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
</div>
|
| 213 |
+
|
| 214 |
+
<footer class="footer text-center py-3 mt-auto">
|
| 215 |
+
<p class="mb-0 text-muted">API Router v0.6.11-preview.6 由 JustSong 构建,源代码遵循 <a href="#" class="text-decoration-none">MIT 协议</a></p>
|
| 216 |
+
</footer>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<!-- Vue 3 CDN -->
|
| 220 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 221 |
+
<!-- Bootstrap 5.3 JS CDN -->
|
| 222 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 223 |
+
<!-- Custom Vue.js App Logic -->
|
| 224 |
+
<script type="module" src="/static/js/admin.js"></script>
|
static/app.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/app.js
|
| 2 |
+
import { authData, authMethods, authMounted } from './js/auth.js';
|
| 3 |
+
import { proxyData, proxyMethods } from './js/proxy.js';
|
| 4 |
+
import { appData, appComputed, appMethods, appMounted } from './js/store.js';
|
| 5 |
+
|
| 6 |
+
const app = Vue.createApp({
|
| 7 |
+
data() {
|
| 8 |
+
return {
|
| 9 |
+
...appData(),
|
| 10 |
+
...authData(),
|
| 11 |
+
...proxyData(),
|
| 12 |
+
};
|
| 13 |
+
},
|
| 14 |
+
computed: {
|
| 15 |
+
...appComputed,
|
| 16 |
+
},
|
| 17 |
+
methods: {
|
| 18 |
+
...appMethods(),
|
| 19 |
+
...authMethods(this), // Pass 'this' (app instance) if methods need access to other parts of the app
|
| 20 |
+
...proxyMethods(),
|
| 21 |
+
},
|
| 22 |
+
mounted() {
|
| 23 |
+
appMounted(this); // Pass 'this' (app instance) to mounted logic
|
| 24 |
+
authMounted(this); // Pass 'this' (app instance) to auth mounted logic
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
app.mount('#app');
|
static/index.html
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>API Router</title>
|
| 7 |
+
<!-- Bootstrap 5.3 CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Font Awesome for icons -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app">
|
| 16 |
+
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
| 17 |
+
<div class="container-fluid">
|
| 18 |
+
<a class="navbar-brand d-flex align-items-center" href="/">
|
| 19 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="30" height="30" class="d-inline-block align-text-top me-2">
|
| 20 |
+
<span class="fw-bold fs-5"></span>API Router</span>
|
| 21 |
+
</a>
|
| 22 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 23 |
+
<span class="navbar-toggler-icon"></span>
|
| 24 |
+
</button>
|
| 25 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 26 |
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 27 |
+
<li class="nav-item">
|
| 28 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 29 |
+
<i class="fas fa-comment-dots me-1"></i> 聊天
|
| 30 |
+
</a>
|
| 31 |
+
</li>
|
| 32 |
+
<li class="nav-item">
|
| 33 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 34 |
+
<i class="fas fa-key me-1"></i> 令牌
|
| 35 |
+
</a>
|
| 36 |
+
</li>
|
| 37 |
+
<li class="nav-item">
|
| 38 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 39 |
+
<i class="fas fa-shopping-cart me-1"></i> 充值
|
| 40 |
+
</a>
|
| 41 |
+
</li>
|
| 42 |
+
<li class="nav-item">
|
| 43 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 44 |
+
<i class="fas fa-chart-bar me-1"></i> 总览
|
| 45 |
+
</a>
|
| 46 |
+
</li>
|
| 47 |
+
<li class="nav-item">
|
| 48 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 49 |
+
<i class="fas fa-file-alt me-1"></i> 日志
|
| 50 |
+
</a>
|
| 51 |
+
</li>
|
| 52 |
+
<li class="nav-item">
|
| 53 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 54 |
+
<i class="fas fa-cog me-1"></i> 设置
|
| 55 |
+
</a>
|
| 56 |
+
</li>
|
| 57 |
+
<li class="nav-item">
|
| 58 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 59 |
+
<i class="fas fa-info-circle me-1"></i> 关于
|
| 60 |
+
</a>
|
| 61 |
+
</li>
|
| 62 |
+
</ul>
|
| 63 |
+
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
| 64 |
+
<li class="nav-item dropdown">
|
| 65 |
+
<a class="nav-link dropdown-toggle d-flex align-items-center me-3" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 66 |
+
<i class="fas fa-font me-1"></i> A|文
|
| 67 |
+
</a>
|
| 68 |
+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
| 69 |
+
<li><a class="dropdown-item" href="#">中文</a></li>
|
| 70 |
+
<li><a class="dropdown-item" href="#">English</a></li>
|
| 71 |
+
</ul>
|
| 72 |
+
</li>
|
| 73 |
+
<li class="nav-item dropdown" v-if="isLoggedIn">
|
| 74 |
+
<a class="nav-link dropdown-toggle d-flex align-items-center me-3" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 75 |
+
<i class="fas fa-user me-1"></i> {{ userEmail }}
|
| 76 |
+
</a>
|
| 77 |
+
<ul class="dropdown-menu" aria-labelledby="userDropdown">
|
| 78 |
+
<li><a class="dropdown-item" href="#" @click="showChangePasswordModal = true">修改密码</a></li>
|
| 79 |
+
<li><hr class="dropdown-divider"></li>
|
| 80 |
+
<li><a class="dropdown-item" href="#" @click="logout">登出</a></li>
|
| 81 |
+
</ul>
|
| 82 |
+
</li>
|
| 83 |
+
<li class="nav-item" v-else>
|
| 84 |
+
<a class="nav-link d-flex align-items-center" href="/login">
|
| 85 |
+
<i class="fas fa-user me-1"></i> 登录
|
| 86 |
+
</a>
|
| 87 |
+
</li>
|
| 88 |
+
</ul>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</nav>
|
| 92 |
+
|
| 93 |
+
<div class="container-fluid hero-section text-center d-flex align-items-center justify-content-center">
|
| 94 |
+
<div class="row justify-content-center">
|
| 95 |
+
<div class="col-lg-8 col-md-10">
|
| 96 |
+
<h1 class="display-3 fw-bold mb-4 hero-title">API Router</h1>
|
| 97 |
+
<p class="lead mb-5 hero-description">
|
| 98 |
+
一个 LLM API 接口管理和分发系统,可以帮助您更好地管理和使用各大厂商的 LLM API。
|
| 99 |
+
</p>
|
| 100 |
+
<div class="d-grid gap-3 d-sm-flex justify-content-sm-center">
|
| 101 |
+
<template v-if="!isLoggedIn">
|
| 102 |
+
<a class="btn btn-primary btn-lg px-4 me-sm-3" href="/login">登录</a>
|
| 103 |
+
<a class="btn btn-outline-secondary btn-lg px-4" href="/signup">注册</a>
|
| 104 |
+
</template>
|
| 105 |
+
<template v-else>
|
| 106 |
+
<a class="btn btn-primary btn-lg px-4 me-sm-3" href="#">开始使用</a>
|
| 107 |
+
</template>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<footer class="footer text-center py-3 mt-auto">
|
| 114 |
+
<p class="mb-0 text-muted">API Router v0.6.11-preview.6 由 JustSong 构建,源代码遵循 <a href="#" class="text-decoration-none">MIT 协议</a></p>
|
| 115 |
+
</footer>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<!-- Vue 3 CDN -->
|
| 119 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 120 |
+
<!-- Bootstrap 5.3 JS CDN -->
|
| 121 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 122 |
+
<!-- Custom Vue.js App Logic -->
|
| 123 |
+
<script type="module" src="/static/app.js"></script>
|
| 124 |
+
</body>
|
| 125 |
+
</html>
|
static/js/admin.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/admin.js
|
| 2 |
+
// static/js/admin.js
|
| 3 |
+
// Vue 3 CDN 版本将 createApp 暴露在全局 Vue 对象下
|
| 4 |
+
const { createApp } = Vue;
|
| 5 |
+
|
| 6 |
+
import { authData, authMethods, authMounted } from './auth.js';
|
| 7 |
+
import { proxyData, proxyMethods } from './proxy.js';
|
| 8 |
+
import { appData, appMethods, appMounted } from './store.js'; // Corrected import names
|
| 9 |
+
|
| 10 |
+
const app = createApp({
|
| 11 |
+
data() {
|
| 12 |
+
return {
|
| 13 |
+
...authData(),
|
| 14 |
+
...proxyData(),
|
| 15 |
+
...appData(), // Corrected data spread
|
| 16 |
+
selectedFile: null,
|
| 17 |
+
isUploading: false,
|
| 18 |
+
uploadMessage: '',
|
| 19 |
+
uploadedImageUrl: '',
|
| 20 |
+
showImageUpload: true, // 控制图片上传模块的显示
|
| 21 |
+
showUserManagement: false, // 控制用户管理模块的显示
|
| 22 |
+
|
| 23 |
+
// 用户管理相关数据
|
| 24 |
+
users: [],
|
| 25 |
+
userSearchQuery: '',
|
| 26 |
+
currentPage: 1,
|
| 27 |
+
pageSize: 10,
|
| 28 |
+
totalUsers: 0,
|
| 29 |
+
totalPages: 0,
|
| 30 |
+
userMessage: '',
|
| 31 |
+
editingUser: {
|
| 32 |
+
id: null,
|
| 33 |
+
email: '',
|
| 34 |
+
password: '',
|
| 35 |
+
email_verified: false,
|
| 36 |
+
is_admin: false
|
| 37 |
+
},
|
| 38 |
+
isSavingUser: false,
|
| 39 |
+
editUserModal: null // Bootstrap Modal instance
|
| 40 |
+
};
|
| 41 |
+
},
|
| 42 |
+
methods: {
|
| 43 |
+
...authMethods(this),
|
| 44 |
+
...proxyMethods(),
|
| 45 |
+
...appMethods(this), // Corrected methods spread
|
| 46 |
+
handleFileUpload(event) {
|
| 47 |
+
this.selectedFile = event.target.files[0];
|
| 48 |
+
},
|
| 49 |
+
async uploadImage() {
|
| 50 |
+
if (!this.selectedFile) {
|
| 51 |
+
this.uploadMessage = '请选择一个文件。';
|
| 52 |
+
return;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
this.isUploading = true;
|
| 56 |
+
this.uploadMessage = '';
|
| 57 |
+
this.uploadedImageUrl = '';
|
| 58 |
+
|
| 59 |
+
const formData = new FormData();
|
| 60 |
+
formData.append('file', this.selectedFile);
|
| 61 |
+
|
| 62 |
+
try {
|
| 63 |
+
const token = localStorage.getItem('access_token');
|
| 64 |
+
const response = await fetch('/api/admin/upload-image', {
|
| 65 |
+
method: 'POST',
|
| 66 |
+
headers: {
|
| 67 |
+
'Authorization': `Bearer ${token}`
|
| 68 |
+
},
|
| 69 |
+
body: formData
|
| 70 |
+
});
|
| 71 |
+
const data = await response.json();
|
| 72 |
+
|
| 73 |
+
if (response.ok) {
|
| 74 |
+
this.uploadMessage = data.message || '图片上传成功!';
|
| 75 |
+
this.uploadedImageUrl = data.path;
|
| 76 |
+
// 清空文件输入框
|
| 77 |
+
document.getElementById('imageUpload').value = '';
|
| 78 |
+
this.selectedFile = null;
|
| 79 |
+
} else {
|
| 80 |
+
this.uploadMessage = data.detail || '图片上传失败。';
|
| 81 |
+
}
|
| 82 |
+
} catch (error) {
|
| 83 |
+
this.uploadMessage = '网络错误或服务器无响应。';
|
| 84 |
+
console.error('图片上传错误:', error);
|
| 85 |
+
} finally {
|
| 86 |
+
this.isUploading = false;
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
async fetchUsers() {
|
| 90 |
+
this.userMessage = '加载用户中...';
|
| 91 |
+
try {
|
| 92 |
+
const token = localStorage.getItem('access_token');
|
| 93 |
+
let url = `/api/admin/users?page=${this.currentPage}&page_size=${this.pageSize}`;
|
| 94 |
+
if (this.userSearchQuery) {
|
| 95 |
+
url += `&search=${encodeURIComponent(this.userSearchQuery)}`;
|
| 96 |
+
}
|
| 97 |
+
const response = await fetch(url, {
|
| 98 |
+
headers: {
|
| 99 |
+
'Authorization': `Bearer ${token}`
|
| 100 |
+
}
|
| 101 |
+
});
|
| 102 |
+
const responseData = await response.json();
|
| 103 |
+
|
| 104 |
+
if (response.ok) {
|
| 105 |
+
this.users = responseData.users;
|
| 106 |
+
this.totalUsers = responseData.total_count;
|
| 107 |
+
this.totalPages = Math.ceil(this.totalUsers / this.pageSize);
|
| 108 |
+
this.userMessage = '';
|
| 109 |
+
console.log('Fetched users successfully. totalUsers:', this.totalUsers, 'totalPages:', this.totalPages); // Add log
|
| 110 |
+
} else {
|
| 111 |
+
this.userMessage = responseData.detail || '获取用户失败。';
|
| 112 |
+
}
|
| 113 |
+
} catch (error) {
|
| 114 |
+
this.userMessage = '网络错误或服务器无响应。';
|
| 115 |
+
console.error('获取用户错误:', error);
|
| 116 |
+
}
|
| 117 |
+
},
|
| 118 |
+
changePage(pageNumber) {
|
| 119 |
+
if (pageNumber > 0 && pageNumber <= this.totalPages) {
|
| 120 |
+
this.currentPage = pageNumber;
|
| 121 |
+
this.fetchUsers();
|
| 122 |
+
}
|
| 123 |
+
},
|
| 124 |
+
editUser(user) {
|
| 125 |
+
this.editingUser = { ...user, password: '' }; // Clear password field for security
|
| 126 |
+
this.editUserModal.show();
|
| 127 |
+
},
|
| 128 |
+
async saveUserChanges() {
|
| 129 |
+
this.isSavingUser = true;
|
| 130 |
+
this.userMessage = '';
|
| 131 |
+
try {
|
| 132 |
+
const token = localStorage.getItem('access_token');
|
| 133 |
+
const updatePayload = {
|
| 134 |
+
email: this.editingUser.email,
|
| 135 |
+
email_verified: this.editingUser.email_verified,
|
| 136 |
+
is_admin: this.editingUser.is_admin
|
| 137 |
+
};
|
| 138 |
+
if (this.editingUser.password) {
|
| 139 |
+
updatePayload.password = this.editingUser.password;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
const response = await fetch(`/api/admin/users/${this.editingUser.id}`, {
|
| 143 |
+
method: 'PUT',
|
| 144 |
+
headers: {
|
| 145 |
+
'Content-Type': 'application/json',
|
| 146 |
+
'Authorization': `Bearer ${token}`
|
| 147 |
+
},
|
| 148 |
+
body: JSON.stringify(updatePayload)
|
| 149 |
+
});
|
| 150 |
+
const data = await response.json();
|
| 151 |
+
|
| 152 |
+
if (response.ok) {
|
| 153 |
+
this.userMessage = '用户更新成功!';
|
| 154 |
+
this.fetchUsers(); // Refresh user list
|
| 155 |
+
this.editUserModal.hide();
|
| 156 |
+
} else {
|
| 157 |
+
this.userMessage = data.detail || '用户更新失败。';
|
| 158 |
+
}
|
| 159 |
+
} catch (error) {
|
| 160 |
+
this.userMessage = '网络错误或服务器无响应。';
|
| 161 |
+
console.error('更新用户错误:', error);
|
| 162 |
+
} finally {
|
| 163 |
+
this.isSavingUser = false;
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
async deleteUser(userId) {
|
| 167 |
+
if (!confirm('确定要删除此用户吗?此操作不可逆!')) {
|
| 168 |
+
return;
|
| 169 |
+
}
|
| 170 |
+
this.userMessage = '删除用户中...';
|
| 171 |
+
try {
|
| 172 |
+
const token = localStorage.getItem('access_token');
|
| 173 |
+
const response = await fetch(`/api/admin/users/${userId}`, {
|
| 174 |
+
method: 'DELETE',
|
| 175 |
+
headers: {
|
| 176 |
+
'Authorization': `Bearer ${token}`
|
| 177 |
+
}
|
| 178 |
+
});
|
| 179 |
+
|
| 180 |
+
if (response.ok) {
|
| 181 |
+
this.userMessage = '用户删除成功!';
|
| 182 |
+
this.fetchUsers(); // Refresh user list
|
| 183 |
+
} else {
|
| 184 |
+
const data = await response.json();
|
| 185 |
+
this.userMessage = data.detail || '用户删除失败。';
|
| 186 |
+
}
|
| 187 |
+
} catch (error) {
|
| 188 |
+
this.userMessage = '网络错误或服务器无响应。';
|
| 189 |
+
console.error('删除用户错误:', error);
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
mounted() {
|
| 194 |
+
authMounted(this);
|
| 195 |
+
// proxyMounted(this); // Removed proxyMounted call
|
| 196 |
+
appMounted(this); // Corrected mounted call
|
| 197 |
+
console.log('Admin app mounted!');
|
| 198 |
+
console.log('Initial totalPages:', this.totalPages); // Add log
|
| 199 |
+
|
| 200 |
+
// Initialize Bootstrap Modal
|
| 201 |
+
this.editUserModal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
| 202 |
+
|
| 203 |
+
// Fetch users when the user management tab is active
|
| 204 |
+
if (this.showUserManagement) {
|
| 205 |
+
this.fetchUsers();
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
watch: {
|
| 209 |
+
showUserManagement(newValue) {
|
| 210 |
+
if (newValue) {
|
| 211 |
+
this.fetchUsers();
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
});
|
| 216 |
+
|
| 217 |
+
app.mount('#app');
|
| 218 |
+
console.log('Vue app successfully mounted to #app element.'); // Add this log
|
static/js/auth.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/auth.js
|
| 2 |
+
export const authData = () => ({
|
| 3 |
+
isLoggedIn: false,
|
| 4 |
+
userEmail: '',
|
| 5 |
+
signupForm: {
|
| 6 |
+
email: '',
|
| 7 |
+
password: '',
|
| 8 |
+
verificationCode: ''
|
| 9 |
+
},
|
| 10 |
+
isSendingCode: false,
|
| 11 |
+
countdown: 60,
|
| 12 |
+
verificationCodeMessage: '',
|
| 13 |
+
loginForm: {
|
| 14 |
+
email: '',
|
| 15 |
+
password: ''
|
| 16 |
+
},
|
| 17 |
+
signupMessage: '',
|
| 18 |
+
authMessage: '', // Combined message for login and forgot password
|
| 19 |
+
showChangePasswordModal: false,
|
| 20 |
+
changePasswordForm: {
|
| 21 |
+
newPassword: '',
|
| 22 |
+
confirmPassword: ''
|
| 23 |
+
},
|
| 24 |
+
changePasswordMessage: '',
|
| 25 |
+
resetPasswordForm: {
|
| 26 |
+
email: '',
|
| 27 |
+
verificationCode: '', // Add verification code field for reset password
|
| 28 |
+
newPassword: '',
|
| 29 |
+
confirmPassword: ''
|
| 30 |
+
},
|
| 31 |
+
resetPasswordMessage: ''
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
export const authMethods = (app) => ({
|
| 35 |
+
resetPasswordLink() {
|
| 36 |
+
const email = this.loginForm.email;
|
| 37 |
+
if (email) {
|
| 38 |
+
return `/reset-password?email=${encodeURIComponent(email)}`;
|
| 39 |
+
}
|
| 40 |
+
return '/reset-password';
|
| 41 |
+
},
|
| 42 |
+
async sendVerificationCode() {
|
| 43 |
+
console.log('sendVerificationCode method triggered.');
|
| 44 |
+
console.log('Current email:', this.signupForm.email);
|
| 45 |
+
this.verificationCodeMessage = '';
|
| 46 |
+
// 更严格的邮箱格式校验
|
| 47 |
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
| 48 |
+
if (!this.signupForm.email || !emailRegex.test(this.signupForm.email)) {
|
| 49 |
+
this.verificationCodeMessage = '请输入有效的邮箱地址。';
|
| 50 |
+
console.log('Invalid email format.');
|
| 51 |
+
return;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
this.isSendingCode = true;
|
| 55 |
+
let timer = setInterval(() => {
|
| 56 |
+
if (this.countdown > 0) {
|
| 57 |
+
this.countdown--;
|
| 58 |
+
} else {
|
| 59 |
+
clearInterval(timer);
|
| 60 |
+
this.isSendingCode = false;
|
| 61 |
+
this.countdown = 60;
|
| 62 |
+
}
|
| 63 |
+
}, 1000);
|
| 64 |
+
|
| 65 |
+
try {
|
| 66 |
+
const response = await fetch('/api/auth/send-verification-code', {
|
| 67 |
+
method: 'POST',
|
| 68 |
+
headers: {
|
| 69 |
+
'Content-Type': 'application/json'
|
| 70 |
+
},
|
| 71 |
+
body: JSON.stringify({ email: this.signupForm.email })
|
| 72 |
+
});
|
| 73 |
+
const data = await response.json();
|
| 74 |
+
if (response.ok) {
|
| 75 |
+
this.verificationCodeMessage = data.message || '验证码已发送,请检查您的邮箱。';
|
| 76 |
+
} else {
|
| 77 |
+
this.verificationCodeMessage = data.detail || data.message || '发送验证码失败。';
|
| 78 |
+
// 如果发送失败,立即停止倒计时并重置按钮
|
| 79 |
+
clearInterval(timer);
|
| 80 |
+
this.isSendingCode = false;
|
| 81 |
+
this.countdown = 60;
|
| 82 |
+
}
|
| 83 |
+
} catch (error) {
|
| 84 |
+
this.verificationCodeMessage = '网络错误或服务器无响应。';
|
| 85 |
+
console.error('Send verification code error:', error);
|
| 86 |
+
// 如果网络错误,立即停止倒计时并重置按钮
|
| 87 |
+
clearInterval(timer);
|
| 88 |
+
this.isSendingCode = false;
|
| 89 |
+
this.countdown = 60;
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
async sendResetVerificationCode() {
|
| 93 |
+
console.log('sendResetVerificationCode method triggered.');
|
| 94 |
+
console.log('Current email for reset:', this.resetPasswordForm.email);
|
| 95 |
+
this.resetPasswordMessage = ''; // Clear previous messages
|
| 96 |
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
| 97 |
+
if (!this.resetPasswordForm.email || !emailRegex.test(this.resetPasswordForm.email)) {
|
| 98 |
+
this.resetPasswordMessage = '请输入有效的邮箱地址。';
|
| 99 |
+
console.log('Invalid email format for reset.');
|
| 100 |
+
return;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
this.isSendingCode = true;
|
| 104 |
+
let timer = setInterval(() => {
|
| 105 |
+
if (this.countdown > 0) {
|
| 106 |
+
this.countdown--;
|
| 107 |
+
} else {
|
| 108 |
+
clearInterval(timer);
|
| 109 |
+
this.isSendingCode = false;
|
| 110 |
+
this.countdown = 60;
|
| 111 |
+
}
|
| 112 |
+
}, 1000);
|
| 113 |
+
|
| 114 |
+
try {
|
| 115 |
+
const response = await fetch('/api/auth/send-reset-password-code', { // New API endpoint for reset password code
|
| 116 |
+
method: 'POST',
|
| 117 |
+
headers: {
|
| 118 |
+
'Content-Type': 'application/json'
|
| 119 |
+
},
|
| 120 |
+
body: JSON.stringify({ email: this.resetPasswordForm.email })
|
| 121 |
+
});
|
| 122 |
+
const data = await response.json();
|
| 123 |
+
if (response.ok) {
|
| 124 |
+
this.resetPasswordMessage = data.message || '重置密码验证码已发送,请检查您的邮箱。';
|
| 125 |
+
} else {
|
| 126 |
+
this.resetPasswordMessage = data.detail || data.message || '发送重置密码验证码失败。';
|
| 127 |
+
clearInterval(timer);
|
| 128 |
+
this.isSendingCode = false;
|
| 129 |
+
this.countdown = 60;
|
| 130 |
+
}
|
| 131 |
+
} catch (error) {
|
| 132 |
+
this.resetPasswordMessage = '网络错误或服务器无响应。';
|
| 133 |
+
console.error('Send reset verification code error:', error);
|
| 134 |
+
clearInterval(timer);
|
| 135 |
+
this.isSendingCode = false;
|
| 136 |
+
this.countdown = 60;
|
| 137 |
+
}
|
| 138 |
+
},
|
| 139 |
+
async signup() {
|
| 140 |
+
this.signupMessage = '';
|
| 141 |
+
this.verificationCodeMessage = ''; // Clear verification code message on signup attempt
|
| 142 |
+
try {
|
| 143 |
+
const response = await fetch('/api/auth/signup', {
|
| 144 |
+
method: 'POST',
|
| 145 |
+
headers: {
|
| 146 |
+
'Content-Type': 'application/json'
|
| 147 |
+
},
|
| 148 |
+
body: JSON.stringify({
|
| 149 |
+
email: this.signupForm.email,
|
| 150 |
+
password: this.signupForm.password,
|
| 151 |
+
verification_code: this.signupForm.verificationCode || '' // Ensure verification_code is always sent
|
| 152 |
+
})
|
| 153 |
+
});
|
| 154 |
+
const data = await response.json();
|
| 155 |
+
if (response.ok) {
|
| 156 |
+
this.signupMessage = '注册成功!请登录。';
|
| 157 |
+
this.signupForm.email = '';
|
| 158 |
+
this.signupForm.password = '';
|
| 159 |
+
this.signupForm.verificationCode = ''; // Clear verification code
|
| 160 |
+
} else {
|
| 161 |
+
this.signupMessage = data.detail || '注册失败。';
|
| 162 |
+
}
|
| 163 |
+
} catch (error) {
|
| 164 |
+
this.signupMessage = '网络错误或服务器无响应。';
|
| 165 |
+
console.error('Signup error:', error);
|
| 166 |
+
}
|
| 167 |
+
},
|
| 168 |
+
async login() {
|
| 169 |
+
this.authMessage = ''; // Clear previous messages
|
| 170 |
+
try {
|
| 171 |
+
const response = await fetch('/api/auth/login', {
|
| 172 |
+
method: 'POST',
|
| 173 |
+
headers: {
|
| 174 |
+
'Content-Type': 'application/json'
|
| 175 |
+
},
|
| 176 |
+
body: JSON.stringify(this.loginForm)
|
| 177 |
+
});
|
| 178 |
+
const data = await response.json();
|
| 179 |
+
if (response.ok) {
|
| 180 |
+
localStorage.setItem('access_token', data.access_token);
|
| 181 |
+
this.isLoggedIn = true;
|
| 182 |
+
this.loginForm.email = '';
|
| 183 |
+
this.loginForm.password = '';
|
| 184 |
+
this.authMessage = '登录成功!'; // Use authMessage
|
| 185 |
+
await this.fetchUserEmail(); // Fetch user email after successful login
|
| 186 |
+
this.fetchProxies();
|
| 187 |
+
if (window.location.pathname === '/login') {
|
| 188 |
+
window.location.href = '/';
|
| 189 |
+
}
|
| 190 |
+
} else {
|
| 191 |
+
this.authMessage = data.detail || '登录失败。'; // Use authMessage
|
| 192 |
+
}
|
| 193 |
+
} catch (error) {
|
| 194 |
+
if (error instanceof TypeError && error.message === 'Failed to fetch') {
|
| 195 |
+
this.authMessage = '网络错误或服务器无响应。'; // Use authMessage
|
| 196 |
+
} else {
|
| 197 |
+
this.authMessage = '邮箱或密码错误。请重新输入!'; // Use authMessage
|
| 198 |
+
}
|
| 199 |
+
console.error('Login error:', error);
|
| 200 |
+
}
|
| 201 |
+
},
|
| 202 |
+
logout() {
|
| 203 |
+
localStorage.removeItem('access_token');
|
| 204 |
+
this.isLoggedIn = false;
|
| 205 |
+
this.userEmail = '';
|
| 206 |
+
this.proxies = [];
|
| 207 |
+
this.authMessage = '已退出登录。'; // Use authMessage
|
| 208 |
+
// 只有在非登录页面才重定向到登录页
|
| 209 |
+
if (window.location.pathname !== '/login') {
|
| 210 |
+
window.location.href = '/login';
|
| 211 |
+
}
|
| 212 |
+
},
|
| 213 |
+
async forgotPassword() {
|
| 214 |
+
const email = this.loginForm.email;
|
| 215 |
+
let redirectUrl = '/reset-password';
|
| 216 |
+
if (email) {
|
| 217 |
+
redirectUrl += `?email=${encodeURIComponent(email)}`;
|
| 218 |
+
}
|
| 219 |
+
window.location.href = redirectUrl;
|
| 220 |
+
},
|
| 221 |
+
async fetchUserEmail() {
|
| 222 |
+
const token = localStorage.getItem('access_token');
|
| 223 |
+
if (!token) {
|
| 224 |
+
this.userEmail = '';
|
| 225 |
+
return;
|
| 226 |
+
}
|
| 227 |
+
try {
|
| 228 |
+
const response = await fetch('/api/user/me', {
|
| 229 |
+
headers: {
|
| 230 |
+
'Authorization': `Bearer ${token}`
|
| 231 |
+
}
|
| 232 |
+
});
|
| 233 |
+
const data = await response.json();
|
| 234 |
+
if (response.ok) {
|
| 235 |
+
this.userEmail = data.email;
|
| 236 |
+
} else {
|
| 237 |
+
console.error('Error fetching user email:', data.detail);
|
| 238 |
+
this.userEmail = '未知用户';
|
| 239 |
+
if (response.status === 401) {
|
| 240 |
+
this.logout();
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
} catch (error) {
|
| 244 |
+
console.error('Error fetching user email:', error);
|
| 245 |
+
this.userEmail = '未知用户';
|
| 246 |
+
}
|
| 247 |
+
},
|
| 248 |
+
checkLoginStatus() {
|
| 249 |
+
const token = localStorage.getItem('access_token');
|
| 250 |
+
if (token) {
|
| 251 |
+
this.isLoggedIn = true;
|
| 252 |
+
this.fetchUserEmail();
|
| 253 |
+
// 只有在登录页面才重定向到首页
|
| 254 |
+
if (window.location.pathname === '/login') {
|
| 255 |
+
window.location.href = '/';
|
| 256 |
+
}
|
| 257 |
+
} else {
|
| 258 |
+
this.isLoggedIn = false;
|
| 259 |
+
this.userEmail = '';
|
| 260 |
+
// 如果在首页,不强制重定向到登录页
|
| 261 |
+
// 如果在需要登录的页面(如 /dashboard 或其他受保护路由),则重定向到登录页
|
| 262 |
+
// 目前只处理 /login 页面,其他页面需要后端或路由守卫处理
|
| 263 |
+
}
|
| 264 |
+
},
|
| 265 |
+
async changePassword() {
|
| 266 |
+
this.changePasswordMessage = '';
|
| 267 |
+
if (this.changePasswordForm.newPassword !== this.changePasswordForm.confirmPassword) {
|
| 268 |
+
this.changePasswordMessage = '两次输入的密码不一致。';
|
| 269 |
+
return;
|
| 270 |
+
}
|
| 271 |
+
const token = localStorage.getItem('access_token');
|
| 272 |
+
if (!token) {
|
| 273 |
+
this.changePasswordMessage = '未登录,请先登录。';
|
| 274 |
+
return;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
try {
|
| 278 |
+
const response = await fetch('/api/auth/change-password', {
|
| 279 |
+
method: 'POST',
|
| 280 |
+
headers: {
|
| 281 |
+
'Content-Type': 'application/json',
|
| 282 |
+
'Authorization': `Bearer ${token}`
|
| 283 |
+
},
|
| 284 |
+
body: JSON.stringify({ new_password: this.changePasswordForm.newPassword })
|
| 285 |
+
});
|
| 286 |
+
const data = await response.json();
|
| 287 |
+
if (response.ok) {
|
| 288 |
+
this.changePasswordMessage = '密码修改成功!请重新登录。';
|
| 289 |
+
this.showChangePasswordModal = false;
|
| 290 |
+
this.changePasswordForm.newPassword = '';
|
| 291 |
+
this.changePasswordForm.confirmPassword = '';
|
| 292 |
+
this.logout();
|
| 293 |
+
} else {
|
| 294 |
+
this.changePasswordMessage = data.detail || '密码修改失败。';
|
| 295 |
+
}
|
| 296 |
+
} catch (error) {
|
| 297 |
+
this.changePasswordMessage = '网络错误或服务器无响应。';
|
| 298 |
+
console.error('Change password error:', error);
|
| 299 |
+
}
|
| 300 |
+
},
|
| 301 |
+
async resetPassword() {
|
| 302 |
+
this.resetPasswordMessage = '';
|
| 303 |
+
if (this.resetPasswordForm.newPassword !== this.resetPasswordForm.confirmPassword) {
|
| 304 |
+
this.resetPasswordMessage = '两次输入的密码不一致。';
|
| 305 |
+
return;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
const urlQueryParams = new URLSearchParams(window.location.search);
|
| 309 |
+
const resetToken = urlQueryParams.get('token');
|
| 310 |
+
|
| 311 |
+
if (!resetToken) {
|
| 312 |
+
this.resetPasswordMessage = '缺少重置密码所需的令牌。';
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// 清除URL中的token,避免泄露
|
| 317 |
+
const newUrl = window.location.pathname + window.location.hash;
|
| 318 |
+
history.replaceState(null, '', newUrl);
|
| 319 |
+
|
| 320 |
+
try {
|
| 321 |
+
const response = await fetch('/api/auth/change-password', {
|
| 322 |
+
method: 'POST',
|
| 323 |
+
headers: {
|
| 324 |
+
'Content-Type': 'application/json'
|
| 325 |
+
},
|
| 326 |
+
body: JSON.stringify({
|
| 327 |
+
new_password: this.resetPasswordForm.newPassword,
|
| 328 |
+
reset_token: resetToken
|
| 329 |
+
})
|
| 330 |
+
});
|
| 331 |
+
const data = await response.json();
|
| 332 |
+
if (response.ok) {
|
| 333 |
+
this.resetPasswordMessage = '密码重置成功!请使用新密码登录。';
|
| 334 |
+
this.resetPasswordForm.newPassword = '';
|
| 335 |
+
this.resetPasswordForm.confirmPassword = '';
|
| 336 |
+
window.location.href = '/login';
|
| 337 |
+
} else {
|
| 338 |
+
this.resetPasswordMessage = data.detail || '密码重置失败。';
|
| 339 |
+
}
|
| 340 |
+
} catch (error) {
|
| 341 |
+
this.resetPasswordMessage = '网络错误或服务器无响应。';
|
| 342 |
+
console.error('Reset password error:', error);
|
| 343 |
+
}
|
| 344 |
+
},
|
| 345 |
+
async resetPasswordWithCode() {
|
| 346 |
+
this.resetPasswordMessage = '';
|
| 347 |
+
if (this.resetPasswordForm.newPassword !== this.resetPasswordForm.confirmPassword) {
|
| 348 |
+
this.resetPasswordMessage = '两次输入的密码不一致。';
|
| 349 |
+
return;
|
| 350 |
+
}
|
| 351 |
+
if (!this.resetPasswordForm.email || !this.resetPasswordForm.verificationCode) {
|
| 352 |
+
this.resetPasswordMessage = '邮箱和验证码不能为空。';
|
| 353 |
+
return;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
try {
|
| 357 |
+
const response = await fetch('/api/auth/reset-password-with-code', { // New API endpoint
|
| 358 |
+
method: 'POST',
|
| 359 |
+
headers: {
|
| 360 |
+
'Content-Type': 'application/json'
|
| 361 |
+
},
|
| 362 |
+
body: JSON.stringify({
|
| 363 |
+
email: this.resetPasswordForm.email,
|
| 364 |
+
verification_code: this.resetPasswordForm.verificationCode,
|
| 365 |
+
new_password: this.resetPasswordForm.newPassword
|
| 366 |
+
})
|
| 367 |
+
});
|
| 368 |
+
const data = await response.json();
|
| 369 |
+
if (response.ok) {
|
| 370 |
+
this.resetPasswordMessage = '密码重置成功!请使用新密码登录。';
|
| 371 |
+
this.resetPasswordForm.email = '';
|
| 372 |
+
this.resetPasswordForm.verificationCode = '';
|
| 373 |
+
this.resetPasswordForm.newPassword = '';
|
| 374 |
+
this.resetPasswordForm.confirmPassword = '';
|
| 375 |
+
window.location.href = '/login';
|
| 376 |
+
} else {
|
| 377 |
+
this.resetPasswordMessage = data.detail || '密码重置失败。';
|
| 378 |
+
}
|
| 379 |
+
} catch (error) {
|
| 380 |
+
this.resetPasswordMessage = '网络错误或服务器无响应。';
|
| 381 |
+
console.error('Reset password with code error:', error);
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
});
|
| 385 |
+
|
| 386 |
+
export const authMounted = (app) => {
|
| 387 |
+
app.checkLoginStatus(); // 在应用挂载时检查登录状态
|
| 388 |
+
// Check if it's the reset-password page and handle token
|
| 389 |
+
if (window.location.pathname === '/reset-password') {
|
| 390 |
+
console.log('Current URL:', window.location.href);
|
| 391 |
+
const urlQueryParams = new URLSearchParams(window.location.search);
|
| 392 |
+
// const resetToken = urlQueryParams.get('token'); // Remove token check
|
| 393 |
+
const emailParam = urlQueryParams.get('email');
|
| 394 |
+
|
| 395 |
+
if (emailParam) {
|
| 396 |
+
app.resetPasswordForm.email = emailParam;
|
| 397 |
+
console.log('Mounted: Email param found and pre-filled:', emailParam);
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// Remove token-related logic
|
| 401 |
+
// if (resetToken) {
|
| 402 |
+
// console.log('Mounted: Valid reset token found. Clearing URL query param.');
|
| 403 |
+
// const newUrl = window.location.pathname + (emailParam ? `?email=${encodeURIComponent(emailParam)}` : '') + window.location.hash;
|
| 404 |
+
// history.replaceState(null, '', newUrl);
|
| 405 |
+
// } else {
|
| 406 |
+
// console.warn('Mounted: No valid reset token found for reset password page.');
|
| 407 |
+
// }
|
| 408 |
+
}
|
| 409 |
+
};
|
static/js/proxy.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/proxy.js
|
| 2 |
+
export const proxyData = () => ({
|
| 3 |
+
proxies: [],
|
| 4 |
+
});
|
| 5 |
+
|
| 6 |
+
export const proxyMethods = () => ({
|
| 7 |
+
async fetchProxies() {
|
| 8 |
+
if (!this.isLoggedIn) { // Assuming isLoggedIn is available in the main app
|
| 9 |
+
this.proxies = [];
|
| 10 |
+
return;
|
| 11 |
+
}
|
| 12 |
+
const token = localStorage.getItem('access_token');
|
| 13 |
+
try {
|
| 14 |
+
const response = await fetch('/api/proxies', {
|
| 15 |
+
headers: {
|
| 16 |
+
'Authorization': `Bearer ${token}`
|
| 17 |
+
}
|
| 18 |
+
});
|
| 19 |
+
const data = await response.json();
|
| 20 |
+
if (response.ok) {
|
| 21 |
+
this.proxies = data;
|
| 22 |
+
console.log('Fetched proxies:', data);
|
| 23 |
+
} else {
|
| 24 |
+
console.error('Error fetching proxies:', data.detail);
|
| 25 |
+
this.proxies = [];
|
| 26 |
+
if (response.status === 401) {
|
| 27 |
+
this.logout(); // Assuming logout is available in the main app
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
} catch (error) {
|
| 31 |
+
console.error('Error fetching proxies:', error);
|
| 32 |
+
this.proxies = [];
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
});
|
static/js/store.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/store.js
|
| 2 |
+
export const appData = () => ({
|
| 3 |
+
message: 'Hello Vue 3!',
|
| 4 |
+
isLoggedIn: false,
|
| 5 |
+
userEmail: '',
|
| 6 |
+
});
|
| 7 |
+
|
| 8 |
+
export const appComputed = {
|
| 9 |
+
reversedMessage() {
|
| 10 |
+
return this.message.split('').reverse().join('');
|
| 11 |
+
}
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export const appMethods = () => ({
|
| 15 |
+
reverseMessage() {
|
| 16 |
+
this.message = this.message.split('').reverse().join('');
|
| 17 |
+
},
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
export const appMounted = (app) => {
|
| 21 |
+
console.log('Vue app mounted!');
|
| 22 |
+
app.checkLoginStatus(); // Assuming checkLoginStatus is available in the main app
|
| 23 |
+
};
|
static/login.html
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>登录</title>
|
| 7 |
+
<!-- Bootstrap 5.3 CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Font Awesome for icons -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app">
|
| 16 |
+
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
| 17 |
+
<div class="container-fluid">
|
| 18 |
+
<a class="navbar-brand d-flex align-items-center" href="/">
|
| 19 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="30" height="30" class="d-inline-block align-text-top me-2">
|
| 20 |
+
<span class="fw-bold fs-5">API Router</span>
|
| 21 |
+
</a>
|
| 22 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 23 |
+
<span class="navbar-toggler-icon"></span>
|
| 24 |
+
</button>
|
| 25 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 26 |
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 27 |
+
<li class="nav-item">
|
| 28 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 29 |
+
<i class="fas fa-comment-dots me-1"></i> 聊天
|
| 30 |
+
</a>
|
| 31 |
+
</li>
|
| 32 |
+
<li class="nav-item">
|
| 33 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 34 |
+
<i class="fas fa-key me-1"></i> 令牌
|
| 35 |
+
</a>
|
| 36 |
+
</li>
|
| 37 |
+
<li class="nav-item">
|
| 38 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 39 |
+
<i class="fas fa-shopping-cart me-1"></i> 充值
|
| 40 |
+
</a>
|
| 41 |
+
</li>
|
| 42 |
+
<li class="nav-item">
|
| 43 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 44 |
+
<i class="fas fa-chart-bar me-1"></i> 总览
|
| 45 |
+
</a>
|
| 46 |
+
</li>
|
| 47 |
+
<li class="nav-item">
|
| 48 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 49 |
+
<i class="fas fa-file-alt me-1"></i> 日志
|
| 50 |
+
</a>
|
| 51 |
+
</li>
|
| 52 |
+
<li class="nav-item">
|
| 53 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 54 |
+
<i class="fas fa-cog me-1"></i> 设置
|
| 55 |
+
</a>
|
| 56 |
+
</li>
|
| 57 |
+
<li class="nav-item">
|
| 58 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 59 |
+
<i class="fas fa-info-circle me-1"></i> 关于
|
| 60 |
+
</a>
|
| 61 |
+
</li>
|
| 62 |
+
</ul>
|
| 63 |
+
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
| 64 |
+
<li class="nav-item dropdown">
|
| 65 |
+
<a class="nav-link dropdown-toggle d-flex align-items-center me-3" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 66 |
+
<i class="fas fa-font me-1"></i> A|文
|
| 67 |
+
</a>
|
| 68 |
+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
| 69 |
+
<li><a class="dropdown-item" href="#">中文</a></li>
|
| 70 |
+
<li><a class="dropdown-item" href="#">English</a></li>
|
| 71 |
+
</ul>
|
| 72 |
+
</li>
|
| 73 |
+
<li class="nav-item">
|
| 74 |
+
<a class="nav-link d-flex align-items-center" href="/login">
|
| 75 |
+
<i class="fas fa-user me-1"></i> 登录
|
| 76 |
+
</a>
|
| 77 |
+
</li>
|
| 78 |
+
</ul>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</nav>
|
| 82 |
+
|
| 83 |
+
<div class="auth-container d-flex justify-content-center align-items-center">
|
| 84 |
+
<div class="login-card card shadow-sm p-4">
|
| 85 |
+
<div class="text-center mb-4 d-flex align-items-center justify-content-center">
|
| 86 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="48" height="48" class="me-2">
|
| 87 |
+
<h3 class="card-title mb-0">用户登录</h3>
|
| 88 |
+
</div>
|
| 89 |
+
<form @submit.prevent="login">
|
| 90 |
+
<div class="mb-3 input-group">
|
| 91 |
+
<span class="input-group-text"><i class="fas fa-user"></i></span>
|
| 92 |
+
<input type="email" class="form-control" id="loginEmail" v-model="loginForm.email" placeholder="用户名 / 邮箱地址" required autocomplete="username">
|
| 93 |
+
</div>
|
| 94 |
+
<div class="mb-3 input-group">
|
| 95 |
+
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
| 96 |
+
<input type="password" class="form-control" id="loginPassword" v-model="loginForm.password" placeholder="密码" required autocomplete="current-password">
|
| 97 |
+
</div>
|
| 98 |
+
<button type="submit" class="btn btn-primary w-100 mb-3">登录</button>
|
| 99 |
+
<div class="d-flex justify-content-between mb-3">
|
| 100 |
+
<div class="text-decoration-none text-muted">忘记密码?<a href="/reset-password"><span class="text-primary">点击重置</span></a></div>
|
| 101 |
+
<div class="text-decoration-none text-muted">没有账号?<a href="/signup"><span class="text-primary">点击注册</span></a></div>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="divider my-4">
|
| 104 |
+
<span class="divider-text">使用其他方式登录</span>
|
| 105 |
+
</div>
|
| 106 |
+
<div class="d-flex justify-content-center social-login-icons">
|
| 107 |
+
<a href="#" class="btn btn-outline-secondary rounded-circle mx-2 github-icon"><i class="fab fa-github fa-lg"></i></a>
|
| 108 |
+
<a href="#" class="btn btn-outline-secondary rounded-circle mx-2 wechat-icon"><i class="fab fa-weixin fa-lg"></i></a>
|
| 109 |
+
</div>
|
| 110 |
+
</form>
|
| 111 |
+
<p v-if="authMessage" class="mt-3 text-center text-info">
|
| 112 |
+
{{ authMessage }}
|
| 113 |
+
</p>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<footer class="footer text-center py-3 mt-auto">
|
| 118 |
+
<p class="mb-0 text-muted">API Router v0.6.11-preview.6 由 JustSong 构建,源代码遵循 <a href="#" class="text-decoration-none">MIT 协议</a></p>
|
| 119 |
+
</footer>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<!-- Vue 3 CDN -->
|
| 123 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 124 |
+
<!-- Bootstrap 5.3 JS CDN -->
|
| 125 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 126 |
+
<!-- Custom Vue.js App Logic -->
|
| 127 |
+
<script type="module" src="/static/app.js"></script>
|
| 128 |
+
</body>
|
| 129 |
+
</html>
|
static/reset-password.html
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>SP Website - 重置密码</title>
|
| 7 |
+
<!-- Bootstrap 5.3 CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Font Awesome CDN for icons -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app">
|
| 16 |
+
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
| 17 |
+
<div class="container-fluid">
|
| 18 |
+
<a class="navbar-brand d-flex align-items-center" href="/">
|
| 19 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="30" height="30" class="d-inline-block align-text-top me-2">
|
| 20 |
+
<span class="fw-bold fs-5">API Router</span>
|
| 21 |
+
</a>
|
| 22 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 23 |
+
<span class="navbar-toggler-icon"></span>
|
| 24 |
+
</button>
|
| 25 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 26 |
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 27 |
+
<li class="nav-item">
|
| 28 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 29 |
+
<i class="fas fa-comment-dots me-1"></i> 聊天
|
| 30 |
+
</a>
|
| 31 |
+
</li>
|
| 32 |
+
<li class="nav-item">
|
| 33 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 34 |
+
<i class="fas fa-key me-1"></i> 令牌
|
| 35 |
+
</a>
|
| 36 |
+
</li>
|
| 37 |
+
<li class="nav-item">
|
| 38 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 39 |
+
<i class="fas fa-shopping-cart me-1"></i> 充值
|
| 40 |
+
</a>
|
| 41 |
+
</li>
|
| 42 |
+
<li class="nav-item">
|
| 43 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 44 |
+
<i class="fas fa-chart-bar me-1"></i> 总览
|
| 45 |
+
</a>
|
| 46 |
+
</li>
|
| 47 |
+
<li class="nav-item">
|
| 48 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 49 |
+
<i class="fas fa-file-alt me-1"></i> 日志
|
| 50 |
+
</a>
|
| 51 |
+
</li>
|
| 52 |
+
<li class="nav-item">
|
| 53 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 54 |
+
<i class="fas fa-cog me-1"></i> 设置
|
| 55 |
+
</a>
|
| 56 |
+
</li>
|
| 57 |
+
<li class="nav-item">
|
| 58 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 59 |
+
<i class="fas fa-info-circle me-1"></i> 关于
|
| 60 |
+
</a>
|
| 61 |
+
</li>
|
| 62 |
+
</ul>
|
| 63 |
+
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
| 64 |
+
<li class="nav-item dropdown">
|
| 65 |
+
<a class="nav-link dropdown-toggle d-flex align-items-center me-3" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 66 |
+
<i class="fas fa-font me-1"></i> A|文
|
| 67 |
+
</a>
|
| 68 |
+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
| 69 |
+
<li><a class="dropdown-item" href="#">中文</a></li>
|
| 70 |
+
<li><a class="dropdown-item" href="#">English</a></li>
|
| 71 |
+
</ul>
|
| 72 |
+
</li>
|
| 73 |
+
<li class="nav-item">
|
| 74 |
+
<a class="nav-link d-flex align-items-center" href="/login">
|
| 75 |
+
<i class="fas fa-user me-1"></i> 登录
|
| 76 |
+
</a>
|
| 77 |
+
</li>
|
| 78 |
+
</ul>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</nav>
|
| 82 |
+
|
| 83 |
+
<div class="auth-container d-flex justify-content-center align-items-center flex-grow-1">
|
| 84 |
+
<div class="login-card p-4">
|
| 85 |
+
<div class="text-center mb-4 d-flex align-items-center justify-content-center">
|
| 86 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="50" height="50" class="me-2">
|
| 87 |
+
<h3 class="card-title mb-0">重置密码</h3>
|
| 88 |
+
</div>
|
| 89 |
+
<form @submit.prevent="resetPasswordWithCode">
|
| 90 |
+
<div class="mb-3 input-group">
|
| 91 |
+
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
| 92 |
+
<input type="email" class="form-control" id="resetEmail" v-model="resetPasswordForm.email" placeholder="输入邮箱地址" required autocomplete="username">
|
| 93 |
+
<button type="button" class="btn btn-outline-secondary" @click="sendResetVerificationCode" :disabled="isSendingCode">{{ isSendingCode ? `${countdown}s` : '获取验证码' }}</button>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="mb-3 input-group">
|
| 96 |
+
<span class="input-group-text"><i class="fas fa-shield-alt"></i></span>
|
| 97 |
+
<input type="text" class="form-control" id="resetVerificationCode" v-model="resetPasswordForm.verificationCode" placeholder="验证码" required autocomplete="one-time-code">
|
| 98 |
+
</div>
|
| 99 |
+
<div class="mb-3 input-group">
|
| 100 |
+
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
| 101 |
+
<input type="password" class="form-control" id="resetNewPassword" v-model="resetPasswordForm.newPassword" placeholder="新密码" required autocomplete="new-password">
|
| 102 |
+
</div>
|
| 103 |
+
<div class="mb-3 input-group">
|
| 104 |
+
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
| 105 |
+
<input type="password" class="form-control" id="resetConfirmPassword" v-model="resetPasswordForm.confirmPassword" placeholder="确认新密码" required autocomplete="new-password">
|
| 106 |
+
</div>
|
| 107 |
+
<button type="submit" class="btn btn-primary w-100 mb-3">重置密码</button>
|
| 108 |
+
</form>
|
| 109 |
+
<p v-if="resetPasswordMessage" class="mt-2 text-info text-center">{{ resetPasswordMessage }}</p>
|
| 110 |
+
<p class="text-center text-muted">
|
| 111 |
+
已有账号? <a href="/login" class="text-decoration-none">立即登录</a>
|
| 112 |
+
</p>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<footer class="footer text-center">
|
| 117 |
+
<div class="container">
|
| 118 |
+
<p class="text-muted mb-0">© 2024 API Router. All rights reserved.</p>
|
| 119 |
+
</div>
|
| 120 |
+
</footer>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- Vue 3 CDN -->
|
| 124 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 125 |
+
<!-- Bootstrap 5.3 JS CDN -->
|
| 126 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 127 |
+
<!-- Custom Vue.js App Logic -->
|
| 128 |
+
<script type="module" src="/static/app.js"></script>
|
| 129 |
+
</body>
|
| 130 |
+
</html>
|
static/signup.html
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>SP Website - 注册</title>
|
| 7 |
+
<!-- Bootstrap 5.3 CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Font Awesome CDN for icons -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app">
|
| 16 |
+
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
| 17 |
+
<div class="container-fluid">
|
| 18 |
+
<a class="navbar-brand d-flex align-items-center" href="/">
|
| 19 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="30" height="30" class="d-inline-block align-text-top me-2">
|
| 20 |
+
<span class="fw-bold fs-5">API Router</span>
|
| 21 |
+
</a>
|
| 22 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 23 |
+
<span class="navbar-toggler-icon"></span>
|
| 24 |
+
</button>
|
| 25 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 26 |
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 27 |
+
<li class="nav-item">
|
| 28 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 29 |
+
<i class="fas fa-comment-dots me-1"></i> 聊天
|
| 30 |
+
</a>
|
| 31 |
+
</li>
|
| 32 |
+
<li class="nav-item">
|
| 33 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 34 |
+
<i class="fas fa-key me-1"></i> 令牌
|
| 35 |
+
</a>
|
| 36 |
+
</li>
|
| 37 |
+
<li class="nav-item">
|
| 38 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 39 |
+
<i class="fas fa-shopping-cart me-1"></i> 充值
|
| 40 |
+
</a>
|
| 41 |
+
</li>
|
| 42 |
+
<li class="nav-item">
|
| 43 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 44 |
+
<i class="fas fa-chart-bar me-1"></i> 总览
|
| 45 |
+
</a>
|
| 46 |
+
</li>
|
| 47 |
+
<li class="nav-item">
|
| 48 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 49 |
+
<i class="fas fa-file-alt me-1"></i> 日志
|
| 50 |
+
</a>
|
| 51 |
+
</li>
|
| 52 |
+
<li class="nav-item">
|
| 53 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 54 |
+
<i class="fas fa-cog me-1"></i> 设置
|
| 55 |
+
</a>
|
| 56 |
+
</li>
|
| 57 |
+
<li class="nav-item">
|
| 58 |
+
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 59 |
+
<i class="fas fa-info-circle me-1"></i> 关于
|
| 60 |
+
</a>
|
| 61 |
+
</li>
|
| 62 |
+
</ul>
|
| 63 |
+
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
| 64 |
+
<li class="nav-item dropdown">
|
| 65 |
+
<a class="nav-link dropdown-toggle d-flex align-items-center me-3" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 66 |
+
<i class="fas fa-font me-1"></i> A|文
|
| 67 |
+
</a>
|
| 68 |
+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
| 69 |
+
<li><a class="dropdown-item" href="#">中文</a></li>
|
| 70 |
+
<li><a class="dropdown-item" href="#">English</a></li>
|
| 71 |
+
</ul>
|
| 72 |
+
</li>
|
| 73 |
+
<li class="nav-item">
|
| 74 |
+
<a class="nav-link d-flex align-items-center" href="/login">
|
| 75 |
+
<i class="fas fa-user me-1"></i> 登录
|
| 76 |
+
</a>
|
| 77 |
+
</li>
|
| 78 |
+
</ul>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</nav>
|
| 82 |
+
|
| 83 |
+
<div class="auth-container d-flex justify-content-center align-items-center flex-grow-1">
|
| 84 |
+
<div class="login-card p-4">
|
| 85 |
+
<div class="text-center mb-4 d-flex align-items-center justify-content-center">
|
| 86 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="50" height="50" class="me-2">
|
| 87 |
+
<h3 class="card-title mb-0">新用户注册</h3>
|
| 88 |
+
</div>
|
| 89 |
+
<form @submit.prevent="signup">
|
| 90 |
+
<div class="mb-3 input-group">
|
| 91 |
+
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
| 92 |
+
<input type="email" class="form-control" id="signupEmail" v-model="signupForm.email" placeholder="输入邮箱地址" required>
|
| 93 |
+
<button type="button" class="btn btn-outline-secondary" @click="sendVerificationCode" :disabled="isSendingCode">{{ isSendingCode ? `${countdown}s` : '获取验证码' }}</button>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="mb-3 input-group">
|
| 96 |
+
<span class="input-group-text"><i class="fas fa-shield-alt"></i></span>
|
| 97 |
+
<input type="text" class="form-control" id="verificationCode" v-model="signupForm.verificationCode" placeholder="验证码" required>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="mb-3 input-group">
|
| 100 |
+
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
| 101 |
+
<input type="password" class="form-control" id="signupPassword" v-model="signupForm.password" placeholder="密码" required>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="mb-3 input-group">
|
| 104 |
+
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
| 105 |
+
<input type="password" class="form-control" id="confirmPassword" v-model="signupForm.confirmPassword" placeholder="确认密码" required>
|
| 106 |
+
</div>
|
| 107 |
+
<button type="submit" class="btn btn-primary w-100 mb-3">注册</button>
|
| 108 |
+
</form>
|
| 109 |
+
<p v-if="signupMessage" class="mt-2 text-info text-center">{{ signupMessage }}</p>
|
| 110 |
+
<p class="text-center text-muted">
|
| 111 |
+
已有账号? <a href="/login" class="text-decoration-none">立即登录</a>
|
| 112 |
+
</p>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<footer class="footer text-center">
|
| 117 |
+
<div class="container">
|
| 118 |
+
<p class="text-muted mb-0">© 2024 API Router. All rights reserved.</p>
|
| 119 |
+
</div>
|
| 120 |
+
</footer>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- Vue 3 CDN -->
|
| 124 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 125 |
+
<!-- Bootstrap 5.3 JS CDN -->
|
| 126 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 127 |
+
<!-- Custom Vue.js App Logic -->
|
| 128 |
+
<script type="module" src="/static/app.js"></script>
|
| 129 |
+
</body>
|
| 130 |
+
</html>
|
static/style.css
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin);
|
| 2 |
+
html, body, #app {
|
| 3 |
+
height: 100%;
|
| 4 |
+
margin: 0;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
body {
|
| 8 |
+
-webkit-font-smoothing: antialiased;
|
| 9 |
+
-moz-osx-smoothing: grayscale;
|
| 10 |
+
font-family: 'PingFang SC', 'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
|
| 11 |
+
overflow-y: scroll;
|
| 12 |
+
padding-top: 56px; /* Adjust based on navbar height */
|
| 13 |
+
scrollbar-width: none;
|
| 14 |
+
background-color: #f8f9fa; /* Light gray background */
|
| 15 |
+
display: flex;
|
| 16 |
+
flex-direction: column;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
body::-webkit-scrollbar {
|
| 20 |
+
display: none;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.main-content {
|
| 24 |
+
padding: 4px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.small-icon .icon {
|
| 28 |
+
font-size: 1em !important;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.custom-footer {
|
| 32 |
+
font-size: 1.1em;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
@media only screen and (max-width: 600px) {
|
| 36 |
+
.hide-on-mobile {
|
| 37 |
+
display: none !important;
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
@media screen and (max-width: 768px) {
|
| 42 |
+
.ui.container {
|
| 43 |
+
padding: 0 10px !important;
|
| 44 |
+
width: 100% !important;
|
| 45 |
+
}
|
| 46 |
+
.ui.card,
|
| 47 |
+
.ui.cards,
|
| 48 |
+
.ui.container,
|
| 49 |
+
.ui.segment {
|
| 50 |
+
margin-left: 0 !important;
|
| 51 |
+
margin-right: 0 !important;
|
| 52 |
+
}
|
| 53 |
+
.ui.table {
|
| 54 |
+
padding-left: 0 !important;
|
| 55 |
+
padding-right: 0 !important;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
@media screen and (min-width: 769px) and (max-width: 1366px) {
|
| 60 |
+
.ui.container {
|
| 61 |
+
margin-left: auto !important;
|
| 62 |
+
margin-right: auto !important;
|
| 63 |
+
max-width: 100% !important;
|
| 64 |
+
padding: 0 24px !important;
|
| 65 |
+
width: auto !important;
|
| 66 |
+
}
|
| 67 |
+
.ui.table {
|
| 68 |
+
font-size: .9em;
|
| 69 |
+
}
|
| 70 |
+
.ui.cards {
|
| 71 |
+
margin-left: -.5em !important;
|
| 72 |
+
margin-right: -.5em !important;
|
| 73 |
+
}
|
| 74 |
+
.ui.cards>.card {
|
| 75 |
+
margin: .5em !important;
|
| 76 |
+
width: calc(50% - 1em) !important;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
@media screen and (min-width: 1367px) {
|
| 81 |
+
.ui.container {
|
| 82 |
+
margin-left: auto !important;
|
| 83 |
+
margin-right: auto !important;
|
| 84 |
+
padding: 0 !important;
|
| 85 |
+
width: 1200px !important;
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
@media screen and (max-width: 1366px) {
|
| 90 |
+
.charts-grid {
|
| 91 |
+
margin: 0 -.5em !important;
|
| 92 |
+
}
|
| 93 |
+
.charts-grid .column {
|
| 94 |
+
padding: .5em !important;
|
| 95 |
+
}
|
| 96 |
+
.chart-card {
|
| 97 |
+
margin: 0 !important;
|
| 98 |
+
}
|
| 99 |
+
.ui.header {
|
| 100 |
+
font-size: 1.1em !important;
|
| 101 |
+
}
|
| 102 |
+
.stat-value {
|
| 103 |
+
font-size: .9em !important;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Custom styles for SP Website */
|
| 108 |
+
.user-avatar {
|
| 109 |
+
width: 32px;
|
| 110 |
+
height: 32px;
|
| 111 |
+
object-fit: cover;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* Login page specific styles */
|
| 115 |
+
.auth-container {
|
| 116 |
+
min-height: calc(100vh - 56px - 60px); /* Full height minus navbar and footer */
|
| 117 |
+
padding-top: 50px;
|
| 118 |
+
padding-bottom: 50px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.login-card {
|
| 122 |
+
max-width: 400px;
|
| 123 |
+
border: none;
|
| 124 |
+
border-radius: 10px;
|
| 125 |
+
background-color: #fff;
|
| 126 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.login-card .card-title {
|
| 130 |
+
font-weight: bold;
|
| 131 |
+
color: #333;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.input-group-text {
|
| 135 |
+
background-color: #e9ecef;
|
| 136 |
+
border-right: none;
|
| 137 |
+
border-color: #ced4da;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.form-control {
|
| 141 |
+
border-left: none;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.form-control:focus {
|
| 145 |
+
box-shadow: none;
|
| 146 |
+
border-color: #80bdff;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.btn-primary {
|
| 150 |
+
background-color: #007bff;
|
| 151 |
+
border-color: #007bff;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.btn-primary:hover {
|
| 155 |
+
background-color: #0056b3;
|
| 156 |
+
border-color: #0056b3;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.divider {
|
| 160 |
+
position: relative;
|
| 161 |
+
text-align: center;
|
| 162 |
+
margin-top: 1.5rem;
|
| 163 |
+
margin-bottom: 1.5rem;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.divider::before {
|
| 167 |
+
content: '';
|
| 168 |
+
position: absolute;
|
| 169 |
+
top: 50%;
|
| 170 |
+
left: 0;
|
| 171 |
+
right: 0;
|
| 172 |
+
border-top: 1px solid #e0e0e0;
|
| 173 |
+
z-index: 1;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.divider-text {
|
| 177 |
+
background-color: #fff;
|
| 178 |
+
padding: 0 10px;
|
| 179 |
+
position: relative;
|
| 180 |
+
z-index: 2;
|
| 181 |
+
color: #6c757d;
|
| 182 |
+
font-size: 0.9rem;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.social-login-icons .btn {
|
| 186 |
+
width: 40px;
|
| 187 |
+
height: 40px;
|
| 188 |
+
display: flex;
|
| 189 |
+
justify-content: center;
|
| 190 |
+
align-items: center;
|
| 191 |
+
font-size: 1.2rem;
|
| 192 |
+
color: #6c757d;
|
| 193 |
+
border-color: #ced4da;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.social-login-icons .btn:hover {
|
| 197 |
+
color: #007bff;
|
| 198 |
+
border-color: #007bff;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.navbar {
|
| 202 |
+
background-color: #ffffff !important;
|
| 203 |
+
border-bottom: none; /* Remove border-bottom as per image */
|
| 204 |
+
box-shadow: 0 0px 2px 12px rgba(0, 0, 0, 0.04); /* Adjusted box-shadow as per image */
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.navbar-brand {
|
| 208 |
+
color: #333 !important;
|
| 209 |
+
font-weight: 600;
|
| 210 |
+
padding-left: 20px; /* Add left padding for brand */
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.navbar-brand .fw-bold {
|
| 214 |
+
color: #333 !important;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.navbar-nav .nav-item .nav-link {
|
| 218 |
+
color: #666 !important; /* Adjust nav-link color */
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
padding: 0.5rem 1rem; /* Adjust padding */
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.navbar-nav .nav-item .nav-link:hover {
|
| 224 |
+
color: #007bff !important; /* Hover effect */
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.container-fluid {
|
| 228 |
+
padding-left: 20px; /* Adjust container padding for left/right */
|
| 229 |
+
padding-right: 20px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* Login page specific styles adjustments */
|
| 233 |
+
.auth-container {
|
| 234 |
+
min-height: calc(100vh - 56px - 60px); /* Full height minus navbar and footer */
|
| 235 |
+
padding-top: 50px;
|
| 236 |
+
padding-bottom: 50px;
|
| 237 |
+
background-color: #f8f9fa; /* Match body background */
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.login-card {
|
| 241 |
+
max-width: 550px; /* Increased width by 100px from previous 450px */
|
| 242 |
+
border: none;
|
| 243 |
+
border-radius: 10px;
|
| 244 |
+
background-color: #fff;
|
| 245 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
| 246 |
+
padding: 2.5rem !important; /* Increased padding */
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.login-card .card-title {
|
| 250 |
+
font-weight: bold;
|
| 251 |
+
color: #333;
|
| 252 |
+
font-size: 1.8rem; /* Larger title */
|
| 253 |
+
/* margin-bottom: 2rem !important; */ /* Removed as it's now horizontal with logo */
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.input-group-text {
|
| 257 |
+
background-color: #f8f9fa; /* Light gray background for input icon */
|
| 258 |
+
border-right: none;
|
| 259 |
+
border-color: #ced4da;
|
| 260 |
+
color: #6c757d; /* Icon color */
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.form-control {
|
| 264 |
+
border-left: none;
|
| 265 |
+
border-color: #ced4da;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.form-control:focus {
|
| 269 |
+
box-shadow: none;
|
| 270 |
+
border-color: #007bff; /* Focus border color */
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.btn-primary {
|
| 274 |
+
background-color: #007bff;
|
| 275 |
+
border-color: #007bff;
|
| 276 |
+
font-size: 1.1rem;
|
| 277 |
+
padding: 0.75rem 1.5rem;
|
| 278 |
+
border-radius: 0.3rem;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.btn-primary:hover {
|
| 282 |
+
background-color: #0056b3;
|
| 283 |
+
border-color: #0056b3;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.text-muted {
|
| 287 |
+
color: #6c757d !important; /* Ensure muted text color */
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.divider {
|
| 291 |
+
position: relative;
|
| 292 |
+
text-align: center;
|
| 293 |
+
margin-top: 2rem !important;
|
| 294 |
+
margin-bottom: 2rem !important;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.divider::before {
|
| 298 |
+
content: '';
|
| 299 |
+
position: absolute;
|
| 300 |
+
top: 50%;
|
| 301 |
+
left: 0;
|
| 302 |
+
right: 0;
|
| 303 |
+
border-top: 1px solid #e0e0e0;
|
| 304 |
+
z-index: 1;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.divider-text {
|
| 308 |
+
background-color: #fff;
|
| 309 |
+
padding: 0 10px;
|
| 310 |
+
position: relative;
|
| 311 |
+
z-index: 2;
|
| 312 |
+
color: #6c757d;
|
| 313 |
+
font-size: 0.9rem;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.social-login-icons .btn {
|
| 317 |
+
width: 45px; /* Slightly larger icons */
|
| 318 |
+
height: 45px;
|
| 319 |
+
display: flex;
|
| 320 |
+
justify-content: center;
|
| 321 |
+
align-items: center;
|
| 322 |
+
font-size: 1.4rem; /* Larger icon size */
|
| 323 |
+
/* Remove border and border-radius */
|
| 324 |
+
border: none;
|
| 325 |
+
background-color: transparent;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.social-login-icons .btn:hover {
|
| 329 |
+
background-color: rgba(0, 0, 0, 0.05); /* Subtle hover effect */
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.social-login-icons .github-icon {
|
| 333 |
+
color: #24292e; /* GitHub brand color */
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.social-login-icons .wechat-icon {
|
| 337 |
+
color: #07c160; /* WeChat brand color */
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.footer {
|
| 341 |
+
background-color: #f8f9fa;
|
| 342 |
+
border-top: 1px solid #e9ecef;
|
| 343 |
+
color: #6c757d;
|
| 344 |
+
font-size: 0.85rem;
|
| 345 |
+
padding: 15px 0;
|
| 346 |
+
width: 100%;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.footer .text-muted {
|
| 350 |
+
color: #6c757d !important;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.hero-section {
|
| 354 |
+
flex-grow: 1;
|
| 355 |
+
background-color: #ffffff;
|
| 356 |
+
padding: 80px 20px; /* Increased padding for top/bottom */
|
| 357 |
+
margin-top: -56px; /* Compensate for fixed navbar */
|
| 358 |
+
display: flex;
|
| 359 |
+
align-items: center;
|
| 360 |
+
justify-content: center;
|
| 361 |
+
min-height: calc(100vh - 56px - 60px); /* Full height minus navbar and footer */
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.hero-title {
|
| 365 |
+
font-size: 3.5rem; /* Larger font size */
|
| 366 |
+
color: #333;
|
| 367 |
+
line-height: 1.2;
|
| 368 |
+
margin-bottom: 1.5rem !important;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.hero-description {
|
| 372 |
+
font-size: 1.3rem; /* Slightly larger lead text */
|
| 373 |
+
color: #666;
|
| 374 |
+
line-height: 1.6;
|
| 375 |
+
max-width: 700px;
|
| 376 |
+
margin-left: auto;
|
| 377 |
+
margin-right: auto;
|
| 378 |
+
margin-bottom: 2.5rem !important;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.btn-primary {
|
| 382 |
+
background-color: #007bff;
|
| 383 |
+
border-color: #007bff;
|
| 384 |
+
font-size: 1.1rem;
|
| 385 |
+
padding: 0.75rem 1.5rem;
|
| 386 |
+
border-radius: 0.3rem;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.btn-primary:hover {
|
| 390 |
+
background-color: #0056b3;
|
| 391 |
+
border-color: #0056b3;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.btn-outline-secondary {
|
| 395 |
+
color: #6c757d;
|
| 396 |
+
background-color: #e9ecef; /* Light gray background */
|
| 397 |
+
border-color: #e9ecef; /* Light gray border */
|
| 398 |
+
font-size: 1.1rem;
|
| 399 |
+
padding: 0.75rem 1.5rem;
|
| 400 |
+
border-radius: 0.3rem;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.btn-outline-secondary:hover {
|
| 404 |
+
color: #fff;
|
| 405 |
+
background-color: #6c757d;
|
| 406 |
+
border-color: #6c757d;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.btn-outline-secondary:disabled {
|
| 410 |
+
color: #6c757d;
|
| 411 |
+
background-color: #e9ecef;
|
| 412 |
+
border-color: #e9ecef;
|
| 413 |
+
opacity: 0.65;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.footer {
|
| 417 |
+
background-color: #f8f9fa;
|
| 418 |
+
border-top: 1px solid #e9ecef;
|
| 419 |
+
color: #6c757d;
|
| 420 |
+
font-size: 0.85rem;
|
| 421 |
+
padding: 15px 0;
|
| 422 |
+
width: 100%;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.footer .text-muted {
|
| 426 |
+
color: #6c757d !important;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
/* Remove dashboard specific styles as they are no longer needed for the unauthenticated homepage */
|
| 430 |
+
|
| 431 |
+
/* User Dropdown specific styles */
|
| 432 |
+
.navbar-nav .nav-item.dropdown .nav-link {
|
| 433 |
+
padding-right: 1rem !important; /* Ensure space for dropdown arrow */
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.navbar-nav .nav-item.dropdown .dropdown-menu {
|
| 437 |
+
right: 0;
|
| 438 |
+
left: auto;
|
| 439 |
+
min-width: 10rem;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
@media (max-width: 991.98px) {
|
| 443 |
+
.navbar-nav .nav-item.dropdown .dropdown-menu {
|
| 444 |
+
position: static;
|
| 445 |
+
float: none;
|
| 446 |
+
width: 100%;
|
| 447 |
+
margin-top: 0;
|
| 448 |
+
background-color: transparent;
|
| 449 |
+
border: none;
|
| 450 |
+
box-shadow: none;
|
| 451 |
+
}
|
| 452 |
+
.navbar-nav .nav-item.dropdown .dropdown-menu .dropdown-item {
|
| 453 |
+
padding-left: calc(1rem + 1.5rem); /* Indent dropdown items */
|
| 454 |
+
color: #666 !important;
|
| 455 |
+
}
|
| 456 |
+
.navbar-nav .nav-item.dropdown .dropdown-menu .dropdown-item:hover {
|
| 457 |
+
color: #007bff !important;
|
| 458 |
+
background-color: transparent;
|
| 459 |
+
}
|
| 460 |
+
.navbar-nav .nav-item.dropdown .dropdown-menu .dropdown-divider {
|
| 461 |
+
display: none; /* Hide divider in mobile view */
|
| 462 |
+
}
|
| 463 |
+
}
|
test_auth_proxy.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import httpx
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
# Load environment variables from .env file
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
# Base URL for the FastAPI application
|
| 10 |
+
BASE_URL = "http://localhost:7864"
|
| 11 |
+
|
| 12 |
+
# Supabase credentials from .env
|
| 13 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 14 |
+
SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
|
| 15 |
+
SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
|
| 16 |
+
|
| 17 |
+
# Test user credentials
|
| 18 |
+
TEST_EMAIL = "test_user@example.com"
|
| 19 |
+
TEST_PASSWORD = "test_password"
|
| 20 |
+
|
| 21 |
+
# Ensure Supabase environment variables are set
|
| 22 |
+
if not all([SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY]):
|
| 23 |
+
pytest.skip("Supabase environment variables not set. Skipping tests.", allow_module_level=True)
|
| 24 |
+
|
| 25 |
+
@pytest.fixture(scope="module")
|
| 26 |
+
def client():
|
| 27 |
+
"""Provides a test client for the FastAPI application."""
|
| 28 |
+
# In a real scenario, you might run the app in a separate process
|
| 29 |
+
# or use TestClient from fastapi.testclient.
|
| 30 |
+
# For this automated test, we assume the app is running at BASE_URL.
|
| 31 |
+
with httpx.Client(base_url=BASE_URL) as client:
|
| 32 |
+
yield client
|
| 33 |
+
|
| 34 |
+
@pytest.fixture(scope="module")
|
| 35 |
+
def auth_headers(client):
|
| 36 |
+
"""Registers and logs in a test user, returning auth headers."""
|
| 37 |
+
# 1. Register user
|
| 38 |
+
signup_data = {"email": TEST_EMAIL, "password": TEST_PASSWORD}
|
| 39 |
+
response = client.post("/api/auth/signup", json=signup_data)
|
| 40 |
+
if response.status_code == 400 and "User already registered" in response.text:
|
| 41 |
+
print(f"User {TEST_EMAIL} already registered. Proceeding with login.")
|
| 42 |
+
elif response.status_code != 200:
|
| 43 |
+
response.raise_for_status() # Raise for other signup errors
|
| 44 |
+
|
| 45 |
+
# 2. Login user
|
| 46 |
+
login_data = {"email": TEST_EMAIL, "password": TEST_PASSWORD}
|
| 47 |
+
response = client.post("/api/auth/login", json=login_data)
|
| 48 |
+
response.raise_for_status()
|
| 49 |
+
token = response.json()["access_token"]
|
| 50 |
+
return {"Authorization": f"Bearer {token}"}
|
| 51 |
+
|
| 52 |
+
def test_signup_and_login(client):
|
| 53 |
+
"""Tests user registration and login."""
|
| 54 |
+
# Ensure a unique email for signup test if running independently
|
| 55 |
+
unique_email = f"test_user_{os.urandom(4).hex()}@example.com"
|
| 56 |
+
signup_data = {"email": unique_email, "password": TEST_PASSWORD}
|
| 57 |
+
response = client.post("/api/auth/signup", json=signup_data)
|
| 58 |
+
assert response.status_code == 200 or (response.status_code == 400 and "User already registered" in response.text)
|
| 59 |
+
|
| 60 |
+
login_data = {"email": unique_email, "password": TEST_PASSWORD}
|
| 61 |
+
response = client.post("/api/auth/login", json=login_data)
|
| 62 |
+
response.raise_for_status()
|
| 63 |
+
assert "access_token" in response.json()
|
| 64 |
+
|
| 65 |
+
def test_generate_api_key(client, auth_headers):
|
| 66 |
+
"""Tests API key generation."""
|
| 67 |
+
response = client.post("/api/user/generate-api-key", headers=auth_headers)
|
| 68 |
+
response.raise_for_status()
|
| 69 |
+
assert "api_key" in response.json()
|
| 70 |
+
|
| 71 |
+
def test_get_api_keys(client, auth_headers):
|
| 72 |
+
"""Tests fetching API keys."""
|
| 73 |
+
response = client.get("/api/user/api-keys", headers=auth_headers)
|
| 74 |
+
response.raise_for_status()
|
| 75 |
+
assert isinstance(response.json(), list)
|
| 76 |
+
|
| 77 |
+
def test_get_proxy_data(client, auth_headers):
|
| 78 |
+
"""Tests retrieval of proxy data."""
|
| 79 |
+
response = client.get("/api/proxies", headers=auth_headers)
|
| 80 |
+
response.raise_for_status()
|
| 81 |
+
assert isinstance(response.json(), list)
|
| 82 |
+
# Optionally, assert on the structure of the returned proxy data
|
| 83 |
+
# For example:
|
| 84 |
+
# if response.json():
|
| 85 |
+
# assert "ip_address" in response.json()[0]
|
| 86 |
+
# assert "port" in response.json()[0]
|
test_email.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import ssl
|
| 3 |
+
import smtplib
|
| 4 |
+
from email.mime.text import MIMEText
|
| 5 |
+
from email.header import Header
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
# Load environment variables from .env file
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
# Email Configuration
|
| 12 |
+
SMTP_SERVER = os.environ.get("SMTP_SERVER")
|
| 13 |
+
SMTP_PORT = int(os.environ.get("SMTP_PORT", 465)) # Default to 465 for SSL
|
| 14 |
+
SMTP_USERNAME = os.environ.get("SMTP_USERNAME")
|
| 15 |
+
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD")
|
| 16 |
+
SENDER_EMAIL = os.environ.get("SENDER_EMAIL")
|
| 17 |
+
SENDER_NAME = os.environ.get("SENDER_NAME", "Test Sender")
|
| 18 |
+
|
| 19 |
+
def test_send_email(to_email: str, subject: str, body: str):
|
| 20 |
+
print("--- 开始邮件发送测试流程 ---")
|
| 21 |
+
print(f"加载环境变量: SMTP_SERVER={SMTP_SERVER}, SMTP_PORT={SMTP_PORT}, SENDER_EMAIL={SENDER_EMAIL}")
|
| 22 |
+
|
| 23 |
+
if not all([SMTP_SERVER, SMTP_USERNAME, SMTP_PASSWORD, SENDER_EMAIL]):
|
| 24 |
+
print("错误: SMTP配置不完整。请检查 .env 文件中的 SMTP_SERVER, SMTP_USERNAME, SMTP_PASSWORD, SENDER_EMAIL。")
|
| 25 |
+
return False
|
| 26 |
+
|
| 27 |
+
msg = MIMEText(body, 'plain', 'utf-8')
|
| 28 |
+
msg['From'] = f"{SENDER_NAME} <{SENDER_EMAIL}>"
|
| 29 |
+
msg['To'] = Header(to_email, 'utf-8')
|
| 30 |
+
msg['Subject'] = Header(subject, 'utf-8')
|
| 31 |
+
print("邮件内容构建完成。")
|
| 32 |
+
print(f"发件人: {msg['From']}, 收件人: {msg['To']}, 主题: {msg['Subject']}")
|
| 33 |
+
|
| 34 |
+
email_sent_successfully = False # 新增标志变量
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
if SMTP_PORT == 587:
|
| 38 |
+
print(f"尝试通过端口 {SMTP_PORT} 连接到 SMTP 服务器: {SMTP_SERVER} (使用 STARTTLS)")
|
| 39 |
+
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
|
| 40 |
+
server.set_debuglevel(1) # 设置调试级别为1,打印SMTP交互日志
|
| 41 |
+
print("SMTP 服务器连接成功。")
|
| 42 |
+
print("尝试启动 TLS...")
|
| 43 |
+
server.starttls(context=ssl.create_default_context())
|
| 44 |
+
print("TLS 启动成功。")
|
| 45 |
+
print(f"尝试使用用户 {SMTP_USERNAME} 登录...")
|
| 46 |
+
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
| 47 |
+
print("SMTP 登录成功。")
|
| 48 |
+
print(f"尝试发送邮件从 {SENDER_EMAIL} 到 {to_email}...")
|
| 49 |
+
server.sendmail(SENDER_EMAIL, to_email, msg.as_string())
|
| 50 |
+
print(f"邮件发送成功至 {to_email}!")
|
| 51 |
+
email_sent_successfully = True # 设置标志为True
|
| 52 |
+
|
| 53 |
+
elif SMTP_PORT == 465:
|
| 54 |
+
print(f"尝试通过端口 {SMTP_PORT} 连接到 SMTP 服务器: {SMTP_SERVER} (使用 SSL/TLS)")
|
| 55 |
+
context = ssl.create_default_context()
|
| 56 |
+
context.check_hostname = False
|
| 57 |
+
context.verify_mode = ssl.CERT_NONE
|
| 58 |
+
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context, timeout=10) as server:
|
| 59 |
+
server.set_debuglevel(1) # 设置调试级别为1,打印SMTP交互日志
|
| 60 |
+
print("SMTP_SSL 服务器连接成功。")
|
| 61 |
+
print(f"尝试使用用户 {SMTP_USERNAME} 登录...")
|
| 62 |
+
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
| 63 |
+
print("SMTP 登录成功。")
|
| 64 |
+
print(f"尝试发送邮件从 {SENDER_EMAIL} 到 {to_email}...")
|
| 65 |
+
server.sendmail(SENDER_EMAIL, to_email, msg.as_string())
|
| 66 |
+
print(f"邮件发送成功至 {to_email}!")
|
| 67 |
+
email_sent_successfully = True # 设置标志为True
|
| 68 |
+
else:
|
| 69 |
+
print(f"错误:不支持的端口 {SMTP_PORT}。目前只支持 465 (SSL) 和 587 (STARTTLS)。")
|
| 70 |
+
# email_sent_successfully 保持为 False
|
| 71 |
+
|
| 72 |
+
except smtplib.SMTPAuthenticationError:
|
| 73 |
+
print("认证失败:请检查 .env 文件中的 SMTP_USERNAME 和 SMTP_PASSWORD。对于 QQ 邮箱,请确保使用的是授权码而非登录密码。")
|
| 74 |
+
except smtplib.SMTPConnectError as e:
|
| 75 |
+
print(f"连接失败:请检查 SMTP_SERVER 地址、SMTP_PORT 端口是否正确,以及网络防火墙设置。错误详情: {e}")
|
| 76 |
+
except smtplib.SMTPServerDisconnected as e:
|
| 77 |
+
print(f"SMTP 服务器意外断开连接。错误详情: {e}")
|
| 78 |
+
except smtplib.SMTPException as e:
|
| 79 |
+
print(f"SMTP 协议错误:{e}")
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"发生未知错误: {e}")
|
| 82 |
+
finally:
|
| 83 |
+
print("--- 邮件发送测试流程结束 ---")
|
| 84 |
+
|
| 85 |
+
return email_sent_successfully # 根据标志变量返回最终结果
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
test_to_email = "geqintan@qq.com"
|
| 89 |
+
test_subject = "SuperProxy 测试邮件"
|
| 90 |
+
test_body = "这是一封来自 SuperProxy 的测试邮件。"
|
| 91 |
+
success = test_send_email(test_to_email, test_subject, test_body)
|
| 92 |
+
if success:
|
| 93 |
+
print("测试邮件发送过程完成。请检查收件箱。")
|
| 94 |
+
else:
|
| 95 |
+
print("测试邮件发送过程失败。")
|
test_supabase.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from supabase import create_client, Client
|
| 4 |
+
from supabase_auth.errors import AuthApiError
|
| 5 |
+
|
| 6 |
+
# Load environment variables from .env file
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
# Supabase Configuration
|
| 10 |
+
SUPABASE_URL = os.environ.get("SUPABASE_URL")
|
| 11 |
+
SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY")
|
| 12 |
+
|
| 13 |
+
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
|
| 14 |
+
print("Error: Supabase URL and Anon Key must be set as environment variables in .env file.")
|
| 15 |
+
exit(1)
|
| 16 |
+
|
| 17 |
+
supabase_anon: Client = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
|
| 18 |
+
|
| 19 |
+
SUPABASE_SERVICE_ROLE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
| 20 |
+
if not SUPABASE_SERVICE_ROLE_KEY:
|
| 21 |
+
print("Error: SUPABASE_SERVICE_ROLE_KEY must be set as an environment variable in .env file for admin operations.")
|
| 22 |
+
exit(1)
|
| 23 |
+
supabase_admin: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
| 24 |
+
|
| 25 |
+
async def test_db_connection():
|
| 26 |
+
"""Tests a simple database connection by querying a non-existent table."""
|
| 27 |
+
print("\n--- Testing Database Connection ---")
|
| 28 |
+
try:
|
| 29 |
+
# Attempt a simple query to check connection.
|
| 30 |
+
# This will likely fail if the table doesn't exist, but confirms connection.
|
| 31 |
+
res = supabase_anon.table('non_existent_table').select('*').limit(1).execute()
|
| 32 |
+
print("Database connection successful (or query executed without connection error).")
|
| 33 |
+
print(f"Query result (expected empty or error): {res.data}")
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print(f"Database connection test failed: {e}")
|
| 36 |
+
print("Please ensure SUPABASE_URL is correct and your network allows access to Supabase.")
|
| 37 |
+
|
| 38 |
+
async def test_user_registration(email, password):
|
| 39 |
+
"""Tests user registration with Supabase."""
|
| 40 |
+
print(f"\n--- Testing User Registration for {email} ---")
|
| 41 |
+
try:
|
| 42 |
+
res = supabase_anon.auth.sign_up({
|
| 43 |
+
"email": email,
|
| 44 |
+
"password": password
|
| 45 |
+
})
|
| 46 |
+
if res.user:
|
| 47 |
+
print(f"User registration successful for {res.user.email}! Please check your email for verification.")
|
| 48 |
+
print(f"User ID: {res.user.id}")
|
| 49 |
+
else:
|
| 50 |
+
print("User registration failed: No user returned.")
|
| 51 |
+
print(f"Supabase response: {res}")
|
| 52 |
+
except AuthApiError as e:
|
| 53 |
+
print(f"User registration failed (AuthApiError): {e.message}")
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"An unexpected error occurred during registration: {e}")
|
| 56 |
+
print("Please ensure Supabase URL and Anon Key are correct, and Email Signups are enabled in Supabase Auth settings.")
|
| 57 |
+
|
| 58 |
+
async def list_supabase_users():
|
| 59 |
+
"""Lists all users in Supabase."""
|
| 60 |
+
print("\n--- Listing Supabase Users ---")
|
| 61 |
+
try:
|
| 62 |
+
# Supabase admin client is required to list users
|
| 63 |
+
users_list = supabase_admin.auth.admin.list_users()
|
| 64 |
+
|
| 65 |
+
if users_list: # Assuming it returns a list of user objects directly
|
| 66 |
+
print(f"Found {len(users_list)} users:")
|
| 67 |
+
for user in users_list:
|
| 68 |
+
print(f" User ID: {user.id}, Email: {user.email}, Created At: {user.created_at}")
|
| 69 |
+
else:
|
| 70 |
+
print("No users found in Supabase.")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"Failed to list Supabase users: {e}")
|
| 73 |
+
print("Please ensure your Supabase Anon Key has sufficient permissions or use a service role key if necessary.")
|
| 74 |
+
|
| 75 |
+
async def main():
|
| 76 |
+
await test_db_connection()
|
| 77 |
+
|
| 78 |
+
# Replace with a test email and password
|
| 79 |
+
test_email = "test@example.com" # Using a more standard test email format
|
| 80 |
+
test_password = "testpassword"
|
| 81 |
+
await test_user_registration(test_email, test_password)
|
| 82 |
+
|
| 83 |
+
await list_supabase_users()
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
import asyncio
|
| 87 |
+
asyncio.run(main())
|