Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(auth): accept DID and AT-URI formats in login input (#178)

* feat(auth): accept DID and AT-URI formats in login input

Login field now normalizes additional AT Protocol identifier formats:
- at://handle and at://did:... (strips at:// prefix)
- did:plc:... and did:web:... (passed through without lowercasing)
- AT-URIs with path segments (extracts authority only)
- bsky.app profile URLs with DIDs

DIDs are kept case-sensitive since the DID spec requires it.
Updates label and helper text to reflect broader input support.

* style(auth): fix prettier formatting in login tests

authored by

Guido X Jansen and committed by
GitHub
842dd249 002a86eb

+82 -8
+66
src/__tests__/auth/login-page.test.tsx
··· 123 123 expect(mockLogin).toHaveBeenCalledWith('test.bsky.social') 124 124 }) 125 125 126 + it('strips at:// prefix from AT-URI with handle', async () => { 127 + const user = userEvent.setup() 128 + render(<LoginPage />) 129 + 130 + await user.type(screen.getByLabelText(/handle/i), 'at://ngerakines.me') 131 + await user.click(screen.getByRole('button', { name: /continue/i })) 132 + 133 + expect(mockLogin).toHaveBeenCalledWith('ngerakines.me') 134 + }) 135 + 136 + it('strips at:// prefix from AT-URI with DID', async () => { 137 + const user = userEvent.setup() 138 + render(<LoginPage />) 139 + 140 + await user.type(screen.getByLabelText(/handle/i), 'at://did:plc:cbkjy5n7bk3ax2wplmtjofq2') 141 + await user.click(screen.getByRole('button', { name: /continue/i })) 142 + 143 + expect(mockLogin).toHaveBeenCalledWith('did:plc:cbkjy5n7bk3ax2wplmtjofq2') 144 + }) 145 + 146 + it('passes plain DID through without lowercasing', async () => { 147 + const user = userEvent.setup() 148 + render(<LoginPage />) 149 + 150 + await user.type(screen.getByLabelText(/handle/i), 'did:plc:CbKjY5N7Bk3Ax2WplmTjOfQ2') 151 + await user.click(screen.getByRole('button', { name: /continue/i })) 152 + 153 + expect(mockLogin).toHaveBeenCalledWith('did:plc:CbKjY5N7Bk3Ax2WplmTjOfQ2') 154 + }) 155 + 156 + it('passes did:web identifier through without lowercasing', async () => { 157 + const user = userEvent.setup() 158 + render(<LoginPage />) 159 + 160 + await user.type(screen.getByLabelText(/handle/i), 'did:web:Example.Com') 161 + await user.click(screen.getByRole('button', { name: /continue/i })) 162 + 163 + expect(mockLogin).toHaveBeenCalledWith('did:web:Example.Com') 164 + }) 165 + 166 + it('extracts DID from bsky.app profile URL', async () => { 167 + const user = userEvent.setup() 168 + render(<LoginPage />) 169 + 170 + await user.type( 171 + screen.getByLabelText(/handle/i), 172 + 'https://bsky.app/profile/did:plc:cbkjy5n7bk3ax2wplmtjofq2' 173 + ) 174 + await user.click(screen.getByRole('button', { name: /continue/i })) 175 + 176 + expect(mockLogin).toHaveBeenCalledWith('did:plc:cbkjy5n7bk3ax2wplmtjofq2') 177 + }) 178 + 179 + it('strips AT-URI path segments after authority', async () => { 180 + const user = userEvent.setup() 181 + render(<LoginPage />) 182 + 183 + await user.type( 184 + screen.getByLabelText(/handle/i), 185 + 'at://ngerakines.me/app.bsky.feed.post/abc123' 186 + ) 187 + await user.click(screen.getByRole('button', { name: /continue/i })) 188 + 189 + expect(mockLogin).toHaveBeenCalledWith('ngerakines.me') 190 + }) 191 + 126 192 it('shows user-friendly error for unknown handle (502)', async () => { 127 193 const { ApiError } = await import('@/lib/api/client') 128 194 mockLogin.mockRejectedValueOnce(new ApiError(502, 'API 502: Bad Gateway'))
+16 -8
src/app/login/page.tsx
··· 38 38 39 39 const handleSubmit = async (e: React.FormEvent) => { 40 40 e.preventDefault() 41 - const trimmed = handle 41 + let identifier = handle 42 42 .trim() 43 43 .replace(/^https?:\/\/bsky\.app\/profile\//i, '') 44 + .replace(/^at:\/\//i, '') 44 45 .replace(/^@/, '') 45 - .replace(/\.$/, '') 46 - .toLowerCase() 47 - if (!trimmed) { 46 + // Strip AT-URI path segments (e.g. "handle/app.bsky.feed.post/abc" → "handle") 47 + if (!identifier.startsWith('did:')) { 48 + identifier = identifier.split('/')[0] ?? identifier 49 + } 50 + identifier = identifier.replace(/\.$/, '') 51 + // Only lowercase handles -- DIDs are case-sensitive 52 + if (!identifier.startsWith('did:')) { 53 + identifier = identifier.toLowerCase() 54 + } 55 + if (!identifier) { 48 56 setError('Please enter your handle') 49 57 return 50 58 } ··· 55 63 try { 56 64 // Store returnTo so callback can redirect back 57 65 sessionStorage.setItem('auth_returnTo', returnTo) 58 - await login(trimmed) 66 + await login(identifier) 59 67 } catch (err) { 60 68 if (err instanceof ApiError && err.status === 502) { 61 69 setError( 62 - `We couldn't find an account for "${trimmed}". Please check for typos and try again.` 70 + `We couldn't find an account for "${identifier}". Please check for typos and try again.` 63 71 ) 64 72 } else { 65 73 setError(err instanceof Error ? err.message : 'Failed to start login') ··· 112 120 113 121 <div className="space-y-1"> 114 122 <FormLabel htmlFor="handle" required> 115 - Handle 123 + Handle or DID 116 124 </FormLabel> 117 125 <input 118 126 id="handle" ··· 131 139 )} 132 140 /> 133 141 <p className="text-xs text-muted-foreground"> 134 - Your Bluesky or AT Protocol handle (e.g. jay.bsky.team) 142 + Handle, DID, or AT Protocol URI (e.g. jay.bsky.team) 135 143 </p> 136 144 </div> 137 145