mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-06 20:02:51 +08:00
增加为需要输入密码才能访问,同时支持远程PGSQL
This commit is contained in:
14
README.md
14
README.md
@@ -36,6 +36,7 @@
|
|||||||
- 注册参数(超时、重试、密码长度等)
|
- 注册参数(超时、重试、密码长度等)
|
||||||
- 验证码等待配置
|
- 验证码等待配置
|
||||||
- 数据库管理(备份、清理)
|
- 数据库管理(备份、清理)
|
||||||
|
- 支持远程 PostgreSQL
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -67,6 +68,17 @@ python webui.py --host 0.0.0.0 --port 8080
|
|||||||
python webui.py --debug
|
python webui.py --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 使用远程 PostgreSQL
|
||||||
|
|
||||||
|
通过环境变量指定数据库连接字符串:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export APP_DATABASE_URL="postgresql://user:password@host:5432/dbname"
|
||||||
|
python webui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
也支持 `DATABASE_URL`,优先级低于 `APP_DATABASE_URL`。
|
||||||
|
|
||||||
启动后访问 http://127.0.0.1:8000
|
启动后访问 http://127.0.0.1:8000
|
||||||
|
|
||||||
## 打包为可执行文件
|
## 打包为可执行文件
|
||||||
@@ -107,7 +119,7 @@ codex-register-v2/
|
|||||||
| 层级 | 技术 |
|
| 层级 | 技术 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Web 框架 | FastAPI + Uvicorn |
|
| Web 框架 | FastAPI + Uvicorn |
|
||||||
| 数据库 | SQLAlchemy + SQLite |
|
| 数据库 | SQLAlchemy + SQLite / PostgreSQL |
|
||||||
| 模板引擎 | Jinja2 |
|
| 模板引擎 | Jinja2 |
|
||||||
| HTTP 客户端 | curl_cffi(浏览器指纹模拟) |
|
| HTTP 客户端 | curl_cffi(浏览器指纹模拟) |
|
||||||
| 实时通信 | WebSocket(websockets 库) |
|
| 实时通信 | WebSocket(websockets 库) |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"pydantic-settings>=2.0.0",
|
"pydantic-settings>=2.0.0",
|
||||||
"sqlalchemy>=2.0.0",
|
"sqlalchemy>=2.0.0",
|
||||||
"aiosqlite>=0.19.0",
|
"aiosqlite>=0.19.0",
|
||||||
|
"psycopg[binary]>=3.1.18",
|
||||||
"websockets>=16.0",
|
"websockets>=16.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -87,6 +87,13 @@ SETTING_DEFINITIONS: Dict[str, SettingDefinition] = {
|
|||||||
description="Web UI 密钥",
|
description="Web UI 密钥",
|
||||||
is_secret=True
|
is_secret=True
|
||||||
),
|
),
|
||||||
|
"webui_access_password": SettingDefinition(
|
||||||
|
db_key="webui.access_password",
|
||||||
|
default_value="admin123",
|
||||||
|
category=SettingCategory.WEBUI,
|
||||||
|
description="Web UI 访问密码",
|
||||||
|
is_secret=True
|
||||||
|
),
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
"log_level": SettingDefinition(
|
"log_level": SettingDefinition(
|
||||||
@@ -434,6 +441,14 @@ def _convert_value(attr_name: str, value: str) -> Any:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_database_url(url: str) -> str:
|
||||||
|
if url.startswith("postgres://"):
|
||||||
|
return "postgresql+psycopg://" + url[len("postgres://"):]
|
||||||
|
if url.startswith("postgresql://"):
|
||||||
|
return "postgresql+psycopg://" + url[len("postgresql://"):]
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
def _value_to_string(value: Any) -> str:
|
def _value_to_string(value: Any) -> str:
|
||||||
"""将值转换为数据库存储的字符串"""
|
"""将值转换为数据库存储的字符串"""
|
||||||
if isinstance(value, SecretStr):
|
if isinstance(value, SecretStr):
|
||||||
@@ -462,7 +477,12 @@ def init_default_settings() -> None:
|
|||||||
for attr_name, defn in SETTING_DEFINITIONS.items():
|
for attr_name, defn in SETTING_DEFINITIONS.items():
|
||||||
existing = get_setting(db, defn.db_key)
|
existing = get_setting(db, defn.db_key)
|
||||||
if not existing:
|
if not existing:
|
||||||
default_value = _value_to_string(defn.default_value)
|
default_value = defn.default_value
|
||||||
|
if attr_name == "database_url":
|
||||||
|
env_url = os.environ.get("APP_DATABASE_URL") or os.environ.get("DATABASE_URL")
|
||||||
|
if env_url:
|
||||||
|
default_value = _normalize_database_url(env_url)
|
||||||
|
default_value = _value_to_string(default_value)
|
||||||
set_setting(
|
set_setting(
|
||||||
db,
|
db,
|
||||||
defn.db_key,
|
defn.db_key,
|
||||||
@@ -490,6 +510,9 @@ def _load_settings_from_db() -> Dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
# 数据库中没有此设置,使用默认值
|
# 数据库中没有此设置,使用默认值
|
||||||
settings_dict[attr_name] = _convert_value(attr_name, _value_to_string(defn.default_value))
|
settings_dict[attr_name] = _convert_value(attr_name, _value_to_string(defn.default_value))
|
||||||
|
env_url = os.environ.get("APP_DATABASE_URL") or os.environ.get("DATABASE_URL")
|
||||||
|
if env_url:
|
||||||
|
settings_dict["database_url"] = _normalize_database_url(env_url)
|
||||||
return settings_dict
|
return settings_dict
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Settings] 从数据库加载设置失败: {e},使用默认值")
|
print(f"[Settings] 从数据库加载设置失败: {e},使用默认值")
|
||||||
@@ -534,9 +557,14 @@ class Settings(BaseModel):
|
|||||||
@field_validator('database_url', mode='before')
|
@field_validator('database_url', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_database_url(cls, v):
|
def validate_database_url(cls, v):
|
||||||
|
if isinstance(v, str):
|
||||||
|
if v.startswith(("postgres://", "postgresql://")):
|
||||||
|
return _normalize_database_url(v)
|
||||||
|
if v.startswith(("postgresql+psycopg://", "postgresql+psycopg2://")):
|
||||||
|
return v
|
||||||
if isinstance(v, str) and v.startswith("sqlite:///"):
|
if isinstance(v, str) and v.startswith("sqlite:///"):
|
||||||
return v
|
return v
|
||||||
if isinstance(v, str) and not v.startswith(("sqlite:///", "postgresql://", "mysql://")):
|
if isinstance(v, str) and not v.startswith(("sqlite:///", "postgresql://", "postgresql+psycopg://", "postgresql+psycopg2://", "mysql://")):
|
||||||
# 如果是文件路径,转换为 SQLite URL
|
# 如果是文件路径,转换为 SQLite URL
|
||||||
if os.path.isabs(v) or ":/" not in v:
|
if os.path.isabs(v) or ":/" not in v:
|
||||||
return f"sqlite:///{v}"
|
return f"sqlite:///{v}"
|
||||||
@@ -546,6 +574,7 @@ class Settings(BaseModel):
|
|||||||
webui_host: str = "0.0.0.0"
|
webui_host: str = "0.0.0.0"
|
||||||
webui_port: int = 8000
|
webui_port: int = 8000
|
||||||
webui_secret_key: SecretStr = SecretStr("your-secret-key-change-in-production")
|
webui_secret_key: SecretStr = SecretStr("your-secret-key-change-in-production")
|
||||||
|
webui_access_password: SecretStr = SecretStr("admin123")
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
log_level: str = "INFO"
|
log_level: str = "INFO"
|
||||||
|
|||||||
@@ -393,6 +393,10 @@ def get_data_dir() -> Path:
|
|||||||
数据目录 Path 对象
|
数据目录 Path 对象
|
||||||
"""
|
"""
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
if not settings.database_url.startswith("sqlite"):
|
||||||
|
data_dir = Path(os.environ.get("APP_DATA_DIR", "data"))
|
||||||
|
data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return data_dir
|
||||||
data_dir = Path(settings.database_url).parent
|
data_dir = Path(settings.database_url).parent
|
||||||
|
|
||||||
# 如果 database_url 是 SQLite URL,提取路径
|
# 如果 database_url 是 SQLite URL,提取路径
|
||||||
@@ -563,4 +567,4 @@ class Timer:
|
|||||||
return self.elapsed
|
return self.elapsed
|
||||||
if self.start_time is not None:
|
if self.start_time is not None:
|
||||||
return time.time() - self.start_time
|
return time.time() - self.start_time
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|||||||
@@ -15,25 +15,37 @@ from .models import Base
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_sqlalchemy_url(database_url: str) -> str:
|
||||||
|
if database_url.startswith("postgresql://"):
|
||||||
|
return "postgresql+psycopg://" + database_url[len("postgresql://"):]
|
||||||
|
if database_url.startswith("postgres://"):
|
||||||
|
return "postgresql+psycopg://" + database_url[len("postgres://"):]
|
||||||
|
return database_url
|
||||||
|
|
||||||
|
|
||||||
class DatabaseSessionManager:
|
class DatabaseSessionManager:
|
||||||
"""数据库会话管理器"""
|
"""数据库会话管理器"""
|
||||||
|
|
||||||
def __init__(self, database_url: str = None):
|
def __init__(self, database_url: str = None):
|
||||||
if database_url is None:
|
if database_url is None:
|
||||||
# 优先使用 APP_DATA_DIR 环境变量(PyInstaller 打包后由 webui.py 设置)
|
env_url = os.environ.get("APP_DATABASE_URL") or os.environ.get("DATABASE_URL")
|
||||||
data_dir = os.environ.get('APP_DATA_DIR') or os.path.join(
|
if env_url:
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
database_url = env_url
|
||||||
'data'
|
else:
|
||||||
)
|
# 优先使用 APP_DATA_DIR 环境变量(PyInstaller 打包后由 webui.py 设置)
|
||||||
db_path = os.path.join(data_dir, 'database.db')
|
data_dir = os.environ.get('APP_DATA_DIR') or os.path.join(
|
||||||
# 确保目录存在
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||||
os.makedirs(data_dir, exist_ok=True)
|
'data'
|
||||||
database_url = f"sqlite:///{db_path}"
|
)
|
||||||
|
db_path = os.path.join(data_dir, 'database.db')
|
||||||
|
# 确保目录存在
|
||||||
|
os.makedirs(data_dir, exist_ok=True)
|
||||||
|
database_url = f"sqlite:///{db_path}"
|
||||||
|
|
||||||
self.database_url = database_url
|
self.database_url = _build_sqlalchemy_url(database_url)
|
||||||
self.engine = create_engine(
|
self.engine = create_engine(
|
||||||
database_url,
|
self.database_url,
|
||||||
connect_args={"check_same_thread": False} if database_url.startswith("sqlite") else {},
|
connect_args={"check_same_thread": False} if self.database_url.startswith("sqlite") else {},
|
||||||
echo=False, # 设置为 True 可以查看所有 SQL 语句
|
echo=False, # 设置为 True 可以查看所有 SQL 语句
|
||||||
pool_pre_ping=True # 连接池预检查
|
pool_pre_ping=True # 连接池预检查
|
||||||
)
|
)
|
||||||
@@ -152,4 +164,4 @@ def get_db() -> Generator[Session, None, None]:
|
|||||||
try:
|
try:
|
||||||
yield db
|
yield db
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ FastAPI 应用主文件
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
import secrets
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request, Form
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from ..config.settings import get_settings
|
from ..config.settings import get_settings
|
||||||
from .routes import api_router
|
from .routes import api_router
|
||||||
@@ -78,24 +81,74 @@ def create_app() -> FastAPI:
|
|||||||
# 模板引擎
|
# 模板引擎
|
||||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||||
|
|
||||||
|
def _auth_token(password: str) -> str:
|
||||||
|
secret = get_settings().webui_secret_key.get_secret_value().encode("utf-8")
|
||||||
|
return hmac.new(secret, password.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
def _is_authenticated(request: Request) -> bool:
|
||||||
|
cookie = request.cookies.get("webui_auth")
|
||||||
|
expected = _auth_token(get_settings().webui_access_password.get_secret_value())
|
||||||
|
return bool(cookie) and secrets.compare_digest(cookie, expected)
|
||||||
|
|
||||||
|
def _redirect_to_login(request: Request) -> RedirectResponse:
|
||||||
|
return RedirectResponse(url=f"/login?next={request.url.path}", status_code=302)
|
||||||
|
|
||||||
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
|
async def login_page(request: Request, next: Optional[str] = "/"):
|
||||||
|
"""登录页面"""
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"login.html",
|
||||||
|
{"request": request, "error": "", "next": next or "/"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/login")
|
||||||
|
async def login_submit(request: Request, password: str = Form(...), next: Optional[str] = "/"):
|
||||||
|
"""处理登录提交"""
|
||||||
|
expected = get_settings().webui_access_password.get_secret_value()
|
||||||
|
if not secrets.compare_digest(password, expected):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"login.html",
|
||||||
|
{"request": request, "error": "密码错误", "next": next or "/"},
|
||||||
|
status_code=401
|
||||||
|
)
|
||||||
|
|
||||||
|
response = RedirectResponse(url=next or "/", status_code=302)
|
||||||
|
response.set_cookie("webui_auth", _auth_token(expected), httponly=True, samesite="lax")
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.get("/logout")
|
||||||
|
async def logout(request: Request, next: Optional[str] = "/login"):
|
||||||
|
"""退出登录"""
|
||||||
|
response = RedirectResponse(url=next or "/login", status_code=302)
|
||||||
|
response.delete_cookie("webui_auth")
|
||||||
|
return response
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
"""首页 - 注册页面"""
|
"""首页 - 注册页面"""
|
||||||
|
if not _is_authenticated(request):
|
||||||
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("index.html", {"request": request})
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
@app.get("/accounts", response_class=HTMLResponse)
|
@app.get("/accounts", response_class=HTMLResponse)
|
||||||
async def accounts_page(request: Request):
|
async def accounts_page(request: Request):
|
||||||
"""账号管理页面"""
|
"""账号管理页面"""
|
||||||
|
if not _is_authenticated(request):
|
||||||
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("accounts.html", {"request": request})
|
return templates.TemplateResponse("accounts.html", {"request": request})
|
||||||
|
|
||||||
@app.get("/email-services", response_class=HTMLResponse)
|
@app.get("/email-services", response_class=HTMLResponse)
|
||||||
async def email_services_page(request: Request):
|
async def email_services_page(request: Request):
|
||||||
"""邮箱服务管理页面"""
|
"""邮箱服务管理页面"""
|
||||||
|
if not _is_authenticated(request):
|
||||||
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("email_services.html", {"request": request})
|
return templates.TemplateResponse("email_services.html", {"request": request})
|
||||||
|
|
||||||
@app.get("/settings", response_class=HTMLResponse)
|
@app.get("/settings", response_class=HTMLResponse)
|
||||||
async def settings_page(request: Request):
|
async def settings_page(request: Request):
|
||||||
"""设置页面"""
|
"""设置页面"""
|
||||||
|
if not _is_authenticated(request):
|
||||||
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("settings.html", {"request": request})
|
return templates.TemplateResponse("settings.html", {"request": request})
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@@ -52,9 +52,10 @@ class RegistrationSettings(BaseModel):
|
|||||||
|
|
||||||
class WebUISettings(BaseModel):
|
class WebUISettings(BaseModel):
|
||||||
"""Web UI 设置"""
|
"""Web UI 设置"""
|
||||||
host: str = "0.0.0.0"
|
host: Optional[str] = None
|
||||||
port: int = 8000
|
port: Optional[int] = None
|
||||||
debug: bool = False
|
debug: Optional[bool] = None
|
||||||
|
access_password: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class AllSettings(BaseModel):
|
class AllSettings(BaseModel):
|
||||||
@@ -96,6 +97,7 @@ async def get_all_settings():
|
|||||||
"host": settings.webui_host,
|
"host": settings.webui_host,
|
||||||
"port": settings.webui_port,
|
"port": settings.webui_port,
|
||||||
"debug": settings.debug,
|
"debug": settings.debug,
|
||||||
|
"has_access_password": bool(settings.webui_access_password and settings.webui_access_password.get_secret_value()),
|
||||||
},
|
},
|
||||||
"tempmail": {
|
"tempmail": {
|
||||||
"base_url": settings.tempmail_base_url,
|
"base_url": settings.tempmail_base_url,
|
||||||
@@ -317,6 +319,23 @@ async def update_registration_settings(request: RegistrationSettings):
|
|||||||
return {"success": True, "message": "注册设置已更新"}
|
return {"success": True, "message": "注册设置已更新"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/webui")
|
||||||
|
async def update_webui_settings(request: WebUISettings):
|
||||||
|
"""更新 Web UI 设置"""
|
||||||
|
update_dict = {}
|
||||||
|
if request.host is not None:
|
||||||
|
update_dict["webui_host"] = request.host
|
||||||
|
if request.port is not None:
|
||||||
|
update_dict["webui_port"] = request.port
|
||||||
|
if request.debug is not None:
|
||||||
|
update_dict["debug"] = request.debug
|
||||||
|
if request.access_password:
|
||||||
|
update_dict["webui_access_password"] = request.access_password
|
||||||
|
|
||||||
|
update_settings(**update_dict)
|
||||||
|
return {"success": True, "message": "Web UI 设置已更新"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/database")
|
@router.get("/database")
|
||||||
async def get_database_info():
|
async def get_database_info():
|
||||||
"""获取数据库信息"""
|
"""获取数据库信息"""
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ const elements = {
|
|||||||
// 验证码设置
|
// 验证码设置
|
||||||
emailCodeForm: document.getElementById('email-code-form'),
|
emailCodeForm: document.getElementById('email-code-form'),
|
||||||
// Outlook 设置
|
// Outlook 设置
|
||||||
outlookSettingsForm: document.getElementById('outlook-settings-form')
|
outlookSettingsForm: document.getElementById('outlook-settings-form'),
|
||||||
|
// Web UI 访问控制
|
||||||
|
webuiSettingsForm: document.getElementById('webui-settings-form')
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选中的服务 ID
|
// 选中的服务 ID
|
||||||
@@ -231,6 +233,10 @@ function initEventListeners() {
|
|||||||
if (elements.outlookSettingsForm) {
|
if (elements.outlookSettingsForm) {
|
||||||
elements.outlookSettingsForm.addEventListener('submit', handleSaveOutlookSettings);
|
elements.outlookSettingsForm.addEventListener('submit', handleSaveOutlookSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elements.webuiSettingsForm) {
|
||||||
|
elements.webuiSettingsForm.addEventListener('submit', handleSaveWebuiSettings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载设置
|
// 加载设置
|
||||||
@@ -269,12 +275,40 @@ async function loadSettings() {
|
|||||||
// 加载 Outlook 设置
|
// 加载 Outlook 设置
|
||||||
loadOutlookSettings();
|
loadOutlookSettings();
|
||||||
|
|
||||||
|
// Web UI 访问密码提示
|
||||||
|
if (data.webui?.has_access_password) {
|
||||||
|
const input = document.getElementById('webui-access-password');
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
input.placeholder = '已配置,留空保持不变';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载设置失败:', error);
|
console.error('加载设置失败:', error);
|
||||||
toast.error('加载设置失败');
|
toast.error('加载设置失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存 Web UI 设置
|
||||||
|
async function handleSaveWebuiSettings(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const accessPassword = document.getElementById('webui-access-password').value;
|
||||||
|
const payload = {
|
||||||
|
access_password: accessPassword || null
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post('/settings/webui', payload);
|
||||||
|
toast.success('Web UI 设置已更新');
|
||||||
|
document.getElementById('webui-access-password').value = '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存 Web UI 设置失败:', error);
|
||||||
|
toast.error('保存 Web UI 设置失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载邮箱服务
|
// 加载邮箱服务
|
||||||
async function loadEmailServices() {
|
async function loadEmailServices() {
|
||||||
// 检查元素是否存在
|
// 检查元素是否存在
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
<a href="/accounts" class="nav-link active">账号管理</a>
|
<a href="/accounts" class="nav-link active">账号管理</a>
|
||||||
<a href="/email-services" class="nav-link">邮箱服务</a>
|
<a href="/email-services" class="nav-link">邮箱服务</a>
|
||||||
<a href="/settings" class="nav-link">设置</a>
|
<a href="/settings" class="nav-link">设置</a>
|
||||||
|
<a href="/logout" class="nav-link">退出</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||||
🌙
|
🌙
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<a href="/accounts" class="nav-link">账号管理</a>
|
<a href="/accounts" class="nav-link">账号管理</a>
|
||||||
<a href="/email-services" class="nav-link active">邮箱服务</a>
|
<a href="/email-services" class="nav-link active">邮箱服务</a>
|
||||||
<a href="/settings" class="nav-link">设置</a>
|
<a href="/settings" class="nav-link">设置</a>
|
||||||
|
<a href="/logout" class="nav-link">退出</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||||
🌙
|
🌙
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
<a href="/accounts" class="nav-link">账号管理</a>
|
<a href="/accounts" class="nav-link">账号管理</a>
|
||||||
<a href="/email-services" class="nav-link">邮箱服务</a>
|
<a href="/email-services" class="nav-link">邮箱服务</a>
|
||||||
<a href="/settings" class="nav-link">设置</a>
|
<a href="/settings" class="nav-link">设置</a>
|
||||||
|
<a href="/logout" class="nav-link">退出</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||||
🌙
|
🌙
|
||||||
|
|||||||
62
templates/login.html
Normal file
62
templates/login.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>访问验证 - OpenAI 注册系统</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔒</text></svg>">
|
||||||
|
<style>
|
||||||
|
.login-wrap {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 12vh auto 0;
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
.login-title {
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
.login-subtitle {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
.login-error {
|
||||||
|
background: var(--danger-light);
|
||||||
|
color: var(--danger-color);
|
||||||
|
border: 1px solid var(--danger-color);
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="login-wrap">
|
||||||
|
<div class="card login-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="login-title">🔒 访问验证</h3>
|
||||||
|
<span class="hint">请输入访问密码继续</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if error %}
|
||||||
|
<div class="login-error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="/login">
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">访问密码</label>
|
||||||
|
<input type="password" id="password" name="password" autocomplete="current-password" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions" style="margin-top: var(--spacing-lg);">
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%">验证进入</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<a href="/accounts" class="nav-link">账号管理</a>
|
<a href="/accounts" class="nav-link">账号管理</a>
|
||||||
<a href="/email-services" class="nav-link">邮箱服务</a>
|
<a href="/email-services" class="nav-link">邮箱服务</a>
|
||||||
<a href="/settings" class="nav-link active">设置</a>
|
<a href="/settings" class="nav-link active">设置</a>
|
||||||
|
<a href="/logout" class="nav-link">退出</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||||
🌙
|
🌙
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
<!-- 设置标签页 -->
|
<!-- 设置标签页 -->
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" data-tab="proxy">🌐 代理设置</button>
|
<button class="tab-btn active" data-tab="proxy">🌐 代理设置</button>
|
||||||
|
<button class="tab-btn" data-tab="webui">🔒 访问控制</button>
|
||||||
<button class="tab-btn" data-tab="cpa">☁️ CPA上传</button>
|
<button class="tab-btn" data-tab="cpa">☁️ CPA上传</button>
|
||||||
<button class="tab-btn" data-tab="outlook">📮 Outlook配置</button>
|
<button class="tab-btn" data-tab="outlook">📮 Outlook配置</button>
|
||||||
<button class="tab-btn" data-tab="registration">⚙️ 注册配置</button>
|
<button class="tab-btn" data-tab="registration">⚙️ 注册配置</button>
|
||||||
@@ -181,6 +183,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 访问控制 -->
|
||||||
|
<div class="tab-content" id="webui-tab">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Web UI 访问密码</h3>
|
||||||
|
<span class="hint">用于访问页面的密码,留空表示不修改</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="webui-settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="webui-access-password">访问密码</label>
|
||||||
|
<input type="password" id="webui-access-password" name="access_password" placeholder="留空保持不变" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">💾 保存设置</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 添加代理模态框 -->
|
<!-- 添加代理模态框 -->
|
||||||
<div class="modal" id="add-proxy-modal">
|
<div class="modal" id="add-proxy-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user