refactor: session-based external auth, dynamic sources, drop netease UID

Auth (external API):
- ID: 16-digit random (non-sequential); Secret: SeaReport- + 32 hex
- POST /auth/session: ID+Secret -> Bearer SESSION (24h, single-session,
  old session invalidated on re-issue, disabled client invalidates)
- clientAuth now validates Bearer SESSION via api_sessions JOIN api_clients

Sources (dynamic, no default, open-source friendly):
- sources table + CRUD route (/api/sources, owner; delete guarded by usage)
- users/user_identities.source ENUM -> VARCHAR, seeded netease/skin
- register/admin create/identity bind: validate against enabled sources
- UI: 来源管理 page; source dropdowns loaded dynamically everywhere
  (register, dashboard identity, users admin, bans), labels dynamic

UID removal:
- game_uid/reporter_game_uid no longer required (db default '', validations
  dropped, frontend fields optional)

Docs: EXTERNAL-API.md session flow + new credential format; API.md updated
Verified: 37 checks (syntax, session logic, source CRUD, UID removal, docs)
This commit is contained in:
2026-08-19 20:08:06 +08:00
parent 6056153f57
commit 65edbaf157
19 changed files with 497 additions and 103 deletions

View File

@@ -75,11 +75,11 @@ async function initSchema(connection) {
password VARCHAR(255) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
game_name VARCHAR(50) NOT NULL,
game_uid VARCHAR(50) NOT NULL,
game_uid VARCHAR(50) NOT NULL DEFAULT '',
role ENUM('owner','admin','player') NOT NULL DEFAULT 'player',
active TINYINT(1) NOT NULL DEFAULT 0,
email_verified TINYINT(1) NOT NULL DEFAULT 0,
source ENUM('netease','skin') NOT NULL DEFAULT 'netease',
source VARCHAR(20) NOT NULL DEFAULT '',
verify_token VARCHAR(255),
verify_expires DATETIME,
reset_token VARCHAR(255),
@@ -91,7 +91,7 @@ async function initSchema(connection) {
await createIfNotExists('user_identities', `CREATE TABLE user_identities (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
source ENUM('netease','skin') NOT NULL,
source VARCHAR(20) NOT NULL,
game_name VARCHAR(50) NOT NULL,
game_uid VARCHAR(50) NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -99,6 +99,25 @@ async function initSchema(connection) {
INDEX idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('sources', `CREATE TABLE sources (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL UNIQUE,
label VARCHAR(50) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('api_sessions', `CREATE TABLE api_sessions (
id INT AUTO_INCREMENT PRIMARY KEY,
client_id VARCHAR(64) NOT NULL,
session_token VARCHAR(128) NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_client (client_id),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('tickets', `CREATE TABLE tickets (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
@@ -108,7 +127,7 @@ async function initSchema(connection) {
status ENUM('pending','processing','awaiting_info','appealing','resolved','rejected','closed') NOT NULL DEFAULT 'pending',
priority ENUM('low','medium','high','urgent') NOT NULL DEFAULT 'medium',
reporter_game_name VARCHAR(50) NOT NULL,
reporter_game_uid VARCHAR(50) NOT NULL,
reporter_game_uid VARCHAR(50) NOT NULL DEFAULT '',
target_game_name VARCHAR(50),
target_game_uid VARCHAR(50),
reason VARCHAR(500),
@@ -391,6 +410,20 @@ async function migrateAdditions(db) {
try { await db.execute(`CREATE TABLE IF NOT EXISTS api_clients (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, client_id VARCHAR(64) NOT NULL UNIQUE, secret_hash VARCHAR(255) NOT NULL, active TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_active (active)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try { await db.execute("ALTER TABLE tickets ADD COLUMN server_name VARCHAR(100) NOT NULL DEFAULT ''"); } catch {}
try { await db.execute("ALTER TABLE bans ADD COLUMN server_name VARCHAR(100) NOT NULL DEFAULT ''"); } catch {}
// ---- 动态来源: sources 表 + users/user_identities.source 改 VARCHAR + game_uid 可空 ----
try { await db.execute(`CREATE TABLE IF NOT EXISTS sources (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) NOT NULL UNIQUE, label VARCHAR(50) NOT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try {
// 预置来源(幂等), 老数据来源保持兼容
await db.execute("INSERT IGNORE INTO sources(code, label, sort_order) VALUES ('netease','网易端',1), ('skin','皮肤站',2)");
} catch {}
try { await db.execute("ALTER TABLE users MODIFY source VARCHAR(20) NOT NULL DEFAULT ''"); } catch {}
try { await db.execute("ALTER TABLE users MODIFY game_uid VARCHAR(50) NOT NULL DEFAULT ''"); } catch {}
try { await db.execute("ALTER TABLE user_identities MODIFY source VARCHAR(20) NOT NULL"); } catch {}
try { await db.execute("ALTER TABLE tickets MODIFY reporter_game_uid VARCHAR(50) NOT NULL DEFAULT ''"); } catch {}
// ---- API SESSION 表 ----
try { await db.execute(`CREATE TABLE IF NOT EXISTS api_sessions (id INT AUTO_INCREMENT PRIMARY KEY, client_id VARCHAR(64) NOT NULL, session_token VARCHAR(128) NOT NULL UNIQUE, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_client (client_id), INDEX idx_expires (expires_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
}
async function seedTemplates(db) {