ai-generated junk tool for migrating atproto identities in-browser

[feat] add 2fa compatibility

penny 39c09641 edcc1d77

Changed files
+146 -9
src
css
pages
+56
src/css/login.css
··· 81 81 border-radius: 0 0.375rem 0.375rem 0; 82 82 display: flex; 83 83 align-items: center; 84 + } 85 + 86 + /* 2FA Modal styles */ 87 + .modal-overlay { 88 + position: fixed; 89 + top: 0; 90 + left: 0; 91 + right: 0; 92 + bottom: 0; 93 + background-color: rgba(0, 0, 0, 0.75); 94 + display: flex; 95 + justify-content: center; 96 + align-items: center; 97 + z-index: 1000; 98 + } 99 + 100 + .modal-content { 101 + background-color: var(--white); 102 + padding: 2rem; 103 + border-radius: 0.5rem; 104 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 105 + width: 90%; 106 + max-width: 28rem; 107 + } 108 + 109 + .modal-content h2 { 110 + margin-top: 0; 111 + margin-bottom: 1rem; 112 + color: var(--text-color); 113 + text-align: center; 114 + } 115 + 116 + .modal-content p { 117 + margin-bottom: 1.5rem; 118 + color: var(--text-light); 119 + text-align: center; 120 + } 121 + 122 + .two-factor-form { 123 + margin-top: 2rem; 124 + } 125 + 126 + .button-group { 127 + display: flex; 128 + gap: 1rem; 129 + margin-top: 1rem; 130 + } 131 + 132 + .button-group .submit-button { 133 + margin-top: 0; 134 + flex: 1; 135 + width: fit-content; 136 + } 137 + 138 + .button-group .back-button { 139 + flex: 1; 84 140 }
+90 -9
src/pages/Login.tsx
··· 15 15 }>; 16 16 } 17 17 18 - type LoginStep = 'idle' | 'resolving-handle' | 'resolving-did' | 'connecting-pds' | 'authenticating' | 'success'; 18 + type LoginStep = 'idle' | 'resolving-handle' | 'resolving-did' | 'connecting-pds' | 'authenticating' | '2fa-required' | 'success'; 19 19 20 20 export default function Login({ onLogin }: LoginProps) { 21 21 const [handle, setHandle] = useState(''); 22 22 const [password, setPassword] = useState(''); 23 + const [twoFactorCode, setTwoFactorCode] = useState(''); 23 24 const [error, setError] = useState(''); 24 25 const [appPasswordAttempts, setAppPasswordAttempts] = useState(0); 25 26 const [loginStep, setLoginStep] = useState<LoginStep>('idle'); 27 + const [agent, setAgent] = useState<AtpAgent | null>(null); 26 28 const navigate = useNavigate(); 27 29 28 30 const isAppPassword = (password: string) => { ··· 40 42 return 'Connecting to your Personal Data Server...'; 41 43 case 'authenticating': 42 44 return 'Authenticating your credentials...'; 45 + case '2fa-required': 46 + return 'Please enter your 2FA code'; 43 47 case 'success': 44 48 return 'Login successful! Redirecting...'; 45 49 default: ··· 50 54 const handleSubmit = async (e: React.FormEvent) => { 51 55 e.preventDefault(); 52 56 setError(''); 57 + 58 + // If we're in 2FA step, handle that separately 59 + if (loginStep === '2fa-required') { 60 + if (!agent) { 61 + setError('Session expired. Please try logging in again.'); 62 + setLoginStep('idle'); 63 + return; 64 + } 65 + 66 + try { 67 + setLoginStep('authenticating'); 68 + await agent.login({ identifier: handle, password, authFactorToken: twoFactorCode }); 69 + setLoginStep('success'); 70 + onLogin(agent); 71 + navigate('/actions'); 72 + } catch (err) { 73 + setError(err instanceof Error ? err.message : '2FA verification failed'); 74 + setLoginStep('2fa-required'); 75 + } 76 + return; 77 + } 78 + 53 79 setLoginStep('resolving-handle'); 54 80 55 81 // app password check and debug method ··· 115 141 setLoginStep('connecting-pds'); 116 142 const pds = ((didDocResponse.data as unknown) as DidDocument).service.find((s) => s.id === '#atproto_pds')?.serviceEndpoint || 'https://bsky.social'; 117 143 118 - const agent = new AtpAgent({ service: pds }); 144 + const newAgent = new AtpAgent({ service: pds }); 145 + setAgent(newAgent); 119 146 120 147 setLoginStep('authenticating'); 121 - await agent.login({ identifier: handle, password }); 122 - 123 - setLoginStep('success'); 124 - onLogin(agent); 125 - navigate('/actions'); 148 + try { 149 + await newAgent.login({ identifier: handle, password }); 150 + setLoginStep('success'); 151 + onLogin(newAgent); 152 + navigate('/actions'); 153 + } catch (err) { 154 + if (err instanceof Error && ( 155 + err.message.includes('AuthFactorTokenRequired') || 156 + err.message.includes('A sign in code has been sent to your email address') 157 + )) { 158 + setLoginStep('2fa-required'); 159 + return; 160 + } 161 + throw err; 162 + } 126 163 } catch (err) { 127 164 setError(err instanceof Error ? err.message : 'Login failed'); 128 165 setLoginStep('idle'); ··· 136 173 <div className="login-card"> 137 174 <h2 className="login-title">Sign in to your account</h2> 138 175 <div className="warning-message"> 139 - ⚠️ Please use your main account password, not an app password. You will also have to temporarily disable 2FA. All operations are performed locally in your browser. 176 + ⚠️ Please use your main account password, not an app password. All operations are performed locally in your browser. 140 177 </div> 141 178 <form className="login-form" onSubmit={handleSubmit}> 142 179 <div className="form-group"> ··· 163 200 </div> 164 201 165 202 {error && <div className="error-message">{error}</div>} 166 - {loginStep !== 'idle' && ( 203 + {loginStep !== 'idle' && loginStep !== '2fa-required' && ( 167 204 <div className="loading-message"> 168 205 {getStepMessage(loginStep)} 169 206 </div> ··· 179 216 </form> 180 217 </div> 181 218 </div> 219 + 220 + {/* 2FA Modal */} 221 + {loginStep === '2fa-required' && ( 222 + <div className="modal-overlay"> 223 + <div className="modal-content"> 224 + <h2>Two-Factor Authentication Required</h2> 225 + <p>A sign in code has been sent to your email address.</p> 226 + <form onSubmit={handleSubmit} className="two-factor-form"> 227 + <div className="form-group"> 228 + <input 229 + type="text" 230 + required 231 + className="form-input" 232 + placeholder="Enter 2FA code" 233 + value={twoFactorCode} 234 + onChange={(e) => setTwoFactorCode(e.target.value)} 235 + autoFocus 236 + /> 237 + </div> 238 + {error && <div className="error-message">{error}</div>} 239 + <div className="button-group"> 240 + <button 241 + type="button" 242 + className="back-button" 243 + onClick={() => { 244 + setLoginStep('idle'); 245 + setError(''); 246 + setTwoFactorCode(''); 247 + }} 248 + > 249 + Cancel 250 + </button> 251 + <button 252 + type="submit" 253 + className="submit-button" 254 + > 255 + Verify 256 + </button> 257 + </div> 258 + </form> 259 + </div> 260 + </div> 261 + )} 262 + 182 263 <Footer /> 183 264 </div> 184 265 );