feat(auth,admin): add LinuxDO OAuth login and admin settings
Implement LinuxDO OAuth authorization-code login flow with state validation and local account binding, and add admin APIs/UI to manage OAuth runtime settings with URL validation and secret masking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
680b551efd
commit
ae478755be
6 files changed
+355
-6
No files matched your search
@@ -2,13 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import secrets
|
||||||
|
from urllib.parse import urlencode, urlparse
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import requests
|
||||||
from flask import Flask, jsonify, redirect, render_template, request, session, url_for
|
from flask import Flask, jsonify, redirect, render_template, request, session, url_for
|
||||||
from flask_socketio import SocketIO, emit, join_room as sio_join_room
|
from flask_socketio import SocketIO, emit, join_room as sio_join_room
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from config import Config
|
from config import Config, DEFAULT_SETTINGS, SETTINGS_FILE
|
||||||
from game.action_resolver import ActionResolver
|
from game.action_resolver import ActionResolver
|
||||||
from game.room_manager import RoomManager
|
from game.room_manager import RoomManager
|
||||||
from game.state_manager import StateManager
|
from game.state_manager import StateManager
|
||||||
@@ -416,6 +419,80 @@ def create_app(config_object: type[Config] = Config) -> tuple[Flask, SocketIO]:
|
|||||||
app.extensions["story_engine"] = story_engine
|
app.extensions["story_engine"] = story_engine
|
||||||
app.extensions["room_manager"] = room_manager
|
app.extensions["room_manager"] = room_manager
|
||||||
|
|
||||||
|
def load_runtime_settings() -> dict[str, Any]:
|
||||||
|
defaults = dict(DEFAULT_SETTINGS)
|
||||||
|
defaults["linuxdo_oauth"] = dict(DEFAULT_SETTINGS.get("linuxdo_oauth", {}))
|
||||||
|
try:
|
||||||
|
payload = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return defaults
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
merged = dict(defaults)
|
||||||
|
merged.update(payload)
|
||||||
|
raw_oauth = payload.get("linuxdo_oauth", {})
|
||||||
|
if isinstance(raw_oauth, dict):
|
||||||
|
merged["linuxdo_oauth"] = {**defaults["linuxdo_oauth"], **raw_oauth}
|
||||||
|
else:
|
||||||
|
merged["linuxdo_oauth"] = dict(defaults["linuxdo_oauth"])
|
||||||
|
return merged
|
||||||
|
|
||||||
|
def save_runtime_settings(settings_payload: dict[str, Any]) -> None:
|
||||||
|
SETTINGS_FILE.write_text(
|
||||||
|
json.dumps(settings_payload, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_valid_http_url(url_text: Any) -> bool:
|
||||||
|
url = str(url_text or "").strip()
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
parsed = urlparse(url)
|
||||||
|
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
||||||
|
|
||||||
|
def get_linuxdo_oauth_config(*, mask_secret: bool = False) -> dict[str, Any]:
|
||||||
|
defaults = DEFAULT_SETTINGS.get("linuxdo_oauth", {})
|
||||||
|
runtime = load_runtime_settings().get("linuxdo_oauth", {})
|
||||||
|
raw = runtime if isinstance(runtime, dict) else {}
|
||||||
|
cfg = {
|
||||||
|
"enabled": bool(raw.get("enabled", defaults.get("enabled", False))),
|
||||||
|
"client_id": str(raw.get("client_id", defaults.get("client_id", ""))).strip(),
|
||||||
|
"client_secret": str(raw.get("client_secret", defaults.get("client_secret", ""))).strip(),
|
||||||
|
"authorize_url": str(raw.get("authorize_url", defaults.get("authorize_url", ""))).strip(),
|
||||||
|
"token_url": str(raw.get("token_url", defaults.get("token_url", ""))).strip(),
|
||||||
|
"userinfo_url": str(raw.get("userinfo_url", defaults.get("userinfo_url", ""))).strip(),
|
||||||
|
"scope": str(raw.get("scope", defaults.get("scope", ""))).strip(),
|
||||||
|
}
|
||||||
|
if mask_secret:
|
||||||
|
secret = cfg["client_secret"]
|
||||||
|
cfg["client_secret"] = "" if not secret else ("*" * 8)
|
||||||
|
cfg["client_secret_configured"] = bool(secret)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
def oauth_error_redirect(message: str) -> Any:
|
||||||
|
return redirect(url_for("login_page", oauth_error=message))
|
||||||
|
|
||||||
|
def resolve_linuxdo_identity(userinfo: dict[str, Any]) -> tuple[str, str]:
|
||||||
|
raw_id = userinfo.get("id")
|
||||||
|
if raw_id in {None, ""} and isinstance(userinfo.get("user"), dict):
|
||||||
|
raw_id = userinfo["user"].get("id")
|
||||||
|
if raw_id in {None, ""}:
|
||||||
|
raise ValueError("missing_user_id")
|
||||||
|
|
||||||
|
safe_id = re.sub(r"[^a-z0-9_]+", "_", str(raw_id).strip().lower()).strip("_") or "user"
|
||||||
|
username = f"linuxdo_{safe_id}"[:32]
|
||||||
|
if len(username) < 3:
|
||||||
|
username = "linuxdo_user"
|
||||||
|
|
||||||
|
display_name = str(
|
||||||
|
userinfo.get("name")
|
||||||
|
or userinfo.get("username")
|
||||||
|
or (userinfo.get("user", {}) if isinstance(userinfo.get("user"), dict) else {}).get("username")
|
||||||
|
or username
|
||||||
|
).strip()
|
||||||
|
return username, (display_name[:24] or username)
|
||||||
|
|
||||||
def current_user() -> dict[str, Any] | None:
|
def current_user() -> dict[str, Any] | None:
|
||||||
user_id = session.get("user_id")
|
user_id = session.get("user_id")
|
||||||
if not user_id:
|
if not user_id:
|
||||||
@@ -476,7 +553,8 @@ def create_app(config_object: type[Config] = Config) -> tuple[Flask, SocketIO]:
|
|||||||
|
|
||||||
@app.get("/login")
|
@app.get("/login")
|
||||||
def login_page() -> str:
|
def login_page() -> str:
|
||||||
return render_template("login.html", user=serialize_user(current_user()))
|
oauth_error = str(request.args.get("oauth_error", "")).strip()[:120]
|
||||||
|
return render_template("login.html", user=serialize_user(current_user()), oauth_error=oauth_error)
|
||||||
|
|
||||||
@app.get("/register")
|
@app.get("/register")
|
||||||
def register_page() -> str:
|
def register_page() -> str:
|
||||||
@@ -531,6 +609,103 @@ def create_app(config_object: type[Config] = Config) -> tuple[Flask, SocketIO]:
|
|||||||
session.pop("user_id", None)
|
session.pop("user_id", None)
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
@app.get("/api/auth/oauth/linuxdo/start")
|
||||||
|
def auth_oauth_linuxdo_start_api() -> Any:
|
||||||
|
cfg = get_linuxdo_oauth_config()
|
||||||
|
if not cfg.get("enabled"):
|
||||||
|
return jsonify({"ok": False, "error": "Linux DO OAuth 未启用。"}), 400
|
||||||
|
required_fields = ["client_id", "client_secret", "authorize_url", "token_url", "userinfo_url"]
|
||||||
|
missing = [field for field in required_fields if not str(cfg.get(field, "")).strip()]
|
||||||
|
if missing:
|
||||||
|
return jsonify({"ok": False, "error": f"Linux DO OAuth 配置缺失:{', '.join(missing)}"}), 400
|
||||||
|
for url_field in ("authorize_url", "token_url", "userinfo_url"):
|
||||||
|
if not _is_valid_http_url(cfg.get(url_field, "")):
|
||||||
|
return jsonify({"ok": False, "error": f"Linux DO OAuth 配置无效:{url_field}"}), 400
|
||||||
|
|
||||||
|
state = secrets.token_urlsafe(24)
|
||||||
|
session["linuxdo_oauth_state"] = state
|
||||||
|
callback_url = url_for("auth_oauth_linuxdo_callback_api", _external=True)
|
||||||
|
query = {
|
||||||
|
"client_id": cfg["client_id"],
|
||||||
|
"response_type": "code",
|
||||||
|
"redirect_uri": callback_url,
|
||||||
|
"state": state,
|
||||||
|
}
|
||||||
|
scope = str(cfg.get("scope", "")).strip()
|
||||||
|
if scope:
|
||||||
|
query["scope"] = scope
|
||||||
|
authorize_url = f"{cfg['authorize_url']}?{urlencode(query)}"
|
||||||
|
return redirect(authorize_url)
|
||||||
|
|
||||||
|
@app.get("/api/auth/oauth/linuxdo/callback")
|
||||||
|
def auth_oauth_linuxdo_callback_api() -> Any:
|
||||||
|
cfg = get_linuxdo_oauth_config()
|
||||||
|
if not cfg.get("enabled"):
|
||||||
|
return oauth_error_redirect("Linux DO OAuth 未启用")
|
||||||
|
|
||||||
|
state = str(request.args.get("state", "")).strip()
|
||||||
|
code = str(request.args.get("code", "")).strip()
|
||||||
|
expected_state = str(session.pop("linuxdo_oauth_state", "")).strip()
|
||||||
|
if not code:
|
||||||
|
return oauth_error_redirect("缺少授权 code")
|
||||||
|
if not state or state != expected_state:
|
||||||
|
return oauth_error_redirect("state 校验失败")
|
||||||
|
|
||||||
|
callback_url = url_for("auth_oauth_linuxdo_callback_api", _external=True)
|
||||||
|
try:
|
||||||
|
token_resp = requests.post(
|
||||||
|
cfg["token_url"],
|
||||||
|
data={
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"code": code,
|
||||||
|
"client_id": cfg["client_id"],
|
||||||
|
"client_secret": cfg["client_secret"],
|
||||||
|
"redirect_uri": callback_url,
|
||||||
|
},
|
||||||
|
timeout=12,
|
||||||
|
)
|
||||||
|
token_resp.raise_for_status()
|
||||||
|
token_payload = token_resp.json()
|
||||||
|
except Exception as exc:
|
||||||
|
return oauth_error_redirect(f"获取 access_token 失败: {exc}")
|
||||||
|
|
||||||
|
access_token = str(token_payload.get("access_token", "")).strip()
|
||||||
|
if not access_token:
|
||||||
|
return oauth_error_redirect("access_token 为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
userinfo_resp = requests.get(
|
||||||
|
cfg["userinfo_url"],
|
||||||
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
|
timeout=12,
|
||||||
|
)
|
||||||
|
userinfo_resp.raise_for_status()
|
||||||
|
userinfo = userinfo_resp.json()
|
||||||
|
if not isinstance(userinfo, dict):
|
||||||
|
return oauth_error_redirect("userinfo 响应格式无效")
|
||||||
|
except Exception as exc:
|
||||||
|
return oauth_error_redirect(f"获取用户信息失败: {exc}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
username, display_name = resolve_linuxdo_identity(userinfo)
|
||||||
|
except ValueError:
|
||||||
|
return oauth_error_redirect("用户信息缺少 id")
|
||||||
|
|
||||||
|
user = persistence.get_user_by_username(username)
|
||||||
|
if user is None:
|
||||||
|
random_password = secrets.token_urlsafe(32)
|
||||||
|
try:
|
||||||
|
user = persistence.create_user(
|
||||||
|
username=username,
|
||||||
|
password_hash=generate_password_hash(random_password, method="pbkdf2:sha256"),
|
||||||
|
display_name=display_name,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
return oauth_error_redirect(f"创建本地账号失败: {readable_error(str(exc))}")
|
||||||
|
|
||||||
|
session["user_id"] = user["user_id"]
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@app.get("/api/me")
|
@app.get("/api/me")
|
||||||
def me_api() -> Any:
|
def me_api() -> Any:
|
||||||
user = current_user()
|
user = current_user()
|
||||||
@@ -875,6 +1050,73 @@ def create_app(config_object: type[Config] = Config) -> tuple[Flask, SocketIO]:
|
|||||||
card["creator_display_name"] = creator["username"] if creator else ("官方" if card.get("is_official") else "玩家")
|
card["creator_display_name"] = creator["username"] if creator else ("官方" if card.get("is_official") else "玩家")
|
||||||
return jsonify({"ok": True, "cards": cards})
|
return jsonify({"ok": True, "cards": cards})
|
||||||
|
|
||||||
|
@app.get("/api/admin/settings/oauth/linuxdo")
|
||||||
|
def admin_linuxdo_oauth_get_api() -> Any:
|
||||||
|
_, error_resp = require_admin_json()
|
||||||
|
if error_resp:
|
||||||
|
return error_resp
|
||||||
|
return jsonify({"ok": True, "config": get_linuxdo_oauth_config(mask_secret=True)})
|
||||||
|
|
||||||
|
@app.patch("/api/admin/settings/oauth/linuxdo")
|
||||||
|
def admin_linuxdo_oauth_patch_api() -> Any:
|
||||||
|
_, error_resp = require_admin_json()
|
||||||
|
if error_resp:
|
||||||
|
return error_resp
|
||||||
|
|
||||||
|
payload = request.get_json(silent=True) or {}
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_settings = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
raw_settings = {}
|
||||||
|
if not isinstance(raw_settings, dict):
|
||||||
|
raw_settings = {}
|
||||||
|
|
||||||
|
defaults = DEFAULT_SETTINGS.get("linuxdo_oauth", {})
|
||||||
|
existing_raw = raw_settings.get("linuxdo_oauth", {})
|
||||||
|
existing_cfg = {**defaults, **(existing_raw if isinstance(existing_raw, dict) else {})}
|
||||||
|
|
||||||
|
enabled = bool(payload.get("enabled", existing_cfg.get("enabled", False)))
|
||||||
|
client_id = str(payload.get("client_id", existing_cfg.get("client_id", ""))).strip()
|
||||||
|
incoming_secret = payload.get("client_secret")
|
||||||
|
if incoming_secret is None:
|
||||||
|
client_secret = str(existing_cfg.get("client_secret", "")).strip()
|
||||||
|
else:
|
||||||
|
normalized_secret = str(incoming_secret).strip()
|
||||||
|
client_secret = normalized_secret or str(existing_cfg.get("client_secret", "")).strip()
|
||||||
|
authorize_url = str(payload.get("authorize_url", existing_cfg.get("authorize_url", ""))).strip()
|
||||||
|
token_url = str(payload.get("token_url", existing_cfg.get("token_url", ""))).strip()
|
||||||
|
userinfo_url = str(payload.get("userinfo_url", existing_cfg.get("userinfo_url", ""))).strip()
|
||||||
|
scope = str(payload.get("scope", existing_cfg.get("scope", ""))).strip()
|
||||||
|
|
||||||
|
for field, url_value in (
|
||||||
|
("authorize_url", authorize_url),
|
||||||
|
("token_url", token_url),
|
||||||
|
("userinfo_url", userinfo_url),
|
||||||
|
):
|
||||||
|
if not _is_valid_http_url(url_value):
|
||||||
|
return jsonify({"ok": False, "error": f"Linux DO OAuth 配置无效:{field}"}), 400
|
||||||
|
|
||||||
|
if enabled and not client_id:
|
||||||
|
return jsonify({"ok": False, "error": "Linux DO OAuth 配置缺失:client_id"}), 400
|
||||||
|
if enabled and not client_secret:
|
||||||
|
return jsonify({"ok": False, "error": "Linux DO OAuth 配置缺失:client_secret"}), 400
|
||||||
|
|
||||||
|
raw_settings["linuxdo_oauth"] = {
|
||||||
|
"enabled": enabled,
|
||||||
|
"client_id": client_id,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"authorize_url": authorize_url,
|
||||||
|
"token_url": token_url,
|
||||||
|
"userinfo_url": userinfo_url,
|
||||||
|
"scope": scope,
|
||||||
|
}
|
||||||
|
save_runtime_settings(raw_settings)
|
||||||
|
|
||||||
|
return jsonify({"ok": True, "config": get_linuxdo_oauth_config(mask_secret=True)})
|
||||||
|
|
||||||
@app.patch("/api/admin/cards/<card_id>")
|
@app.patch("/api/admin/cards/<card_id>")
|
||||||
def admin_patch_card_api(card_id: str) -> Any:
|
def admin_patch_card_api(card_id: str) -> Any:
|
||||||
_, error_resp = require_admin_json()
|
_, error_resp = require_admin_json()
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ DEFAULT_SETTINGS: dict[str, Any] = {
|
|||||||
"app_host": "0.0.0.0",
|
"app_host": "0.0.0.0",
|
||||||
"app_port": 8095,
|
"app_port": 8095,
|
||||||
"debug": True,
|
"debug": True,
|
||||||
|
"linuxdo_oauth": {
|
||||||
|
"enabled": False,
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": "",
|
||||||
|
"authorize_url": "https://connect.linux.do/oauth2/authorize",
|
||||||
|
"token_url": "https://connect.linux.do/oauth2/token",
|
||||||
|
"userinfo_url": "https://connect.linux.do/api/user",
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +58,12 @@ def _load_settings() -> dict[str, Any]:
|
|||||||
|
|
||||||
merged = dict(DEFAULT_SETTINGS)
|
merged = dict(DEFAULT_SETTINGS)
|
||||||
merged.update(payload)
|
merged.update(payload)
|
||||||
|
linuxdo_defaults = DEFAULT_SETTINGS["linuxdo_oauth"]
|
||||||
|
linuxdo_payload = payload.get("linuxdo_oauth", {})
|
||||||
|
if isinstance(linuxdo_payload, dict):
|
||||||
|
merged["linuxdo_oauth"] = {**linuxdo_defaults, **linuxdo_payload}
|
||||||
|
else:
|
||||||
|
merged["linuxdo_oauth"] = dict(linuxdo_defaults)
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|
||||||
@@ -82,6 +97,7 @@ class Config:
|
|||||||
APP_HOST = str(SETTINGS.get("app_host", DEFAULT_SETTINGS["app_host"]))
|
APP_HOST = str(SETTINGS.get("app_host", DEFAULT_SETTINGS["app_host"]))
|
||||||
APP_PORT = int(SETTINGS.get("app_port", DEFAULT_SETTINGS["app_port"]))
|
APP_PORT = int(SETTINGS.get("app_port", DEFAULT_SETTINGS["app_port"]))
|
||||||
DEBUG = bool(SETTINGS.get("debug", DEFAULT_SETTINGS["debug"]))
|
DEBUG = bool(SETTINGS.get("debug", DEFAULT_SETTINGS["debug"]))
|
||||||
|
LINUXDO_OAUTH = SETTINGS.get("linuxdo_oauth", DEFAULT_SETTINGS["linuxdo_oauth"])
|
||||||
|
|
||||||
|
|
||||||
class TestConfig(Config):
|
class TestConfig(Config):
|
||||||
|
|||||||
+10
-1
@@ -12,5 +12,14 @@
|
|||||||
"max_action_length": 280,
|
"max_action_length": 280,
|
||||||
"app_host": "0.0.0.0",
|
"app_host": "0.0.0.0",
|
||||||
"app_port": 8095,
|
"app_port": 8095,
|
||||||
"debug": true
|
"debug": true,
|
||||||
|
"linuxdo_oauth": {
|
||||||
|
"enabled": false,
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": "",
|
||||||
|
"authorize_url": "https://connect.linux.do/oauth2/authorize",
|
||||||
|
"token_url": "https://connect.linux.do/oauth2/token",
|
||||||
|
"userinfo_url": "https://connect.linux.do/api/user",
|
||||||
|
"scope": "read"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+52
-2
@@ -12,6 +12,7 @@
|
|||||||
const usersTable = document.getElementById('users-table');
|
const usersTable = document.getElementById('users-table');
|
||||||
const roomsTable = document.getElementById('rooms-table');
|
const roomsTable = document.getElementById('rooms-table');
|
||||||
const cardsTable = document.getElementById('cards-table');
|
const cardsTable = document.getElementById('cards-table');
|
||||||
|
const oauthForm = document.getElementById('linuxdo-oauth-form');
|
||||||
|
|
||||||
const ROLE_OPTIONS = ['player', 'moderator', 'admin'];
|
const ROLE_OPTIONS = ['player', 'moderator', 'admin'];
|
||||||
|
|
||||||
@@ -252,6 +253,54 @@
|
|||||||
cardsTable.appendChild(table);
|
cardsTable.appendChild(table);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadLinuxdoOauth = async () => {
|
||||||
|
if (!oauthForm) return true;
|
||||||
|
const data = await getJson('/api/admin/settings/oauth/linuxdo');
|
||||||
|
if (!data.ok) {
|
||||||
|
setStatus(data.error || '加载 OAuth 配置失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = data.config || {};
|
||||||
|
document.getElementById('linuxdo-enabled').value = cfg.enabled ? 'true' : 'false';
|
||||||
|
document.getElementById('linuxdo-client-id').value = cfg.client_id || '';
|
||||||
|
document.getElementById('linuxdo-client-secret').value = '';
|
||||||
|
document.getElementById('linuxdo-authorize-url').value = cfg.authorize_url || '';
|
||||||
|
document.getElementById('linuxdo-token-url').value = cfg.token_url || '';
|
||||||
|
document.getElementById('linuxdo-userinfo-url').value = cfg.userinfo_url || '';
|
||||||
|
document.getElementById('linuxdo-scope').value = cfg.scope || '';
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (oauthForm) {
|
||||||
|
oauthForm.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setStatus('正在保存 OAuth 配置...');
|
||||||
|
const payload = {
|
||||||
|
enabled: document.getElementById('linuxdo-enabled').value === 'true',
|
||||||
|
client_id: document.getElementById('linuxdo-client-id').value.trim(),
|
||||||
|
client_secret: document.getElementById('linuxdo-client-secret').value,
|
||||||
|
authorize_url: document.getElementById('linuxdo-authorize-url').value.trim(),
|
||||||
|
token_url: document.getElementById('linuxdo-token-url').value.trim(),
|
||||||
|
userinfo_url: document.getElementById('linuxdo-userinfo-url').value.trim(),
|
||||||
|
scope: document.getElementById('linuxdo-scope').value.trim(),
|
||||||
|
};
|
||||||
|
if (!payload.client_secret) {
|
||||||
|
delete payload.client_secret;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await patchJson('/api/admin/settings/oauth/linuxdo', payload);
|
||||||
|
if (!data.ok) {
|
||||||
|
setStatus(data.error || '保存失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('linuxdo-client-secret').value = '';
|
||||||
|
setStatus('OAuth 配置已保存。');
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`网络错误: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const loadAll = async () => {
|
const loadAll = async () => {
|
||||||
if (!user || user.role !== 'admin') {
|
if (!user || user.role !== 'admin') {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
@@ -259,12 +308,13 @@
|
|||||||
}
|
}
|
||||||
setStatus('正在加载后台数据...');
|
setStatus('正在加载后台数据...');
|
||||||
try {
|
try {
|
||||||
const [usersResp, roomsResp, cardsResp] = await Promise.all([
|
const [usersResp, roomsResp, cardsResp, oauthOk] = await Promise.all([
|
||||||
getJson('/api/admin/users'),
|
getJson('/api/admin/users'),
|
||||||
getJson('/api/admin/rooms'),
|
getJson('/api/admin/rooms'),
|
||||||
getJson('/api/admin/cards'),
|
getJson('/api/admin/cards'),
|
||||||
|
loadLinuxdoOauth(),
|
||||||
]);
|
]);
|
||||||
if (!usersResp.ok || !roomsResp.ok || !cardsResp.ok) {
|
if (!usersResp.ok || !roomsResp.ok || !cardsResp.ok || !oauthOk) {
|
||||||
const msg = usersResp.error || roomsResp.error || cardsResp.error || '加载失败';
|
const msg = usersResp.error || roomsResp.error || cardsResp.error || '加载失败';
|
||||||
setStatus(msg);
|
setStatus(msg);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -21,6 +21,37 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="admin-grid">
|
<main class="admin-grid">
|
||||||
|
<section class="card admin-panel">
|
||||||
|
<h2>Linux DO OAuth 配置</h2>
|
||||||
|
<form id="linuxdo-oauth-form">
|
||||||
|
<label for="linuxdo-enabled">启用 OAuth</label>
|
||||||
|
<select id="linuxdo-enabled" name="enabled">
|
||||||
|
<option value="false">关闭</option>
|
||||||
|
<option value="true">开启</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="linuxdo-client-id">Client ID</label>
|
||||||
|
<input id="linuxdo-client-id" name="client_id" placeholder="请输入 client_id" />
|
||||||
|
|
||||||
|
<label for="linuxdo-client-secret">Client Secret</label>
|
||||||
|
<input id="linuxdo-client-secret" name="client_secret" type="password" placeholder="留空则保持不变" />
|
||||||
|
|
||||||
|
<label for="linuxdo-authorize-url">Authorize URL</label>
|
||||||
|
<input id="linuxdo-authorize-url" name="authorize_url" placeholder="https://connect.linux.do/oauth2/authorize" />
|
||||||
|
|
||||||
|
<label for="linuxdo-token-url">Token URL</label>
|
||||||
|
<input id="linuxdo-token-url" name="token_url" placeholder="https://connect.linux.do/oauth2/token" />
|
||||||
|
|
||||||
|
<label for="linuxdo-userinfo-url">Userinfo URL</label>
|
||||||
|
<input id="linuxdo-userinfo-url" name="userinfo_url" placeholder="https://connect.linux.do/api/user" />
|
||||||
|
|
||||||
|
<label for="linuxdo-scope">Scope</label>
|
||||||
|
<input id="linuxdo-scope" name="scope" placeholder="read" />
|
||||||
|
|
||||||
|
<button id="linuxdo-save" type="submit" class="secondary-btn">保存 OAuth 配置</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card admin-panel">
|
<section class="card admin-panel">
|
||||||
<h2>用户权限管理</h2>
|
<h2>用户权限管理</h2>
|
||||||
<div id="users-table" class="table-wrap"></div>
|
<div id="users-table" class="table-wrap"></div>
|
||||||
|
|||||||
@@ -16,9 +16,10 @@
|
|||||||
<input id="password" name="password" type="password" required placeholder="至少 6 位" />
|
<input id="password" name="password" type="password" required placeholder="至少 6 位" />
|
||||||
<button type="submit">登录</button>
|
<button type="submit">登录</button>
|
||||||
</form>
|
</form>
|
||||||
|
<a class="link-btn" href="{{ url_for('auth_oauth_linuxdo_start_api') }}" style="margin-top: 0.75rem;">使用 Linux DO 登录</a>
|
||||||
<p class="auth-links">没有账号?<a href="{{ url_for('register_page') }}">去注册</a></p>
|
<p class="auth-links">没有账号?<a href="{{ url_for('register_page') }}">去注册</a></p>
|
||||||
<p class="auth-links"><a href="{{ url_for('index') }}">返回大厅</a></p>
|
<p class="auth-links"><a href="{{ url_for('index') }}">返回大厅</a></p>
|
||||||
<section id="status" class="status-banner"></section>
|
<section id="status" class="status-banner">{{ oauth_error or '' }}</section>
|
||||||
</main>
|
</main>
|
||||||
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
||||||
|
|||||||
Reference in new issue
Block a user