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

[feat] add support for did:web, add login process indicator

Changed files
+87 -5
src
components
auth
styles
+63 -5
src/components/auth/Login.tsx
··· 16 16 }>; 17 17 } 18 18 19 + type LoginStep = 'idle' | 'resolving-handle' | 'resolving-did' | 'connecting-pds' | 'authenticating' | 'success'; 20 + 19 21 export default function Login({ onLogin }: LoginProps) { 20 22 const [handle, setHandle] = useState(''); 21 23 const [password, setPassword] = useState(''); 22 24 const [error, setError] = useState(''); 23 25 const [appPasswordAttempts, setAppPasswordAttempts] = useState(0); 26 + const [loginStep, setLoginStep] = useState<LoginStep>('idle'); 24 27 const navigate = useNavigate(); 25 28 26 29 const isAppPassword = (password: string) => { ··· 28 31 return /^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/.test(password); 29 32 }; 30 33 34 + const getStepMessage = (step: LoginStep) => { 35 + switch (step) { 36 + case 'resolving-handle': 37 + return 'Resolving your handle...'; 38 + case 'resolving-did': 39 + return 'Resolving your DID...'; 40 + case 'connecting-pds': 41 + return 'Connecting to your Personal Data Server...'; 42 + case 'authenticating': 43 + return 'Authenticating your credentials...'; 44 + case 'success': 45 + return 'Login successful! Redirecting...'; 46 + default: 47 + return ''; 48 + } 49 + }; 50 + 31 51 const handleSubmit = async (e: React.FormEvent) => { 32 52 e.preventDefault(); 33 53 setError(''); 54 + setLoginStep('resolving-handle'); 34 55 56 + // app password check and debug method 35 57 if (isAppPassword(password)) { 36 58 if (appPasswordAttempts < 3) { 37 59 setAppPasswordAttempts(appPasswordAttempts + 1); 38 - setError(`Warning: You have entered an app password, which does not allow you to migrate your account.`); 60 + setError(`You have entered an app password, which does not allow for you to migrate your account. Please enter your main account password instead.`); 61 + setLoginStep('idle'); 39 62 return; 40 63 } 41 64 } 42 65 43 66 setHandle(handle.trim()); 44 - setPassword(password.trim()); 45 67 46 68 try { 47 69 // Create temporary agent to resolve DID 48 - const tempAgent = new AtpAgent({ service: 'https://bsky.social' }); 70 + const tempAgent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 49 71 50 72 // Get DID document from handle 73 + setLoginStep('resolving-handle'); 51 74 const didResponse = await tempAgent.com.atproto.identity.resolveHandle({ 52 75 handle: handle 53 76 }); 54 77 78 + if (!didResponse.success) { 79 + // Try did:web resolution first 80 + const domain = handle.split('.').join(':'); 81 + const webDid = `did:web:${domain}`; 82 + try { 83 + const webResponse = await fetch(`https://${handle}/.well-known/did.json`); 84 + if (webResponse.ok) { 85 + // If successful, continue with the did:web 86 + didResponse.data.did = webDid; 87 + } else { 88 + throw new Error('Invalid handle'); 89 + } 90 + } catch { 91 + throw new Error('Invalid handle'); 92 + } 93 + } 94 + 55 95 // Get PDS endpoint from DID document 96 + setLoginStep('resolving-did'); 56 97 let didDocResponse; 57 98 const did = didResponse.data.did; 58 99 ··· 72 113 }); 73 114 } 74 115 116 + setLoginStep('connecting-pds'); 75 117 const pds = ((didDocResponse.data as unknown) as DidDocument).service.find((s) => s.id === '#atproto_pds')?.serviceEndpoint || 'https://bsky.social'; 76 118 77 119 const agent = new AtpAgent({ service: pds }); 120 + 121 + setLoginStep('authenticating'); 78 122 await agent.login({ identifier: handle, password }); 123 + 124 + setLoginStep('success'); 79 125 onLogin(agent); 80 126 navigate('/actions'); 81 127 } catch (err) { 82 128 setError(err instanceof Error ? err.message : 'Login failed'); 129 + setLoginStep('idle'); 83 130 } 84 131 }; 85 132 ··· 101 148 placeholder="Handle (e.g., example.bsky.social)" 102 149 value={handle} 103 150 onChange={(e) => setHandle(e.target.value)} 151 + disabled={loginStep !== 'idle'} 104 152 /> 105 153 </div> 106 154 <div className="form-group"> ··· 111 159 placeholder="Password" 112 160 value={password} 113 161 onChange={(e) => setPassword(e.target.value)} 162 + disabled={loginStep !== 'idle'} 114 163 /> 115 164 </div> 116 165 117 166 {error && <div className="error-message">{error}</div>} 167 + {loginStep !== 'idle' && ( 168 + <div className="loading-message"> 169 + {getStepMessage(loginStep)} 170 + </div> 171 + )} 118 172 119 - <button type="submit" className="submit-button"> 120 - Sign in 173 + <button 174 + type="submit" 175 + className="submit-button" 176 + disabled={loginStep !== 'idle'} 177 + > 178 + {loginStep === 'idle' ? 'Sign in' : 'Signing in...'} 121 179 </button> 122 180 </form> 123 181 </div>
+24
src/styles/App.css
··· 421 421 422 422 .footer-link strong { 423 423 font-weight: 600; 424 + } 425 + 426 + .loading-message { 427 + margin: 10px 0; 428 + padding: 10px; 429 + background-color: var(--bg-color); 430 + border-radius: 4px; 431 + color: var(--text-color); 432 + font-size: 0.9em; 433 + text-align: center; 434 + } 435 + 436 + .form-input:disabled { 437 + background-color: var(--bg-color); 438 + border-color: var(--border-color); 439 + color: var(--text-light); 440 + cursor: not-allowed; 441 + opacity: 0.8; 442 + } 443 + 444 + .submit-button:disabled { 445 + background-color: var(--text-light); 446 + cursor: not-allowed; 447 + opacity: 0.8; 424 448 }