+56
src/css/login.css
+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
+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
);