Fix: password signup crashes with JSON parse error when MAIL_URL is configured (#10519)

When signing up with a password on deployments with MAIL_URL set, the
account service returns token: undefined to enforce email confirmation.
SignupForm.svelte was calling logIn() unconditionally, which triggered
PUT /cookie without an Authorization header, returning a 401 with an
unparseable body and leaving the user stuck on the signup page.

- Guard logIn() in SignupForm.svelte with `result.token != null`, matching
  the pattern already used in doLoginNavigate() in utils.ts
- Fix PUT /cookie 401 response to use Koa's ctx.status/ctx.body instead of
  raw ctx.res.writeHead/end, so the error body is correctly serialized
- Add unit tests for the token guard logic

Fixes #10518

Signed-off-by: Yulian Diaz <5605867+spatialy@users.noreply.github.com>
This commit is contained in:
Yulian Diaz
2026-02-19 14:09:29 +07:00
committed by GitHub
parent b7ca5a1d45
commit f391dd0a5d
3 changed files with 102 additions and 6 deletions
@@ -0,0 +1,97 @@
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Tests for the token guard fix in SignupForm.svelte (issue #10518).
*
* When MAIL_URL is configured the account service returns token: undefined
* to force email confirmation. The signup handler must skip logIn() in that
* case — calling logIn() without a token triggers PUT /cookie with no
* Authorization header, which returns 401 and crashes the client's JSON
* parser with "Unexpected token".
*/
interface LoginInfo {
account: string
name?: string
token?: string
}
/**
* Mirrors the fixed logic from SignupForm.svelte:
*
* if (result != null) {
* if (result.token != null) {
* await logIn(result)
* }
* goTo('confirmationSend')
* }
*/
async function handleSignupResult (
result: LoginInfo | null,
logIn: (info: LoginInfo) => Promise<void>,
goTo: (page: string) => void
): Promise<void> {
if (result != null) {
if (result.token != null) {
await logIn(result)
}
goTo('confirmationSend')
}
}
describe('SignupForm token guard (issue #10518)', () => {
let logIn: jest.Mock
let goTo: jest.Mock
beforeEach(() => {
logIn = jest.fn().mockResolvedValue(undefined)
goTo = jest.fn()
})
it('skips logIn and redirects to confirmationSend when token is undefined (MAIL_URL configured)', async () => {
// Server returns token: undefined when email confirmation is required
const result: LoginInfo = { account: 'acc-uuid', name: 'Alice Smith', token: undefined }
await handleSignupResult(result, logIn, goTo)
expect(logIn).not.toHaveBeenCalled()
expect(goTo).toHaveBeenCalledWith('confirmationSend')
})
it('skips logIn and redirects to confirmationSend when token is absent', async () => {
const result: LoginInfo = { account: 'acc-uuid' }
await handleSignupResult(result, logIn, goTo)
expect(logIn).not.toHaveBeenCalled()
expect(goTo).toHaveBeenCalledWith('confirmationSend')
})
it('calls logIn then redirects to confirmationSend when token is present (no MAIL_URL)', async () => {
const result: LoginInfo = { account: 'acc-uuid', name: 'Bob Jones', token: 'eyJhbGciOiJIUzI1NiJ9.test' }
await handleSignupResult(result, logIn, goTo)
expect(logIn).toHaveBeenCalledTimes(1)
expect(logIn).toHaveBeenCalledWith(result)
expect(goTo).toHaveBeenCalledWith('confirmationSend')
})
it('calls neither logIn nor goTo when result is null (signup error)', async () => {
await handleSignupResult(null, logIn, goTo)
expect(logIn).not.toHaveBeenCalled()
expect(goTo).not.toHaveBeenCalled()
})
})
@@ -95,7 +95,9 @@
status = loginStatus
if (result != null) {
await logIn(result)
if (result.token != null) {
await logIn(result)
}
goTo('confirmationSend')
}
}
+2 -5
View File
@@ -306,11 +306,8 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap
router.put('/cookie', async (ctx) => {
const token = extractToken(ctx.request.headers)
if (token === undefined) {
ctx.body = JSON.stringify({
error: new Status(Severity.ERROR, platform.status.Unauthorized, {})
})
ctx.res.writeHead(401)
ctx.res.end()
ctx.status = 401
ctx.body = { error: new Status(Severity.ERROR, platform.status.Unauthorized, {}) }
return
}