diff --git a/src/__tests__/auth/login-page.test.tsx b/src/__tests__/auth/login-page.test.tsx index 09c569e..5d44255 100644 --- a/src/__tests__/auth/login-page.test.tsx +++ b/src/__tests__/auth/login-page.test.tsx @@ -123,6 +123,72 @@ describe('LoginPage', () => { expect(mockLogin).toHaveBeenCalledWith('test.bsky.social') }) + it('strips at:// prefix from AT-URI with handle', async () => { + const user = userEvent.setup() + render() + + await user.type(screen.getByLabelText(/handle/i), 'at://ngerakines.me') + await user.click(screen.getByRole('button', { name: /continue/i })) + + expect(mockLogin).toHaveBeenCalledWith('ngerakines.me') + }) + + it('strips at:// prefix from AT-URI with DID', async () => { + const user = userEvent.setup() + render() + + await user.type(screen.getByLabelText(/handle/i), 'at://did:plc:cbkjy5n7bk3ax2wplmtjofq2') + await user.click(screen.getByRole('button', { name: /continue/i })) + + expect(mockLogin).toHaveBeenCalledWith('did:plc:cbkjy5n7bk3ax2wplmtjofq2') + }) + + it('passes plain DID through without lowercasing', async () => { + const user = userEvent.setup() + render() + + await user.type(screen.getByLabelText(/handle/i), 'did:plc:CbKjY5N7Bk3Ax2WplmTjOfQ2') + await user.click(screen.getByRole('button', { name: /continue/i })) + + expect(mockLogin).toHaveBeenCalledWith('did:plc:CbKjY5N7Bk3Ax2WplmTjOfQ2') + }) + + it('passes did:web identifier through without lowercasing', async () => { + const user = userEvent.setup() + render() + + await user.type(screen.getByLabelText(/handle/i), 'did:web:Example.Com') + await user.click(screen.getByRole('button', { name: /continue/i })) + + expect(mockLogin).toHaveBeenCalledWith('did:web:Example.Com') + }) + + it('extracts DID from bsky.app profile URL', async () => { + const user = userEvent.setup() + render() + + await user.type( + screen.getByLabelText(/handle/i), + 'https://bsky.app/profile/did:plc:cbkjy5n7bk3ax2wplmtjofq2' + ) + await user.click(screen.getByRole('button', { name: /continue/i })) + + expect(mockLogin).toHaveBeenCalledWith('did:plc:cbkjy5n7bk3ax2wplmtjofq2') + }) + + it('strips AT-URI path segments after authority', async () => { + const user = userEvent.setup() + render() + + await user.type( + screen.getByLabelText(/handle/i), + 'at://ngerakines.me/app.bsky.feed.post/abc123' + ) + await user.click(screen.getByRole('button', { name: /continue/i })) + + expect(mockLogin).toHaveBeenCalledWith('ngerakines.me') + }) + it('shows user-friendly error for unknown handle (502)', async () => { const { ApiError } = await import('@/lib/api/client') mockLogin.mockRejectedValueOnce(new ApiError(502, 'API 502: Bad Gateway')) diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4840a78..42e6c0e 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -38,13 +38,21 @@ function LoginContent() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - const trimmed = handle + let identifier = handle .trim() .replace(/^https?:\/\/bsky\.app\/profile\//i, '') + .replace(/^at:\/\//i, '') .replace(/^@/, '') - .replace(/\.$/, '') - .toLowerCase() - if (!trimmed) { + // Strip AT-URI path segments (e.g. "handle/app.bsky.feed.post/abc" → "handle") + if (!identifier.startsWith('did:')) { + identifier = identifier.split('/')[0] ?? identifier + } + identifier = identifier.replace(/\.$/, '') + // Only lowercase handles -- DIDs are case-sensitive + if (!identifier.startsWith('did:')) { + identifier = identifier.toLowerCase() + } + if (!identifier) { setError('Please enter your handle') return } @@ -55,11 +63,11 @@ function LoginContent() { try { // Store returnTo so callback can redirect back sessionStorage.setItem('auth_returnTo', returnTo) - await login(trimmed) + await login(identifier) } catch (err) { if (err instanceof ApiError && err.status === 502) { setError( - `We couldn't find an account for "${trimmed}". Please check for typos and try again.` + `We couldn't find an account for "${identifier}". Please check for typos and try again.` ) } else { setError(err instanceof Error ? err.message : 'Failed to start login') @@ -112,7 +120,7 @@ function LoginContent() {
- Handle + Handle or DID

- Your Bluesky or AT Protocol handle (e.g. jay.bsky.team) + Handle, DID, or AT Protocol URI (e.g. jay.bsky.team)