fix: map duplicate-username race to 409 and test session/cascade behavior
This commit is contained in:
@@ -80,7 +80,15 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const { username, password } = (request.body ?? {}) as { username?: string; password?: string }
|
const { username, password } = (request.body ?? {}) as { username?: string; password?: string }
|
||||||
const invalid = validateCredentials(username, password)
|
const invalid = validateCredentials(username, password)
|
||||||
if (invalid) return reply.code(400).send({ error: 'invalid_input', detail: invalid })
|
if (invalid) return reply.code(400).send({ error: 'invalid_input', detail: invalid })
|
||||||
const user = createUser(request.server.db, username as string, await hashPassword(password as string), true)
|
let user
|
||||||
|
try {
|
||||||
|
user = createUser(request.server.db, username as string, await hashPassword(password as string), true)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (String(err.message).includes('UNIQUE constraint failed')) {
|
||||||
|
return reply.code(403).send({ error: 'setup_already_done' })
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
request.server.db.prepare('INSERT INTO settings (user_id) VALUES (?)').run(user.id)
|
request.server.db.prepare('INSERT INTO settings (user_id) VALUES (?)').run(user.id)
|
||||||
const token = createSession(request.server.db, user.id)
|
const token = createSession(request.server.db, user.id)
|
||||||
return reply.setCookie(COOKIE_NAME, token, cookieOpts()).code(200).send({ user: toPublicUser(user) })
|
return reply.setCookie(COOKIE_NAME, token, cookieOpts()).code(200).send({ user: toPublicUser(user) })
|
||||||
@@ -122,7 +130,15 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (getUserByUsername(request.server.db, username as string)) {
|
if (getUserByUsername(request.server.db, username as string)) {
|
||||||
return reply.code(409).send({ error: 'username_taken' })
|
return reply.code(409).send({ error: 'username_taken' })
|
||||||
}
|
}
|
||||||
const user = createUser(request.server.db, username as string, await hashPassword(password as string), false)
|
let user
|
||||||
|
try {
|
||||||
|
user = createUser(request.server.db, username as string, await hashPassword(password as string), false)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (String(err.message).includes('UNIQUE constraint failed')) {
|
||||||
|
return reply.code(409).send({ error: 'username_taken' })
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
request.server.db.prepare('INSERT INTO settings (user_id) VALUES (?)').run(user.id)
|
request.server.db.prepare('INSERT INTO settings (user_id) VALUES (?)').run(user.id)
|
||||||
return reply.code(200).send(toPublicUser(user))
|
return reply.code(200).send(toPublicUser(user))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,6 +71,33 @@ describe('auth routes', () => {
|
|||||||
expect(res.statusCode).toBe(401)
|
expect(res.statusCode).toBe(401)
|
||||||
await app.close()
|
await app.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('re-login invalidates the previous session', async () => {
|
||||||
|
const app = await buildTestApp()
|
||||||
|
await setupAdmin(app)
|
||||||
|
const first = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/login',
|
||||||
|
payload: { username: 'admin', password: 'adminpass123' },
|
||||||
|
})
|
||||||
|
const firstCookie = getCookie(first)
|
||||||
|
|
||||||
|
const second = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/login',
|
||||||
|
...auth(firstCookie),
|
||||||
|
payload: { username: 'admin', password: 'adminpass123' },
|
||||||
|
})
|
||||||
|
expect(second.statusCode).toBe(200)
|
||||||
|
|
||||||
|
const oldStale = await app.inject({ method: 'GET', url: '/api/me', ...auth(firstCookie) })
|
||||||
|
expect(oldStale.statusCode).toBe(401)
|
||||||
|
|
||||||
|
const newCookie = getCookie(second)
|
||||||
|
const fresh = await app.inject({ method: 'GET', url: '/api/me', ...auth(newCookie) })
|
||||||
|
expect(fresh.statusCode).toBe(200)
|
||||||
|
await app.close()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('user admin', () => {
|
describe('user admin', () => {
|
||||||
@@ -114,11 +141,19 @@ describe('user admin', () => {
|
|||||||
it('cannot delete self; deleting another user works and cascades settings', async () => {
|
it('cannot delete self; deleting another user works and cascades settings', async () => {
|
||||||
const { app, cookie } = await adminApp()
|
const { app, cookie } = await adminApp()
|
||||||
await loginAs(app, cookie, 'bob', 'bobpass123')
|
await loginAs(app, cookie, 'bob', 'bobpass123')
|
||||||
|
const settingsCount = () =>
|
||||||
|
(app.db.prepare('SELECT COUNT(*) AS n FROM settings WHERE user_id = 2').get() as { n: number }).n
|
||||||
|
const sessionsCount = () =>
|
||||||
|
(app.db.prepare('SELECT COUNT(*) AS n FROM sessions WHERE user_id = 2').get() as { n: number }).n
|
||||||
|
expect(settingsCount()).toBe(1)
|
||||||
|
expect(sessionsCount()).toBe(1)
|
||||||
const selfDelete = await app.inject({ method: 'DELETE', url: '/api/users/1', ...auth(cookie) })
|
const selfDelete = await app.inject({ method: 'DELETE', url: '/api/users/1', ...auth(cookie) })
|
||||||
expect(selfDelete.statusCode).toBe(400)
|
expect(selfDelete.statusCode).toBe(400)
|
||||||
|
|
||||||
const del = await app.inject({ method: 'DELETE', url: '/api/users/2', ...auth(cookie) })
|
const del = await app.inject({ method: 'DELETE', url: '/api/users/2', ...auth(cookie) })
|
||||||
expect(del.statusCode).toBe(200)
|
expect(del.statusCode).toBe(200)
|
||||||
|
expect(settingsCount()).toBe(0)
|
||||||
|
expect(sessionsCount()).toBe(0)
|
||||||
const list = await app.inject({ method: 'GET', url: '/api/users', ...auth(cookie) })
|
const list = await app.inject({ method: 'GET', url: '/api/users', ...auth(cookie) })
|
||||||
expect(list.json().users).toHaveLength(1)
|
expect(list.json().users).toHaveLength(1)
|
||||||
await app.close()
|
await app.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user