mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-03 03:00:15 +08:00
Compare commits
2 Commits
dev
...
feat/one-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26c114379f | ||
|
|
41ec03b285 |
@@ -6,6 +6,7 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import traceback
|
||||
|
||||
from astrbot.core import LogBroker, logger
|
||||
@@ -17,11 +18,17 @@ from astrbot.dashboard.server import AstrBotDashboard
|
||||
class InitialLoader:
|
||||
"""AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。"""
|
||||
|
||||
def __init__(self, db: BaseDatabase, log_broker: LogBroker) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
db: BaseDatabase,
|
||||
log_broker: LogBroker,
|
||||
dashboard_temporary_login_token: str | None = None,
|
||||
) -> None:
|
||||
self.db = db
|
||||
self.logger = logger
|
||||
self.log_broker = log_broker
|
||||
self.webui_dir: str | None = None
|
||||
self.dashboard_temporary_login_token = dashboard_temporary_login_token
|
||||
|
||||
async def start(self) -> None:
|
||||
core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db)
|
||||
@@ -33,6 +40,22 @@ class InitialLoader:
|
||||
logger.critical(f"😭 初始化 AstrBot 失败:{e} !!!")
|
||||
return
|
||||
|
||||
if self.dashboard_temporary_login_token:
|
||||
token_hash = hashlib.sha256(
|
||||
self.dashboard_temporary_login_token.encode("utf-8")
|
||||
).hexdigest()
|
||||
object.__setattr__(
|
||||
core_lifecycle.astrbot_config,
|
||||
"_dashboard_temporary_login_token_hash",
|
||||
token_hash,
|
||||
)
|
||||
object.__setattr__(
|
||||
core_lifecycle.astrbot_config,
|
||||
"_dashboard_temporary_login_token",
|
||||
self.dashboard_temporary_login_token,
|
||||
)
|
||||
self.dashboard_temporary_login_token = None
|
||||
|
||||
core_task = core_lifecycle.start()
|
||||
|
||||
webui_dir = self.webui_dir
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import secrets
|
||||
|
||||
import jwt
|
||||
import pyotp
|
||||
@@ -43,6 +45,7 @@ from .route import Response, Route, RouteContext
|
||||
|
||||
DASHBOARD_JWT_COOKIE_NAME = "astrbot_dashboard_jwt"
|
||||
DASHBOARD_JWT_COOKIE_MAX_AGE = 7 * 24 * 60 * 60
|
||||
DASHBOARD_TEMPORARY_LOGIN_JWT_MAX_AGE = 3 * 24 * 60 * 60
|
||||
SKIP_DEFAULT_PASSWORD_AUTH_ENV = "ASTRBOT_DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH"
|
||||
SKIP_DEFAULT_PASSWORD_AUTH_ENV_LEGACY = "DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH"
|
||||
LOCAL_DASHBOARD_HOSTS = {"127.0.0.1", "localhost", "::1"}
|
||||
@@ -64,6 +67,15 @@ LEGACY_PASSWORD_LOGIN_FAILURE_MESSAGE = (
|
||||
)
|
||||
|
||||
|
||||
def verify_dashboard_temporary_login_token(config, token: str) -> bool:
|
||||
token = token.strip()
|
||||
token_hash = getattr(config, "_dashboard_temporary_login_token_hash", "")
|
||||
if not token or not isinstance(token_hash, str) or not token_hash:
|
||||
return False
|
||||
candidate_hash = hashlib.sha256(token.encode("utf-8")).hexdigest()
|
||||
return secrets.compare_digest(candidate_hash, token_hash)
|
||||
|
||||
|
||||
class AuthRoute(Route):
|
||||
def __init__(self, context: RouteContext, db) -> None:
|
||||
super().__init__(context)
|
||||
@@ -87,6 +99,7 @@ class AuthRoute(Route):
|
||||
{
|
||||
"setup_required": await self._is_setup_required(),
|
||||
"skip_default_password_auth": self._can_skip_default_password_auth(),
|
||||
"temporary_login_token_enabled": self._temporary_login_token_enabled(),
|
||||
"password_upgrade_required": not await is_password_storage_upgraded(
|
||||
self.db,
|
||||
self.config,
|
||||
@@ -221,19 +234,28 @@ class AuthRoute(Route):
|
||||
storage_upgraded = await is_password_storage_upgraded(self.db, self.config)
|
||||
password = get_dashboard_password_hash(self.config, upgraded=storage_upgraded)
|
||||
post_data = await request.json
|
||||
if not isinstance(post_data, dict):
|
||||
return Response().error("Invalid request payload").__dict__
|
||||
|
||||
req_username = (
|
||||
post_data.get("username") if isinstance(post_data, dict) else None
|
||||
)
|
||||
req_password = (
|
||||
post_data.get("password") if isinstance(post_data, dict) else None
|
||||
)
|
||||
totp_code = post_data.get("code") if isinstance(post_data, dict) else None
|
||||
trust_device_flag = (
|
||||
post_data.get("trust_device_flag") is True
|
||||
if isinstance(post_data, dict)
|
||||
else False
|
||||
)
|
||||
login_type = post_data.get("login_type")
|
||||
if login_type == "temporary_token":
|
||||
req_token = post_data.get("temporary_token")
|
||||
if not isinstance(req_token, str) or not self._verify_temporary_login_token(
|
||||
req_token
|
||||
):
|
||||
await asyncio.sleep(3)
|
||||
return await self._error_response("临时 Token 无效", 401)
|
||||
return await self._create_login_response(
|
||||
username,
|
||||
storage_upgraded,
|
||||
password,
|
||||
jwt_max_age=DASHBOARD_TEMPORARY_LOGIN_JWT_MAX_AGE,
|
||||
)
|
||||
|
||||
req_username = post_data.get("username")
|
||||
req_password = post_data.get("password")
|
||||
totp_code = post_data.get("code")
|
||||
trust_device_flag = post_data.get("trust_device_flag") is True
|
||||
if not isinstance(req_username, str) or not isinstance(req_password, str):
|
||||
return Response().error("Invalid request payload").__dict__
|
||||
|
||||
@@ -291,6 +313,24 @@ class AuthRoute(Route):
|
||||
else:
|
||||
return await self._error_response("恢复码无效", 401)
|
||||
|
||||
return await self._create_login_response(
|
||||
username,
|
||||
storage_upgraded,
|
||||
password,
|
||||
totp_verified=totp_verified,
|
||||
trust_device_flag=trust_device_flag,
|
||||
)
|
||||
|
||||
async def _create_login_response(
|
||||
self,
|
||||
username: str,
|
||||
storage_upgraded: bool,
|
||||
password: str,
|
||||
*,
|
||||
totp_verified: bool = False,
|
||||
trust_device_flag: bool = False,
|
||||
jwt_max_age: int = DASHBOARD_JWT_COOKIE_MAX_AGE,
|
||||
):
|
||||
change_pwd_hint = False
|
||||
legacy_pwd_hint = is_legacy_dashboard_password(password)
|
||||
password_change_required = await is_password_change_required(
|
||||
@@ -308,7 +348,7 @@ class AuthRoute(Route):
|
||||
logger.warning("为了保证安全,请尽快修改默认密码。")
|
||||
if password_change_required and not DEMO_MODE:
|
||||
change_pwd_hint = True
|
||||
token = self.generate_jwt(username)
|
||||
token = self.generate_jwt(username, max_age=jwt_max_age)
|
||||
login_data = {
|
||||
"token": token,
|
||||
"username": username,
|
||||
@@ -318,7 +358,7 @@ class AuthRoute(Route):
|
||||
}
|
||||
payload = Response().ok(login_data)
|
||||
response = await make_response(jsonify(payload.__dict__))
|
||||
self._set_dashboard_jwt_cookie(response, token)
|
||||
self._set_dashboard_jwt_cookie(response, token, max_age=jwt_max_age)
|
||||
|
||||
if totp_verified and trust_device_flag:
|
||||
raw_token = await issue_totp_trusted_device(self.config, self.db)
|
||||
@@ -334,6 +374,13 @@ class AuthRoute(Route):
|
||||
)
|
||||
return response
|
||||
|
||||
def _temporary_login_token_enabled(self) -> bool:
|
||||
token_hash = getattr(self.config, "_dashboard_temporary_login_token_hash", "")
|
||||
return isinstance(token_hash, str) and bool(token_hash)
|
||||
|
||||
def _verify_temporary_login_token(self, token: str) -> bool:
|
||||
return verify_dashboard_temporary_login_token(self.config, token)
|
||||
|
||||
async def logout(self):
|
||||
response = await make_response(
|
||||
jsonify(Response().ok(None, "已退出登录").__dict__)
|
||||
@@ -396,11 +443,11 @@ class AuthRoute(Route):
|
||||
|
||||
return Response().ok(None, "Updated account successfully").__dict__
|
||||
|
||||
def generate_jwt(self, username):
|
||||
def generate_jwt(self, username, *, max_age: int = DASHBOARD_JWT_COOKIE_MAX_AGE):
|
||||
payload = {
|
||||
"username": username,
|
||||
"exp": datetime.datetime.now(datetime.timezone.utc)
|
||||
+ datetime.timedelta(days=7),
|
||||
+ datetime.timedelta(seconds=max_age),
|
||||
}
|
||||
jwt_token = self.config["dashboard"].get("jwt_secret", None)
|
||||
if not jwt_token:
|
||||
@@ -463,11 +510,15 @@ class AuthRoute(Route):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _set_dashboard_jwt_cookie(response, token: str) -> None:
|
||||
def _set_dashboard_jwt_cookie(
|
||||
response,
|
||||
token: str,
|
||||
max_age: int = DASHBOARD_JWT_COOKIE_MAX_AGE,
|
||||
) -> None:
|
||||
response.set_cookie(
|
||||
DASHBOARD_JWT_COOKIE_NAME,
|
||||
token,
|
||||
max_age=DASHBOARD_JWT_COOKIE_MAX_AGE,
|
||||
max_age=max_age,
|
||||
httponly=True,
|
||||
samesite="Strict",
|
||||
secure=AuthRoute._use_secure_dashboard_jwt_cookie(),
|
||||
|
||||
@@ -430,7 +430,6 @@ class AstrBotDashboard:
|
||||
r = jsonify(Response().error("Token 无效").__dict__)
|
||||
r.status_code = 401
|
||||
return r
|
||||
|
||||
username = payload.get("username")
|
||||
if not isinstance(username, str) or not username.strip():
|
||||
raise jwt.InvalidTokenError("missing username in token payload")
|
||||
@@ -565,15 +564,37 @@ class AstrBotDashboard:
|
||||
def _build_dashboard_credentials_display(self) -> str:
|
||||
username = self.config["dashboard"].get("username", "astrbot")
|
||||
generated_password = getattr(self.config, "_generated_dashboard_password", None)
|
||||
if not generated_password:
|
||||
temporary_login_token = getattr(
|
||||
self.config,
|
||||
"_dashboard_temporary_login_token",
|
||||
None,
|
||||
)
|
||||
if not generated_password and not temporary_login_token:
|
||||
return f" ➜ Username: {username}\n ✨✨✨\n"
|
||||
|
||||
credentials_display = (
|
||||
f" ➜ Initial username: {username}\n"
|
||||
f" ➜ Initial password: {generated_password}\n"
|
||||
" ➜ Change it after logging in\n ✨✨✨\n"
|
||||
)
|
||||
object.__setattr__(self.config, "_generated_dashboard_password", None)
|
||||
lines = [f" ➜ Initial username: {username}\n"]
|
||||
if generated_password:
|
||||
lines.extend(
|
||||
[
|
||||
f" ➜ Initial password: {generated_password}\n",
|
||||
" ➜ Change it after logging in\n",
|
||||
]
|
||||
)
|
||||
object.__setattr__(self.config, "_generated_dashboard_password", None)
|
||||
if temporary_login_token:
|
||||
lines.extend(
|
||||
[
|
||||
f" ➜ Temporary login token: {temporary_login_token}\n",
|
||||
" ➜ The token is valid until this AstrBot process exits\n",
|
||||
]
|
||||
)
|
||||
object.__setattr__(
|
||||
self.config,
|
||||
"_dashboard_temporary_login_token",
|
||||
None,
|
||||
)
|
||||
lines.append(" ✨✨✨\n")
|
||||
credentials_display = "".join(lines)
|
||||
return credentials_display
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 271 icons */
|
||||
/* Auto-generated MDI subset – 273 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -464,6 +464,10 @@
|
||||
content: "\F1036";
|
||||
}
|
||||
|
||||
.mdi-file-search-outline::before {
|
||||
content: "\F0C7D";
|
||||
}
|
||||
|
||||
.mdi-file-upload::before {
|
||||
content: "\F0A4D";
|
||||
}
|
||||
@@ -688,6 +692,10 @@
|
||||
content: "\F16B2";
|
||||
}
|
||||
|
||||
.mdi-logout::before {
|
||||
content: "\F0343";
|
||||
}
|
||||
|
||||
.mdi-magnify::before {
|
||||
content: "\F0349";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,7 @@
|
||||
"buttons": {
|
||||
"update": "Update",
|
||||
"account": "Account",
|
||||
"logout": "Log Out",
|
||||
"theme": {
|
||||
"light": "Light Mode",
|
||||
"dark": "Dark Mode"
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"defaultHint": "If this is your first login, check the logs for the default password.",
|
||||
"temporaryToken": {
|
||||
"link": "Log in with temporary token",
|
||||
"title": "Temporary Token Login",
|
||||
"subtitle": "Use the temporary token printed in the startup logs.",
|
||||
"token": "Temporary Token",
|
||||
"submit": "Log in with Temporary Token",
|
||||
"backToLogin": "Back to Login"
|
||||
},
|
||||
"totp": {
|
||||
"code": "Verification code",
|
||||
"verify": "Verify",
|
||||
@@ -60,4 +68,4 @@
|
||||
"switchToDark": "Switch to Dark Theme",
|
||||
"switchToLight": "Switch to Light Theme"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"buttons": {
|
||||
"update": "Обновить",
|
||||
"account": "Аккаунт",
|
||||
"logout": "Выйти",
|
||||
"theme": {
|
||||
"light": "Светлая тема",
|
||||
"dark": "Темная тема"
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"defaultHint": "Если это первый вход, проверьте пароль по умолчанию в логах.",
|
||||
"temporaryToken": {
|
||||
"link": "Войти с временным токеном",
|
||||
"title": "Вход по временному токену",
|
||||
"subtitle": "Используйте временный токен, напечатанный в логах запуска.",
|
||||
"token": "Временный токен",
|
||||
"submit": "Войти по временному токену",
|
||||
"backToLogin": "Назад к входу"
|
||||
},
|
||||
"totp": {
|
||||
"code": "Код подтверждения",
|
||||
"verify": "Проверить",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"buttons": {
|
||||
"update": "更新",
|
||||
"account": "账户",
|
||||
"logout": "退出登录",
|
||||
"theme": {
|
||||
"light": "浅色模式",
|
||||
"dark": "深色模式"
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"defaultHint": "如果这是首次登录,请在日志中查看默认密码。",
|
||||
"temporaryToken": {
|
||||
"link": "输入临时 Token 登录",
|
||||
"title": "临时 Token 登录",
|
||||
"subtitle": "使用启动日志中输出的临时 Token 登录。",
|
||||
"token": "临时 Token",
|
||||
"submit": "使用临时 Token 登录",
|
||||
"backToLogin": "返回账号登录"
|
||||
},
|
||||
"totp": {
|
||||
"code": "验证码",
|
||||
"verify": "验证",
|
||||
|
||||
@@ -390,6 +390,11 @@ function accountEdit() {
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
const authStore = useAuthStore();
|
||||
authStore.logout();
|
||||
}
|
||||
|
||||
function getVersion() {
|
||||
axios
|
||||
.get("/api/stat/version")
|
||||
@@ -1134,6 +1139,15 @@ onMounted(async () => {
|
||||
t("core.header.accountDialog.title")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="logout" class="styled-menu-item" rounded="md">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
t("core.header.buttons.logout")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
|
||||
<!-- 更新对话框 -->
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { router } from '@/router';
|
||||
import axios from 'axios';
|
||||
import { defineStore } from "pinia";
|
||||
import { router } from "@/router";
|
||||
import axios from "axios";
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
// @ts-ignore
|
||||
username: '',
|
||||
username: "",
|
||||
returnUrl: null,
|
||||
}),
|
||||
actions: {
|
||||
async finishAuthenticatedSession(data: any): Promise<void> {
|
||||
this.username = data.username;
|
||||
localStorage.setItem('user', this.username);
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem("user", this.username);
|
||||
localStorage.setItem("token", data.token);
|
||||
const passwordUpgradeRequired = !!data?.password_upgrade_required;
|
||||
const passwordWarning =
|
||||
!!data?.change_pwd_hint ||
|
||||
(!!data?.legacy_pwd_hint && !passwordUpgradeRequired);
|
||||
if (passwordWarning) {
|
||||
localStorage.setItem('change_pwd_hint', 'true');
|
||||
localStorage.setItem("change_pwd_hint", "true");
|
||||
if (data?.legacy_pwd_hint && !passwordUpgradeRequired) {
|
||||
localStorage.setItem('legacy_pwd_hint', 'true');
|
||||
localStorage.setItem("legacy_pwd_hint", "true");
|
||||
} else {
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
localStorage.removeItem("legacy_pwd_hint");
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('change_pwd_hint');
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
localStorage.removeItem("change_pwd_hint");
|
||||
localStorage.removeItem("legacy_pwd_hint");
|
||||
}
|
||||
if (passwordUpgradeRequired) {
|
||||
localStorage.setItem('password_upgrade_required', 'true');
|
||||
localStorage.setItem("password_upgrade_required", "true");
|
||||
} else {
|
||||
localStorage.removeItem('password_upgrade_required');
|
||||
localStorage.removeItem("password_upgrade_required");
|
||||
}
|
||||
|
||||
const onboardingCompleted = await this.checkOnboardingCompleted();
|
||||
this.returnUrl = null;
|
||||
if (passwordWarning) {
|
||||
router.push('/auth/setup');
|
||||
router.push("/auth/setup");
|
||||
return;
|
||||
}
|
||||
if (onboardingCompleted) {
|
||||
router.push('/dashboard/default');
|
||||
router.push("/dashboard/default");
|
||||
} else {
|
||||
router.push('/welcome');
|
||||
router.push("/welcome");
|
||||
}
|
||||
},
|
||||
async login(
|
||||
@@ -51,22 +51,43 @@ export const useAuthStore = defineStore("auth", {
|
||||
password: string,
|
||||
code?: string,
|
||||
trustDeviceToken = false,
|
||||
): Promise<'totp_required' | void> {
|
||||
): Promise<"totp_required" | void> {
|
||||
try {
|
||||
const res = await axios.post('/api/auth/login', {
|
||||
username: username,
|
||||
password: password,
|
||||
code: code,
|
||||
trust_device_flag: trustDeviceToken,
|
||||
}, {
|
||||
validateStatus: (status) => (status >= 200 && status < 300) || status === 401
|
||||
});
|
||||
const res = await axios.post(
|
||||
"/api/auth/login",
|
||||
{
|
||||
username: username,
|
||||
password: password,
|
||||
code: code,
|
||||
trust_device_flag: trustDeviceToken,
|
||||
},
|
||||
{
|
||||
validateStatus: (status) =>
|
||||
(status >= 200 && status < 300) || status === 401,
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status === 401 && res.data?.data?.totp_required) {
|
||||
return 'totp_required';
|
||||
return "totp_required";
|
||||
}
|
||||
|
||||
if (res.data.status === 'error') {
|
||||
if (res.data.status === "error") {
|
||||
return Promise.reject(res.data.message);
|
||||
}
|
||||
|
||||
await this.finishAuthenticatedSession(res.data.data);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async loginWithTemporaryToken(token: string): Promise<void> {
|
||||
try {
|
||||
const res = await axios.post("/api/auth/login", {
|
||||
login_type: "temporary_token",
|
||||
temporary_token: token,
|
||||
});
|
||||
|
||||
if (res.data.status === "error") {
|
||||
return Promise.reject(res.data.message);
|
||||
}
|
||||
|
||||
@@ -81,14 +102,16 @@ export const useAuthStore = defineStore("auth", {
|
||||
confirmPassword: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const endpoint = this.has_token() ? '/api/auth/setup-authenticated' : '/api/auth/setup';
|
||||
const endpoint = this.has_token()
|
||||
? "/api/auth/setup-authenticated"
|
||||
: "/api/auth/setup";
|
||||
const res = await axios.post(endpoint, {
|
||||
username: username,
|
||||
password: password,
|
||||
confirm_password: confirmPassword,
|
||||
});
|
||||
|
||||
if (res.data.status === 'error') {
|
||||
if (res.data.status === "error") {
|
||||
return Promise.reject(res.data.message);
|
||||
}
|
||||
|
||||
@@ -100,44 +123,46 @@ export const useAuthStore = defineStore("auth", {
|
||||
async checkOnboardingCompleted(): Promise<boolean> {
|
||||
try {
|
||||
// 1. 检查平台配置
|
||||
const platformRes = await axios.get('/api/config/get');
|
||||
const hasPlatform = (platformRes.data.data.config.platform || []).length > 0;
|
||||
const platformRes = await axios.get("/api/config/get");
|
||||
const hasPlatform =
|
||||
(platformRes.data.data.config.platform || []).length > 0;
|
||||
if (!hasPlatform) return false;
|
||||
|
||||
// 2. 检查提供者配置
|
||||
const providerRes = await axios.get('/api/config/provider/template');
|
||||
const providerRes = await axios.get("/api/config/provider/template");
|
||||
const providers = providerRes.data.data?.providers || [];
|
||||
const sources = providerRes.data.data?.provider_sources || [];
|
||||
const sourceMap = new Map();
|
||||
sources.forEach((s: any) => sourceMap.set(s.id, s.provider_type));
|
||||
|
||||
|
||||
const hasProvider = providers.some((provider: any) => {
|
||||
if (provider.provider_type) return provider.provider_type === 'chat_completion';
|
||||
if (provider.provider_type)
|
||||
return provider.provider_type === "chat_completion";
|
||||
if (provider.provider_source_id) {
|
||||
const type = sourceMap.get(provider.provider_source_id);
|
||||
if (type === 'chat_completion') return true;
|
||||
if (type === "chat_completion") return true;
|
||||
}
|
||||
return String(provider.type || '').includes('chat_completion');
|
||||
return String(provider.type || "").includes("chat_completion");
|
||||
});
|
||||
|
||||
return hasProvider;
|
||||
} catch (e) {
|
||||
console.error('Failed to check onboarding status:', e);
|
||||
console.error("Failed to check onboarding status:", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.username = '';
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('change_pwd_hint');
|
||||
localStorage.removeItem('legacy_pwd_hint');
|
||||
localStorage.removeItem('password_upgrade_required');
|
||||
void axios.post('/api/auth/logout').catch(() => undefined);
|
||||
router.push('/auth/login');
|
||||
this.username = "";
|
||||
localStorage.removeItem("user");
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("change_pwd_hint");
|
||||
localStorage.removeItem("legacy_pwd_hint");
|
||||
localStorage.removeItem("password_upgrade_required");
|
||||
void axios.post("/api/auth/logout").catch(() => undefined);
|
||||
router.push("/auth/login");
|
||||
},
|
||||
has_token(): boolean {
|
||||
return !!localStorage.getItem('token');
|
||||
}
|
||||
}
|
||||
return !!localStorage.getItem("token");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,43 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import AuthStageAccount from './stages/AuthStageAccount.vue';
|
||||
import AuthStageTotp from './stages/AuthStageTotp.vue';
|
||||
import AuthStageRecovery from './stages/AuthStageRecovery.vue';
|
||||
import axios from "axios";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import AuthStageAccount from "./stages/AuthStageAccount.vue";
|
||||
import AuthStageTotp from "./stages/AuthStageTotp.vue";
|
||||
import AuthStageRecovery from "./stages/AuthStageRecovery.vue";
|
||||
import AuthStageTemporaryToken from "./stages/AuthStageTemporaryToken.vue";
|
||||
|
||||
const { tm: t } = useModuleI18n('features/auth');
|
||||
const { tm: t } = useModuleI18n("features/auth");
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const totpCode = ref('');
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const totpCode = ref("");
|
||||
const trustTotpDevice = ref(false);
|
||||
const recoveryCode = ref('');
|
||||
const recoveryCode = ref("");
|
||||
const temporaryToken = ref("");
|
||||
const temporaryTokenLoginEnabled = ref(false);
|
||||
const loading = ref(false);
|
||||
const apiError = ref('');
|
||||
const stage = ref<'account' | 'totp' | 'recovery'>('account');
|
||||
const apiError = ref("");
|
||||
const stage = ref<"account" | "totp" | "recovery" | "temporary-token">(
|
||||
"account",
|
||||
);
|
||||
|
||||
function syncReturnUrl() {
|
||||
// @ts-ignore
|
||||
authStore.returnUrl = new URLSearchParams(window.location.search).get(
|
||||
"redirect",
|
||||
);
|
||||
}
|
||||
|
||||
function resetTotpStage() {
|
||||
totpCode.value = '';
|
||||
totpCode.value = "";
|
||||
trustTotpDevice.value = false;
|
||||
}
|
||||
|
||||
function goToAccountStage() {
|
||||
stage.value = 'account';
|
||||
apiError.value = '';
|
||||
stage.value = "account";
|
||||
apiError.value = "";
|
||||
resetTotpStage();
|
||||
temporaryToken.value = "";
|
||||
}
|
||||
|
||||
function goToTotpStage() {
|
||||
stage.value = 'totp';
|
||||
apiError.value = '';
|
||||
stage.value = "totp";
|
||||
apiError.value = "";
|
||||
}
|
||||
|
||||
function goToRecoveryStage() {
|
||||
stage.value = 'recovery';
|
||||
apiError.value = '';
|
||||
recoveryCode.value = '';
|
||||
stage.value = "recovery";
|
||||
apiError.value = "";
|
||||
recoveryCode.value = "";
|
||||
}
|
||||
|
||||
function goToTemporaryTokenStage() {
|
||||
stage.value = "temporary-token";
|
||||
apiError.value = "";
|
||||
temporaryToken.value = "";
|
||||
}
|
||||
|
||||
async function submitAccountStage() {
|
||||
@@ -45,16 +65,15 @@ async function submitAccountStage() {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
apiError.value = '';
|
||||
apiError.value = "";
|
||||
try {
|
||||
// @ts-ignore
|
||||
authStore.returnUrl = new URLSearchParams(window.location.search).get('redirect');
|
||||
syncReturnUrl();
|
||||
const res = await authStore.login(username.value, password.value);
|
||||
if (res === 'totp_required') {
|
||||
if (res === "totp_required") {
|
||||
goToTotpStage();
|
||||
}
|
||||
} catch (err) {
|
||||
apiError.value = String(err || '') || 'Login failed';
|
||||
apiError.value = String(err || "") || "Login failed";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -65,7 +84,7 @@ async function submitTotpStage() {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
apiError.value = '';
|
||||
apiError.value = "";
|
||||
try {
|
||||
await authStore.login(
|
||||
username.value,
|
||||
@@ -74,12 +93,43 @@ async function submitTotpStage() {
|
||||
trustTotpDevice.value,
|
||||
);
|
||||
} catch (err) {
|
||||
apiError.value = String(err || '') || 'Verification failed';
|
||||
apiError.value = String(err || "") || "Verification failed";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitTemporaryTokenStage() {
|
||||
const token = temporaryToken.value.trim();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
apiError.value = "";
|
||||
try {
|
||||
syncReturnUrl();
|
||||
await authStore.loginWithTemporaryToken(token);
|
||||
} catch (err) {
|
||||
apiError.value = String(err || "") || "Temporary token login failed";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTemporaryTokenLoginStatus() {
|
||||
try {
|
||||
const res = await axios.get("/api/auth/setup-status");
|
||||
temporaryTokenLoginEnabled.value =
|
||||
!!res.data?.data?.temporary_login_token_enabled;
|
||||
} catch {
|
||||
temporaryTokenLoginEnabled.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadTemporaryTokenLoginStatus();
|
||||
});
|
||||
|
||||
defineExpose({ stage });
|
||||
|
||||
async function submitRecoveryStage() {
|
||||
@@ -87,11 +137,11 @@ async function submitRecoveryStage() {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
apiError.value = '';
|
||||
apiError.value = "";
|
||||
try {
|
||||
await authStore.login(username.value, password.value, recoveryCode.value);
|
||||
} catch (err) {
|
||||
apiError.value = String(err || '') || 'Recovery login failed';
|
||||
apiError.value = String(err || "") || "Recovery login failed";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -100,15 +150,29 @@ async function submitRecoveryStage() {
|
||||
|
||||
<template>
|
||||
<div class="mt-4 login-form">
|
||||
<AuthStageAccount
|
||||
v-if="stage === 'account'"
|
||||
:username="username"
|
||||
:password="password"
|
||||
:loading="loading"
|
||||
@update:username="(value) => (username = value)"
|
||||
@update:password="(value) => (password = value)"
|
||||
@submit="submitAccountStage"
|
||||
/>
|
||||
<template v-if="stage === 'account'">
|
||||
<AuthStageAccount
|
||||
:username="username"
|
||||
:password="password"
|
||||
:loading="loading"
|
||||
@update:username="(value) => (username = value)"
|
||||
@update:password="(value) => (password = value)"
|
||||
@submit="submitAccountStage"
|
||||
/>
|
||||
|
||||
<div v-if="temporaryTokenLoginEnabled" class="temporary-token-link-row">
|
||||
<span
|
||||
class="temporary-token-login-link"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="goToTemporaryTokenStage"
|
||||
@keyup.enter="goToTemporaryTokenStage"
|
||||
@keyup.space.prevent="goToTemporaryTokenStage"
|
||||
>
|
||||
{{ t("temporaryToken.link") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<AuthStageTotp
|
||||
v-else-if="stage === 'totp'"
|
||||
@@ -123,6 +187,15 @@ async function submitRecoveryStage() {
|
||||
@use-recovery="goToRecoveryStage"
|
||||
/>
|
||||
|
||||
<AuthStageTemporaryToken
|
||||
v-else-if="stage === 'temporary-token'"
|
||||
:token="temporaryToken"
|
||||
:loading="loading"
|
||||
@update:token="(value) => (temporaryToken = value)"
|
||||
@submit="submitTemporaryTokenStage"
|
||||
@back="goToAccountStage"
|
||||
/>
|
||||
|
||||
<AuthStageRecovery
|
||||
v-else
|
||||
:code="recoveryCode"
|
||||
@@ -133,7 +206,12 @@ async function submitRecoveryStage() {
|
||||
/>
|
||||
|
||||
<div v-if="apiError" class="mt-4 error-container">
|
||||
<v-alert color="error" variant="tonal" icon="mdi-alert-circle" border="start">
|
||||
<v-alert
|
||||
color="error"
|
||||
variant="tonal"
|
||||
icon="mdi-alert-circle"
|
||||
border="start"
|
||||
>
|
||||
{{ apiError }}
|
||||
</v-alert>
|
||||
</div>
|
||||
@@ -224,5 +302,30 @@ async function submitRecoveryStage() {
|
||||
color: rgba(var(--v-theme-on-surface), 0.85);
|
||||
}
|
||||
|
||||
.temporary-token-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 0.82rem;
|
||||
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.temporary-token-link-row {
|
||||
margin-top: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.temporary-token-login-link {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.temporary-token-login-link:focus-visible {
|
||||
border-radius: 4px;
|
||||
outline: 2px solid rgba(var(--v-theme-primary), 0.35);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
|
||||
const { tm: t } = useModuleI18n("features/auth");
|
||||
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:token", value: string): void;
|
||||
(e: "submit"): void;
|
||||
(e: "back"): void;
|
||||
}>();
|
||||
|
||||
function onSubmit() {
|
||||
emit("submit");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="account-stage-header">
|
||||
<div>
|
||||
<div class="account-stage-user">{{ t("temporaryToken.title") }}</div>
|
||||
<div class="temporary-token-subtitle">
|
||||
{{ t("temporaryToken.subtitle") }}
|
||||
</div>
|
||||
</div>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
icon="mdi-arrow-left"
|
||||
:disabled="props.loading"
|
||||
@click="emit('back')"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ t("temporaryToken.backToLogin") }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
:model-value="props.token"
|
||||
:label="t('temporaryToken.token')"
|
||||
class="mt-4 pwd-input"
|
||||
required
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
type="password"
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
:disabled="props.loading"
|
||||
@update:model-value="(value: string) => emit('update:token', value)"
|
||||
@keyup.enter="onSubmit"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
block
|
||||
class="login-btn mt-8"
|
||||
variant="flat"
|
||||
size="large"
|
||||
:loading="props.loading"
|
||||
:disabled="props.loading || !props.token"
|
||||
@click="onSubmit"
|
||||
>
|
||||
<span class="login-btn-text">{{ t("temporaryToken.submit") }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -26,6 +26,36 @@ Set dashboard.host in data/cmd_config.json to enable remote access.
|
||||
|
||||
其中的 `UiYVpZxnW8k22IWqf0ru5pOy` 就是默认密码。在使用默认密码登录后,会自动进入设置账户环节。
|
||||
|
||||
### 管理面板登录相关安全配置
|
||||
|
||||
如果需要在本机初始化场景中跳过默认密码验证,可以设置环境变量 `ASTRBOT_DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH=true`。兼容旧变量名 `DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH`。该能力只会在管理面板监听地址为 `127.0.0.1`、`localhost` 或 `::1` 时生效;如果 `dashboard.host` 是 `0.0.0.0` 或其他非本机地址,即使设置了环境变量也不会跳过密码验证。
|
||||
|
||||
管理面板还内置了登录相关限流配置,位于 `data/cmd_config.json` 的 `dashboard.auth_rate_limit`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"auth_rate_limit": {
|
||||
"enable": true,
|
||||
"average_interval": 1.0,
|
||||
"max_burst": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该限流只应用于以下接口:
|
||||
|
||||
| 接口 | 说明 |
|
||||
| --- | --- |
|
||||
| `/api/auth/login` | 管理面板账号密码登录,以及临时 Token 登录 |
|
||||
| `/api/auth/totp/setup` | TOTP 配置/验证 |
|
||||
| `/api/config/astrbot/update` | 触发 AstrBot 更新 |
|
||||
|
||||
限流按客户端 IP 统计,上述接口共享同一个令牌桶。默认配置表示同一 IP 最多可瞬时请求 3 次,之后平均每 1 秒恢复 1 次请求额度。达到限制时会返回 HTTP 429。
|
||||
|
||||
如果 AstrBot 部署在反向代理后,默认会使用直接连接 IP 统计限流。只有在确认代理会正确传递客户端 IP 时,才建议开启 `dashboard.trust_proxy_headers`,开启后会尝试读取 `X-Forwarded-For` 或 `X-Real-IP`。
|
||||
|
||||
### 管理面板的密码忘记了
|
||||
|
||||
如果你忘记了 AstrBot 管理面板的密码,你可以在 `AstrBot/data/cmd_config.json` 配置文件中找到 `"dashboard"` 字段,如下:
|
||||
|
||||
19
main.py
19
main.py
@@ -2,6 +2,7 @@ import argparse
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -116,7 +117,9 @@ async def check_dashboard_files(webui_dir: str | None = None):
|
||||
return data_dist_path
|
||||
|
||||
|
||||
async def main_async(webui_dir_arg: str | None) -> None:
|
||||
async def main_async(
|
||||
webui_dir_arg: str | None, print_login_token: bool = False
|
||||
) -> None:
|
||||
"""主异步入口"""
|
||||
# 检查仪表板文件
|
||||
webui_dir = await check_dashboard_files(webui_dir_arg)
|
||||
@@ -131,7 +134,12 @@ async def main_async(webui_dir_arg: str | None) -> None:
|
||||
# 打印 logo
|
||||
logger.info(logo_tmpl)
|
||||
|
||||
core_lifecycle = InitialLoader(db, log_broker)
|
||||
temporary_login_token = secrets.token_urlsafe(32) if print_login_token else None
|
||||
core_lifecycle = InitialLoader(
|
||||
db,
|
||||
log_broker,
|
||||
dashboard_temporary_login_token=temporary_login_token,
|
||||
)
|
||||
core_lifecycle.webui_dir = webui_dir
|
||||
await core_lifecycle.start()
|
||||
|
||||
@@ -144,6 +152,11 @@ if __name__ == "__main__":
|
||||
help="Specify the directory path for WebUI static files",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--print-login-token",
|
||||
action="store_true",
|
||||
help="Print a temporary dashboard login token for this process",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
check_env()
|
||||
@@ -153,4 +166,4 @@ if __name__ == "__main__":
|
||||
LogManager.set_queue_handler(logger, log_broker)
|
||||
|
||||
# 只使用一次 asyncio.run()
|
||||
asyncio.run(main_async(args.webui_dir))
|
||||
asyncio.run(main_async(args.webui_dir, args.print_login_token))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
@@ -12,6 +13,7 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs, urlsplit, urlunsplit
|
||||
|
||||
import jwt
|
||||
import pyotp
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
@@ -40,7 +42,10 @@ from astrbot.dashboard.password_state import (
|
||||
set_password_change_required,
|
||||
set_password_storage_upgraded,
|
||||
)
|
||||
from astrbot.dashboard.routes.auth import DASHBOARD_JWT_COOKIE_NAME
|
||||
from astrbot.dashboard.routes.auth import (
|
||||
DASHBOARD_JWT_COOKIE_NAME,
|
||||
DASHBOARD_TEMPORARY_LOGIN_JWT_MAX_AGE,
|
||||
)
|
||||
from astrbot.dashboard.routes.plugin import PluginRoute
|
||||
from astrbot.dashboard.server import AstrBotDashboard
|
||||
from tests.fixtures.helpers import (
|
||||
@@ -334,6 +339,100 @@ async def test_auth_login(
|
||||
assert "Secure" not in jwt_cookie_header
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_login_with_temporary_token(
|
||||
app: Quart,
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
async def skip_sleep(_seconds):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("astrbot.dashboard.routes.auth.asyncio.sleep", skip_sleep)
|
||||
monkeypatch.setitem(app.config, "DASHBOARD_JWT_COOKIE_SECURE", False)
|
||||
|
||||
token = f"temporary-token-{uuid.uuid4()}"
|
||||
token_hash = hashlib.sha256(token.encode("utf-8")).hexdigest()
|
||||
previous_hash = getattr(
|
||||
core_lifecycle_td.astrbot_config,
|
||||
"_dashboard_temporary_login_token_hash",
|
||||
None,
|
||||
)
|
||||
try:
|
||||
object.__setattr__(
|
||||
core_lifecycle_td.astrbot_config,
|
||||
"_dashboard_temporary_login_token_hash",
|
||||
token_hash,
|
||||
)
|
||||
|
||||
test_client = app.test_client()
|
||||
response = await test_client.get("/api/auth/setup-status")
|
||||
data = await response.get_json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["data"]["temporary_login_token_enabled"] is True
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"login_type": "temporary_token",
|
||||
"temporary_token": "wrong-token",
|
||||
},
|
||||
)
|
||||
data = await response.get_json()
|
||||
assert response.status_code == 401
|
||||
assert data["status"] == "error"
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"login_type": "temporary_token",
|
||||
"temporary_token": token,
|
||||
},
|
||||
)
|
||||
data = await response.get_json()
|
||||
assert data["status"] == "ok"
|
||||
assert (
|
||||
data["data"]["username"]
|
||||
== core_lifecycle_td.astrbot_config["dashboard"]["username"]
|
||||
)
|
||||
dashboard_jwt = data["data"]["token"]
|
||||
assert dashboard_jwt
|
||||
set_cookie_headers = response.headers.getlist("Set-Cookie")
|
||||
jwt_cookie_header = next(
|
||||
(
|
||||
value
|
||||
for value in set_cookie_headers
|
||||
if DASHBOARD_JWT_COOKIE_NAME in value
|
||||
),
|
||||
"",
|
||||
)
|
||||
assert jwt_cookie_header
|
||||
assert f"Max-Age={DASHBOARD_TEMPORARY_LOGIN_JWT_MAX_AGE}" in jwt_cookie_header
|
||||
|
||||
jwt_payload = jwt.decode(
|
||||
dashboard_jwt,
|
||||
core_lifecycle_td.astrbot_config["dashboard"]["jwt_secret"],
|
||||
algorithms=["HS256"],
|
||||
)
|
||||
now_ts = datetime.now().timestamp()
|
||||
expires_in = jwt_payload["exp"] - now_ts
|
||||
assert 0 < expires_in <= DASHBOARD_TEMPORARY_LOGIN_JWT_MAX_AGE
|
||||
|
||||
response = await test_client.get(
|
||||
"/api/stat/version",
|
||||
headers={"Authorization": f"Bearer {dashboard_jwt}"},
|
||||
)
|
||||
data = await response.get_json()
|
||||
assert response.status_code == 200
|
||||
assert data["status"] == "ok"
|
||||
finally:
|
||||
object.__setattr__(
|
||||
core_lifecycle_td.astrbot_config,
|
||||
"_dashboard_temporary_login_token_hash",
|
||||
previous_hash,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_login_secure_cookie_override(
|
||||
app: Quart,
|
||||
@@ -374,7 +473,11 @@ async def test_auth_rate_limit_uses_same_bucket_across_paths(
|
||||
cfg = core_lifecycle_td.astrbot_config["dashboard"]
|
||||
rl_original = cfg.get("auth_rate_limit", {})
|
||||
tp_original = cfg.get("trust_proxy_headers", False)
|
||||
cfg["auth_rate_limit"] = {"enable": True, "average_interval": 3600.0, "max_burst": 1}
|
||||
cfg["auth_rate_limit"] = {
|
||||
"enable": True,
|
||||
"average_interval": 3600.0,
|
||||
"max_burst": 1,
|
||||
}
|
||||
cfg["trust_proxy_headers"] = True
|
||||
|
||||
try:
|
||||
@@ -406,7 +509,11 @@ async def test_auth_rate_limit_separates_different_client_ips(
|
||||
cfg = core_lifecycle_td.astrbot_config["dashboard"]
|
||||
rl_original = cfg.get("auth_rate_limit", {})
|
||||
tp_original = cfg.get("trust_proxy_headers", False)
|
||||
cfg["auth_rate_limit"] = {"enable": True, "average_interval": 3600.0, "max_burst": 1}
|
||||
cfg["auth_rate_limit"] = {
|
||||
"enable": True,
|
||||
"average_interval": 3600.0,
|
||||
"max_burst": 1,
|
||||
}
|
||||
cfg["trust_proxy_headers"] = True
|
||||
|
||||
try:
|
||||
@@ -450,7 +557,11 @@ async def test_auth_rate_limit_ignores_proxy_headers_by_default(
|
||||
cfg = core_lifecycle_td.astrbot_config["dashboard"]
|
||||
rl_original = cfg.get("auth_rate_limit", {})
|
||||
tp_original = cfg.get("trust_proxy_headers", False)
|
||||
cfg["auth_rate_limit"] = {"enable": True, "average_interval": 3600.0, "max_burst": 1}
|
||||
cfg["auth_rate_limit"] = {
|
||||
"enable": True,
|
||||
"average_interval": 3600.0,
|
||||
"max_burst": 1,
|
||||
}
|
||||
cfg["trust_proxy_headers"] = False
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user