feat: skin site UUID lookup - auto-fetch UUID from player name
- GET /settings/uuid-lookup?site=&name= : proxy Yggdrasil API
(POST {site}/api/yggdrasil/api/profiles/minecraft), SSRF guard,
10s timeout, name regex, UUID formatting (with dashes)
- GET /settings/skin-site : public read of configured skin site
- settings page: 皮肤站地址 config
- bind identity modals (player dashboard + admin users): 获取UUID button,
auto-fill site from settings, auto-fill UID from lookup
- verified live against littleskin.cn (Steve -> df273bda...)
- API docs updated
This commit is contained in:
@@ -43,4 +43,52 @@ router.put('/settings', authenticate, requireRole('owner','admin'), async (req,
|
||||
res.json({ message: '设置已保存' });
|
||||
});
|
||||
|
||||
// ---- 皮肤站 UUID 查询(Yggdrasil API: POST {site}/api/yggdrasil/api/profiles/minecraft) ----
|
||||
function formatUuid(id) {
|
||||
if (!id) return '';
|
||||
const s = String(id).replace(/-/g, '').toLowerCase();
|
||||
if (s.length !== 32) return String(id);
|
||||
return `${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20)}`;
|
||||
}
|
||||
|
||||
// 公开: 读取站点配置的皮肤站地址(玩家绑定身份时自动带出)
|
||||
router.get('/skin-site', async (req, res) => {
|
||||
try {
|
||||
const row = await getRow("SELECT v FROM settings WHERE k = 'skin_site'");
|
||||
res.json({ site: row?.v || '' });
|
||||
} catch { res.json({ site: '' }); }
|
||||
});
|
||||
|
||||
router.get('/uuid-lookup', authenticate, async (req, res) => {
|
||||
const { site, name } = req.query;
|
||||
if (!site || !name) return res.status(400).json({ error: '缺少参数 site 或 name' });
|
||||
if (!/^[A-Za-z0-9_]{1,32}$/.test(name)) return res.status(400).json({ error: '用户名仅允许字母数字下划线' });
|
||||
let u;
|
||||
try { u = new URL(site); } catch { return res.status(400).json({ error: '皮肤站地址无效' }); }
|
||||
if (u.protocol !== 'https:' && u.protocol !== 'http:') return res.status(400).json({ error: '协议不支持' });
|
||||
const host = u.hostname;
|
||||
if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' ||
|
||||
host.startsWith('192.168.') || host.startsWith('10.') || host.startsWith('172.16.')) {
|
||||
return res.status(400).json({ error: '不允许内网地址(SSRF 防护)' });
|
||||
}
|
||||
try {
|
||||
const ctl = new AbortController();
|
||||
const t = setTimeout(() => ctl.abort(), 10000);
|
||||
const resp = await fetch(`${u.origin}/api/yggdrasil/api/profiles/minecraft`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'MCReport/1.0' },
|
||||
body: JSON.stringify([name]),
|
||||
signal: ctl.signal,
|
||||
});
|
||||
clearTimeout(t);
|
||||
if (resp.status === 204) return res.json({ found: false, uuid: '', message: '皮肤站未找到该用户名' });
|
||||
const data = await resp.json();
|
||||
const hit = Array.isArray(data) ? data.find(x => x && x.name && String(x.name).toLowerCase() === name.toLowerCase()) : null;
|
||||
if (!hit?.id) return res.json({ found: false, uuid: '', message: '皮肤站未找到该用户名' });
|
||||
res.json({ found: true, uuid: formatUuid(hit.id), raw_id: String(hit.id).replace(/-/g, '') });
|
||||
} catch (e) {
|
||||
res.status(502).json({ error: '查询皮肤站失败: ' + (e.message || '网络错误') });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user