geqintan commited on
Commit
133609a
·
1 Parent(s): 5b6f6ac
.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())