mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 11:25:48 +02:00
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:
@@ -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', {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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: 'ログアウト',
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user