security: API Key 改为仅存储在浏览器 localStorage,不再经过服务器

This commit is contained in:
zwbcc
2026-03-28 21:17:17 +08:00
parent 62f040bb8c
commit 667a147abc
3 changed files with 38 additions and 44 deletions

44
app.js
View File

@@ -7,13 +7,19 @@ const fs = require('fs');
const PORT = 8195;
const HOST = '0.0.0.0';
const app = express();
app.use(express.json());
app.use(express.static(__dirname));
// ── Config ──────────────────────────────────────────────────────
const CONFIG_FILE = path.join(__dirname, 'config.json');
function loadConfig() {
try {
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
} catch {
return { apiKey: '', baseUrl: 'https://api.minimaxi.com' };
return { baseUrl: 'https://api.minimaxi.com' };
}
}
@@ -21,23 +27,17 @@ function saveConfig(cfg) {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
}
const app = express();
app.use(express.json());
app.use(express.static(__dirname));
// ── Config ──────────────────────────────────────────────────────
app.get('/api/config', (req, res) => {
const cfg = loadConfig();
res.json({ hasApiKey: !!cfg.apiKey, baseUrl: cfg.baseUrl || 'https://api.minimaxi.com' });
res.json({ baseUrl: cfg.baseUrl || 'https://api.minimaxi.com' });
});
app.post('/api/config', (req, res) => {
const { apiKey, baseUrl } = req.body;
if (typeof apiKey !== 'string' || typeof baseUrl !== 'string') {
const { baseUrl } = req.body;
if (typeof baseUrl !== 'string') {
return res.status(400).json({ error: '参数格式错误' });
}
const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimaxi.com' };
const cfg = { baseUrl: baseUrl.trim() || 'https://api.minimaxi.com' };
saveConfig(cfg);
res.json({ ok: true });
});
@@ -46,9 +46,9 @@ app.post('/api/config', (req, res) => {
app.post('/api/generate', async (req, res) => {
const {
model, prompt, style, aspect_ratio,
model, prompt, aspect_ratio,
width, height, response_format, seed,
n, prompt_optimizer, aigc_watermark,
n, prompt_optimizer, aigc_watermark, apiKey,
} = req.body;
// Validation
@@ -58,9 +58,7 @@ app.post('/api/generate', async (req, res) => {
if (model !== 'image-01') {
return res.status(400).json({ error: 'model 参数无效' });
}
const cfg = loadConfig();
if (!cfg.apiKey) {
if (!apiKey || typeof apiKey !== 'string') {
return res.status(400).json({ error: '未配置 API Key请先在设置中填写。' });
}
@@ -78,7 +76,7 @@ app.post('/api/generate', async (req, res) => {
if (prompt_optimizer) payload.prompt_optimizer = true;
if (aigc_watermark) payload.aigc_watermark = true;
const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com';
const baseUrl = loadConfig().baseUrl || 'https://api.minimaxi.com';
const endpoint = `${baseUrl}/v1/image_generation`;
let response;
@@ -86,7 +84,7 @@ app.post('/api/generate', async (req, res) => {
response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${cfg.apiKey}`,
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -117,14 +115,14 @@ app.post('/api/generate', async (req, res) => {
// ── Task Status (future-proofing) ───────────────────────────────
app.get('/api/task/:id', async (req, res) => {
const cfg = loadConfig();
if (!cfg.apiKey) {
return res.status(400).json({ error: '未配置 API Key。' });
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(400).json({ error: '缺少 API Key。' });
}
const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com';
const baseUrl = loadConfig().baseUrl || 'https://api.minimaxi.com';
try {
const response = await fetch(`${baseUrl}/v1/image_generation/${req.params.id}`, {
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
headers: { 'Authorization': `Bearer ${apiKey}` },
});
const data = await response.json();
if (!response.ok) {

View File

@@ -262,7 +262,7 @@
</svg>
</button>
</div>
<p class="field-hint">请从 <a href="https://platform.minimaxi.com/user-center/payment/token-plan" target="_blank" rel="noopener">platform.minimaxi.com</a> 获取</p>
<p class="field-hint">存储在浏览器本地,不会上传服务器。<a href="https://platform.minimaxi.com/user-center/payment/token-plan" target="_blank" rel="noopener">获取 API Key →</a></p>
</div>
<div class="field">
<label for="baseUrlInput">API 地址</label>

36
ui.js
View File

@@ -89,11 +89,12 @@ function buildPayload() {
const height = heightInput.value ? parseInt(heightInput.value) : undefined;
const payload = {
model: currentModel,
prompt: promptInput.value.trim(),
aspect_ratio: aspectRatio.value,
model: currentModel,
prompt: promptInput.value.trim(),
aspect_ratio: aspectRatio.value,
response_format: responseFormat.value,
n: n,
n: n,
apiKey: localStorage.getItem('imgGen-apiKey') || '',
};
if (seed) payload.seed = seed;
@@ -179,11 +180,11 @@ async function copySrc(src, fmt, btn) {
// ── Init ────────────────────────────────────────────────────────
async function init() {
try {
const cfg = await api('/api/config');
if (!cfg.hasApiKey) apiKeyAlert.style.display = 'flex';
baseUrlInput.value = cfg.baseUrl || 'https://api.minimaxi.com';
} catch { /* non-fatal */ }
const savedKey = localStorage.getItem('imgGen-apiKey') || '';
const savedBaseUrl = localStorage.getItem('imgGen-baseUrl') || 'https://api.minimaxi.com';
if (!savedKey) apiKeyAlert.style.display = 'flex';
apiKeyInput.value = savedKey;
baseUrlInput.value = savedBaseUrl;
}
// ── Generate ────────────────────────────────────────────────────
@@ -269,17 +270,12 @@ saveSettingsBtn.addEventListener('click', async () => {
clearError();
const apiKey = apiKeyInput.value.trim();
const baseUrl = baseUrlInput.value.trim() || 'https://api.minimaxi.com';
try {
await api('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey, baseUrl }),
});
if (apiKey) apiKeyAlert.style.display = 'none';
closeModal();
} catch (err) {
showError(err.message);
}
localStorage.setItem('imgGen-apiKey', apiKey);
localStorage.setItem('imgGen-baseUrl', baseUrl);
if (apiKey) apiKeyAlert.style.display = 'none';
closeModal();
});
// ── Theme switcher ─────────────────────────────────────────────