mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
This commit is contained in:
Generated
+10
@@ -28,6 +28,7 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.555.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
@@ -6396,6 +6397,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode.react": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.555.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Tests for the API-key QR payload builder.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildApiKeyQrPayload, API_KEY_QR_VERSION } from '../../utils/apiKeyQr';
|
||||
|
||||
describe('buildApiKeyQrPayload', () => {
|
||||
it('uses the bambuddy://config scheme with v first', () => {
|
||||
const payload = buildApiKeyQrPayload('https://printer.local', 'bb_abc123');
|
||||
expect(payload.startsWith(`bambuddy://config?v=${API_KEY_QR_VERSION}`)).toBe(true);
|
||||
});
|
||||
|
||||
it('encodes the url and key parameters', () => {
|
||||
const payload = buildApiKeyQrPayload('https://printer.local', 'bb_abc123');
|
||||
expect(payload).toBe('bambuddy://config?v=1&url=https%3A%2F%2Fprinter.local&key=bb_abc123');
|
||||
});
|
||||
|
||||
it('URL-encodes special characters in both values', () => {
|
||||
const baseUrl = 'http://host:5173/sub path';
|
||||
const key = 'bb_a+b/c=d&e';
|
||||
const payload = buildApiKeyQrPayload(baseUrl, key);
|
||||
|
||||
expect(payload).toContain(`url=${encodeURIComponent(baseUrl)}`);
|
||||
expect(payload).toContain(`key=${encodeURIComponent(key)}`);
|
||||
// The raw, unencoded key must never leak into the payload.
|
||||
expect(payload).not.toContain(key);
|
||||
});
|
||||
|
||||
it('round-trips the values back out of the query string', () => {
|
||||
const baseUrl = 'https://my.bambuddy.example:8443';
|
||||
const key = 'bb_ZZ/99+aa==';
|
||||
const payload = buildApiKeyQrPayload(baseUrl, key);
|
||||
|
||||
const query = payload.slice(payload.indexOf('?') + 1);
|
||||
const params = new URLSearchParams(query);
|
||||
expect(params.get('v')).toBe(String(API_KEY_QR_VERSION));
|
||||
expect(params.get('url')).toBe(baseUrl);
|
||||
expect(params.get('key')).toBe(key);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useEffect } from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { X, AlertTriangle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from './Button';
|
||||
import { buildApiKeyQrPayload } from '../utils/apiKeyQr';
|
||||
|
||||
interface ApiKeyQRCodeModalProps {
|
||||
/** Raw API key string (only available in-memory right after creation). */
|
||||
apiKey: string;
|
||||
/** Base URL a client uses to reach Bambuddy. Defaults to the current origin. */
|
||||
baseUrl?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ApiKeyQRCodeModal({ apiKey, baseUrl, onClose }: ApiKeyQRCodeModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const origin = baseUrl ?? window.location.origin;
|
||||
const payload = buildApiKeyQrPayload(origin, apiKey);
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary w-full max-w-sm"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-bambu-dark-tertiary">
|
||||
<h2 className="text-lg font-semibold text-white">{t('settings.apiKeyQrTitle')}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-bambu-gray hover:text-white transition-colors"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 flex flex-col items-center">
|
||||
<p className="text-sm text-bambu-gray mb-4 text-center">
|
||||
{t('settings.apiKeyQrCaption')}
|
||||
</p>
|
||||
<div className="bg-white p-4 rounded-lg mb-4">
|
||||
<QRCodeSVG value={payload} size={256} />
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-xs text-amber-400 mb-4">
|
||||
<AlertTriangle className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||
<span>{t('settings.apiKeyQrWarning')}</span>
|
||||
</div>
|
||||
<Button onClick={onClose} className="w-full">
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1843,6 +1843,10 @@ export default {
|
||||
apiKeyCreated: 'API-Schlüssel erfolgreich erstellt',
|
||||
apiKeyCopyWarning: 'Kopieren Sie diesen Schlüssel jetzt - er wird nicht mehr angezeigt!',
|
||||
useInApiBrowser: 'Im API-Browser verwenden',
|
||||
apiKeyQrButton: 'QR-Code',
|
||||
apiKeyQrTitle: 'Zum Einrichten scannen',
|
||||
apiKeyQrCaption: 'Mit deiner mobilen App scannen, um diesen Server und API-Schlüssel hinzuzufügen.',
|
||||
apiKeyQrWarning: 'Enthält deinen geheimen API-Schlüssel – nicht teilen oder dort abfotografieren, wo andere ihn sehen können.',
|
||||
createNewApiKey: 'Neuen API-Schlüssel erstellen',
|
||||
keyName: 'Schlüsselname',
|
||||
keyNamePlaceholder: 'z.B. Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1853,6 +1853,10 @@ export default {
|
||||
apiKeyCreated: 'API Key Created Successfully',
|
||||
apiKeyCopyWarning: "Copy this key now - it won't be shown again!",
|
||||
useInApiBrowser: 'Use in API Browser',
|
||||
apiKeyQrButton: 'QR code',
|
||||
apiKeyQrTitle: 'Scan to configure',
|
||||
apiKeyQrCaption: 'Scan with your mobile app to add this server and API key.',
|
||||
apiKeyQrWarning: "Contains your secret API key — don't share or screenshot it where others can see.",
|
||||
createNewApiKey: 'Create New API Key',
|
||||
keyName: 'Key Name',
|
||||
keyNamePlaceholder: 'e.g., Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1846,6 +1846,10 @@ export default {
|
||||
apiKeyCreated: 'Clave API creada correctamente',
|
||||
apiKeyCopyWarning: '¡Copie esta clave ahora; no se volverá a mostrar!',
|
||||
useInApiBrowser: 'Usar en el explorador de API',
|
||||
apiKeyQrButton: 'Código QR',
|
||||
apiKeyQrTitle: 'Escanea para configurar',
|
||||
apiKeyQrCaption: 'Escanea con tu app móvil para añadir este servidor y la clave API.',
|
||||
apiKeyQrWarning: 'Contiene tu clave API secreta: no la compartas ni hagas capturas donde otros puedan verla.',
|
||||
createNewApiKey: 'Crear nueva clave API',
|
||||
keyName: 'Nombre de la clave',
|
||||
keyNamePlaceholder: 'p. ej., Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1799,6 +1799,10 @@ export default {
|
||||
apiKeyCreated: 'Clé API créée avec succès',
|
||||
apiKeyCopyWarning: 'Copiez cette clé maintenant - elle ne sera plus affichée !',
|
||||
useInApiBrowser: 'Utiliser dans l\'explorateur API',
|
||||
apiKeyQrButton: 'Code QR',
|
||||
apiKeyQrTitle: 'Scanner pour configurer',
|
||||
apiKeyQrCaption: 'Scannez avec votre application mobile pour ajouter ce serveur et cette clé API.',
|
||||
apiKeyQrWarning: 'Contient votre clé API secrète — ne la partagez pas et ne la capturez pas là où d\'autres peuvent la voir.',
|
||||
createNewApiKey: 'Nouvelle clé API',
|
||||
keyName: 'Nom de la clé',
|
||||
keyNamePlaceholder: 'ex: Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1799,6 +1799,10 @@ export default {
|
||||
apiKeyCreated: 'Chiave API creata con successo',
|
||||
apiKeyCopyWarning: 'Copia questa chiave ora - non verra mostrata di nuovo!',
|
||||
useInApiBrowser: 'Usa nel Browser API',
|
||||
apiKeyQrButton: 'Codice QR',
|
||||
apiKeyQrTitle: 'Scansiona per configurare',
|
||||
apiKeyQrCaption: 'Scansiona con la tua app mobile per aggiungere questo server e la chiave API.',
|
||||
apiKeyQrWarning: 'Contiene la tua chiave API segreta: non condividerla né farne screenshot dove altri possono vederla.',
|
||||
createNewApiKey: 'Crea nuova chiave API',
|
||||
keyName: 'Nome chiave',
|
||||
keyNamePlaceholder: 'es., Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1842,6 +1842,10 @@ export default {
|
||||
apiKeyCreated: 'APIキーを作成しました',
|
||||
apiKeyCopyWarning: '今すぐこのキーをコピーしてください - 再表示されません!',
|
||||
useInApiBrowser: 'APIブラウザーで使用',
|
||||
apiKeyQrButton: 'QRコード',
|
||||
apiKeyQrTitle: 'スキャンして設定',
|
||||
apiKeyQrCaption: 'モバイルアプリでスキャンして、このサーバーとAPIキーを追加します。',
|
||||
apiKeyQrWarning: '秘密のAPIキーが含まれています。他人に見られる場所で共有・スクリーンショットしないでください。',
|
||||
createNewApiKey: '新しいAPIキーを作成',
|
||||
keyName: 'キー名',
|
||||
keyNamePlaceholder: '例: Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1729,6 +1729,10 @@ export default {
|
||||
apiKeyCreated: 'API 키가 성공적으로 생성되었습니다',
|
||||
apiKeyCopyWarning: '지금 이 키를 복사하세요 - 다시 표시되지 않습니다!',
|
||||
useInApiBrowser: 'API 브라우저에서 사용',
|
||||
apiKeyQrButton: 'QR 코드',
|
||||
apiKeyQrTitle: '스캔하여 설정',
|
||||
apiKeyQrCaption: '모바일 앱으로 스캔하여 이 서버와 API 키를 추가하세요.',
|
||||
apiKeyQrWarning: '비밀 API 키가 포함되어 있습니다. 다른 사람이 볼 수 있는 곳에서 공유하거나 스크린샷하지 마세요.',
|
||||
createNewApiKey: '새 API 키 만들기',
|
||||
keyName: '키 이름',
|
||||
keyNamePlaceholder: '예: Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1799,6 +1799,10 @@ export default {
|
||||
apiKeyCreated: 'Chave API criada com sucesso',
|
||||
apiKeyCopyWarning: 'Copie esta chave agora - ela não será exibida novamente!',
|
||||
useInApiBrowser: 'Usar no Navegador API',
|
||||
apiKeyQrButton: 'Código QR',
|
||||
apiKeyQrTitle: 'Escaneie para configurar',
|
||||
apiKeyQrCaption: 'Escaneie com seu app móvel para adicionar este servidor e a chave de API.',
|
||||
apiKeyQrWarning: 'Contém sua chave de API secreta — não compartilhe nem faça captura de tela onde outros possam ver.',
|
||||
createNewApiKey: 'Criar Nova Chave API',
|
||||
keyName: 'Nome da Chave',
|
||||
keyNamePlaceholder: 'e.g., Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1846,6 +1846,10 @@ export default {
|
||||
apiKeyCreated: 'API Anahtarı Başarıyla Oluşturuldu',
|
||||
apiKeyCopyWarning: 'Bu anahtarı şimdi kopyalayın - bir daha gösterilmeyecek!',
|
||||
useInApiBrowser: 'API Tarayıcısında Kullan',
|
||||
apiKeyQrButton: 'QR kodu',
|
||||
apiKeyQrTitle: 'Yapılandırmak için tarayın',
|
||||
apiKeyQrCaption: 'Bu sunucuyu ve API anahtarını eklemek için mobil uygulamanızla tarayın.',
|
||||
apiKeyQrWarning: 'Gizli API anahtarınızı içerir — başkalarının görebileceği yerlerde paylaşmayın veya ekran görüntüsü almayın.',
|
||||
createNewApiKey: 'Yeni API Anahtarı Oluştur',
|
||||
keyName: 'Anahtar Adı',
|
||||
keyNamePlaceholder: 'örn., Home Assistant, OctoPrint',
|
||||
|
||||
@@ -1844,6 +1844,10 @@ export default {
|
||||
apiKeyCreated: 'API 密钥创建成功',
|
||||
apiKeyCopyWarning: '请立即复制此密钥 - 它不会再次显示!',
|
||||
useInApiBrowser: '在 API 浏览器中使用',
|
||||
apiKeyQrButton: '二维码',
|
||||
apiKeyQrTitle: '扫码配置',
|
||||
apiKeyQrCaption: '使用手机应用扫描以添加此服务器和 API 密钥。',
|
||||
apiKeyQrWarning: '包含您的机密 API 密钥——请勿在他人可见的地方分享或截图。',
|
||||
createNewApiKey: '创建新 API 密钥',
|
||||
keyName: '密钥名称',
|
||||
keyNamePlaceholder: '例如:Home Assistant、OctoPrint',
|
||||
|
||||
@@ -1844,6 +1844,10 @@ export default {
|
||||
apiKeyCreated: 'API 金鑰建立成功',
|
||||
apiKeyCopyWarning: '請立即複製此金鑰 - 它不會再次顯示!',
|
||||
useInApiBrowser: '在 API 瀏覽器中使用',
|
||||
apiKeyQrButton: '二維碼',
|
||||
apiKeyQrTitle: '掃碼設定',
|
||||
apiKeyQrCaption: '使用手機應用程式掃描以新增此伺服器和 API 金鑰。',
|
||||
apiKeyQrWarning: '包含您的機密 API 金鑰——請勿在他人可見的地方分享或截圖。',
|
||||
createNewApiKey: '建立新 API 金鑰',
|
||||
keyName: '金鑰名稱',
|
||||
keyNamePlaceholder: '例如:Home Assistant、OctoPrint',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, Save, Mail, Flame, Layers, ListOrdered, Code, Search, Scale, Settings as SettingsIcon, ScanEye, Cog } from 'lucide-react';
|
||||
import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, Save, Mail, Flame, Layers, ListOrdered, Code, Search, Scale, Settings as SettingsIcon, ScanEye, Cog, QrCode } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { api } from '../api/client';
|
||||
@@ -21,6 +21,7 @@ import { AddNotificationModal } from '../components/AddNotificationModal';
|
||||
import { NotificationTemplateEditor } from '../components/NotificationTemplateEditor';
|
||||
import { NotificationLogViewer } from '../components/NotificationLogViewer';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
import { ApiKeyQRCodeModal } from '../components/ApiKeyQRCodeModal';
|
||||
import { CreateUserAdvancedAuthModal } from '../components/CreateUserAdvancedAuthModal';
|
||||
import { LdapUserPicker } from '../components/LdapUserPicker';
|
||||
import { SpoolmanSettings } from '../components/SpoolmanSettings';
|
||||
@@ -210,6 +211,7 @@ export function SettingsPage() {
|
||||
can_update_energy_cost: false,
|
||||
});
|
||||
const [createdAPIKey, setCreatedAPIKey] = useState<string | null>(null);
|
||||
const [showApiKeyQR, setShowApiKeyQR] = useState(false);
|
||||
const [showDeleteAPIKeyConfirm, setShowDeleteAPIKeyConfirm] = useState<number | null>(null);
|
||||
const [testApiKey, setTestApiKey] = useState('');
|
||||
|
||||
@@ -3657,7 +3659,18 @@ export function SettingsPage() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setCreatedAPIKey(null)}
|
||||
onClick={() => setShowApiKeyQR(true)}
|
||||
>
|
||||
<QrCode className="w-4 h-4" />
|
||||
{t('settings.apiKeyQrButton')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowApiKeyQR(false);
|
||||
setCreatedAPIKey(null);
|
||||
}}
|
||||
>
|
||||
{t('common.dismiss')}
|
||||
</Button>
|
||||
@@ -3668,6 +3681,16 @@ export function SettingsPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* QR code with base URL + key for mobile clients. Prefer the
|
||||
configured External URL; fall back to the current origin. */}
|
||||
{showApiKeyQR && createdAPIKey && (
|
||||
<ApiKeyQRCodeModal
|
||||
apiKey={createdAPIKey}
|
||||
baseUrl={localSettings?.external_url || undefined}
|
||||
onClose={() => setShowApiKeyQR(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Create Key Form */}
|
||||
{showCreateAPIKey && (
|
||||
<Card className="mb-6">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Helpers for the API-key QR code.
|
||||
*
|
||||
* The QR encodes the Bambuddy base URL and the freshly-created API key together
|
||||
* so a mobile client can scan one code to configure both.
|
||||
*
|
||||
* Payload contract (fixed — bump `v` if it changes):
|
||||
* bambuddy://config?v=1&url=<encodeURIComponent(baseUrl)>&key=<encodeURIComponent(apiKey)>
|
||||
*/
|
||||
|
||||
/** Current payload schema version. */
|
||||
export const API_KEY_QR_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Build the QR payload string encoding the base URL + API key.
|
||||
*
|
||||
* @param baseUrl Origin a client uses to reach Bambuddy (origin only, no path).
|
||||
* @param apiKey Raw API key string.
|
||||
*/
|
||||
export function buildApiKeyQrPayload(baseUrl: string, apiKey: string): string {
|
||||
return (
|
||||
`bambuddy://config?v=${API_KEY_QR_VERSION}` +
|
||||
`&url=${encodeURIComponent(baseUrl)}` +
|
||||
`&key=${encodeURIComponent(apiKey)}`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user