Add TOTP authenticator support for Bambu Cloud login (fixes #182)

TOTP (Two-Factor Authentication):
- Detect TOTP vs email verification from Bambu API loginType response
- Use dedicated TFA endpoint on bambulab.com (not api.bambulab.com)
- Include browser-like headers to bypass Cloudflare protection
- Extract token from JSON response or cookies
- Frontend shows appropriate messages for each verification type
- Added i18n translations for TOTP UI (en, de, ja)

Closes #182
This commit is contained in:
maziggy
2026-02-03 07:18:13 +01:00
parent cc0bfa764e
commit ea93535bfc
12 changed files with 467 additions and 69 deletions
+4 -2
View File
@@ -769,6 +769,8 @@ export interface CloudLoginResponse {
success: boolean;
needs_verification: boolean;
message: string;
verification_type?: 'email' | 'totp' | null;
tfa_key?: string | null;
}
export interface SlicerSetting {
@@ -2620,10 +2622,10 @@ export const api = {
method: 'POST',
body: JSON.stringify({ email, password, region }),
}),
cloudVerify: (email: string, code: string) =>
cloudVerify: (email: string, code: string, tfaKey?: string) =>
request<CloudLoginResponse>('/cloud/verify', {
method: 'POST',
body: JSON.stringify({ email, code }),
body: JSON.stringify({ email, code, tfa_key: tfaKey }),
}),
cloudSetToken: (access_token: string) =>
request<CloudAuthStatus>('/cloud/token', {
+3
View File
@@ -1656,7 +1656,9 @@ export default {
regionGlobal: 'Global',
regionChina: 'China',
verificationCode: 'Bestätigungscode',
totpCode: 'Authenticator-Code',
checkEmail: 'Prüfen Sie Ihre E-Mail ({{email}}) für einen 6-stelligen Code',
enterTotpHint: 'Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein',
accessToken: 'Zugriffstoken',
accessTokenHint: 'Fügen Sie Ihr Bambu Lab Zugriffstoken ein (aus Bambu Studio)',
back: 'Zurück',
@@ -1668,6 +1670,7 @@ export default {
toast: {
loggedIn: 'Erfolgreich angemeldet',
codeSent: 'Bestätigungscode an Ihre E-Mail gesendet',
enterTotp: 'Geben Sie den Code aus Ihrer Authenticator-App ein',
tokenSet: 'Token erfolgreich gesetzt',
},
},
+3
View File
@@ -1656,7 +1656,9 @@ export default {
regionGlobal: 'Global',
regionChina: 'China',
verificationCode: 'Verification Code',
totpCode: 'Authenticator Code',
checkEmail: 'Check your email ({{email}}) for a 6-digit code',
enterTotpHint: 'Enter the 6-digit code from your authenticator app',
accessToken: 'Access Token',
accessTokenHint: 'Paste your Bambu Lab access token (from Bambu Studio)',
back: 'Back',
@@ -1668,6 +1670,7 @@ export default {
toast: {
loggedIn: 'Logged in successfully',
codeSent: 'Verification code sent to your email',
enterTotp: 'Enter code from your authenticator app',
tokenSet: 'Token set successfully',
},
},
+3
View File
@@ -731,7 +731,9 @@ export default {
regionGlobal: 'グローバル',
regionChina: '中国',
verificationCode: '認証コード',
totpCode: '認証アプリコード',
checkEmail: 'メール ({{email}}) に届いた6桁のコードを入力してください',
enterTotpHint: '認証アプリの6桁のコードを入力してください',
accessToken: 'アクセストークン',
accessTokenHint: 'Bambu Labのアクセストークンを貼り付け(Bambu Studioから取得)',
login: 'ログイン',
@@ -741,6 +743,7 @@ export default {
loginWithEmail: 'メールでログイン',
loggedIn: 'ログインしました',
verificationSent: '認証コードをメールに送信しました',
enterTotp: '認証アプリのコードを入力してください',
tokenSet: 'トークンを設定しました',
loggedOut: 'ログアウトしました',
logout: 'ログアウト',
+18 -4
View File
@@ -117,6 +117,8 @@ function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) {
const [code, setCode] = useState('');
const [token, setToken] = useState('');
const [region, setRegion] = useState('global');
const [verificationType, setVerificationType] = useState<'email' | 'totp' | null>(null);
const [tfaKey, setTfaKey] = useState<string | null>(null);
const loginMutation = useMutation({
mutationFn: () => api.cloudLogin(email, password, region),
@@ -125,7 +127,13 @@ function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) {
showToast(t('profiles.login.toast.loggedIn'));
onSuccess();
} else if (result.needs_verification) {
showToast(t('profiles.login.toast.codeSent'));
setVerificationType(result.verification_type || 'email');
setTfaKey(result.tfa_key || null);
if (result.verification_type === 'totp') {
showToast(t('profiles.login.toast.enterTotp'));
} else {
showToast(t('profiles.login.toast.codeSent'));
}
setStep('code');
} else {
showToast(result.message, 'error');
@@ -135,7 +143,7 @@ function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) {
});
const verifyMutation = useMutation({
mutationFn: () => api.cloudVerify(email, code),
mutationFn: () => api.cloudVerify(email, code, tfaKey || undefined),
onSuccess: (result) => {
if (result.success) {
showToast(t('profiles.login.toast.loggedIn'));
@@ -217,8 +225,14 @@ function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) {
{step === 'code' && (
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('profiles.login.verificationCode')}</label>
<p className="text-xs text-bambu-gray mb-2">{t('profiles.login.checkEmail', { email })}</p>
<label className="block text-sm text-bambu-gray mb-1">
{verificationType === 'totp' ? t('profiles.login.totpCode') : t('profiles.login.verificationCode')}
</label>
<p className="text-xs text-bambu-gray mb-2">
{verificationType === 'totp'
? t('profiles.login.enterTotpHint')
: t('profiles.login.checkEmail', { email })}
</p>
<input
type="text"
value={code}