fix(auth): preserve permission group names when creating API tokens
objectToSnakeCase recursively transforms all object keys, including the permissions map. This mangles "time-entries" → "time_entries", causing the backend to reject the permission as invalid. Override autoTransformBeforePut and manually restore the original permissions map after the snake_case transform in beforeCreate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f8eacca7c8
commit
f5a99e4c15
|
|
@ -0,0 +1,35 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
// Regression test: objectToSnakeCase mangles hyphenated permission group
|
||||
// names like "time-entries" → "time_entries". ApiTokenService.beforeCreate
|
||||
// works around this by restoring the original permissions after the transform.
|
||||
// This test ensures the underlying problem is documented and will tell us
|
||||
// if the library behaviour changes.
|
||||
describe('objectToSnakeCase on API token permissions', () => {
|
||||
it('mangles time-entries to time_entries (the bug we work around)', () => {
|
||||
const input = {
|
||||
title: 'test',
|
||||
expiresAt: '2099-01-01T00:00:00Z',
|
||||
permissions: {
|
||||
'tasks': ['read_all', 'create'],
|
||||
'time-entries': ['read_all', 'create'],
|
||||
'tasks_assignees': ['create', 'delete'],
|
||||
},
|
||||
}
|
||||
|
||||
const result = objectToSnakeCase(input)
|
||||
|
||||
// The outer key is fine
|
||||
expect(result.expires_at).toBe('2099-01-01T00:00:00Z')
|
||||
|
||||
// time-entries gets mangled — this is the bug
|
||||
expect(result.permissions['time_entries']).toEqual(['read_all', 'create'])
|
||||
expect(result.permissions['time-entries']).toBeUndefined()
|
||||
|
||||
// Other groups survive
|
||||
expect(result.permissions['tasks']).toEqual(['read_all', 'create'])
|
||||
expect(result.permissions['tasks_assignees']).toEqual(['create', 'delete'])
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import AbstractService from '@/services/abstractService'
|
||||
import type {IApiToken} from '@/modelTypes/IApiToken'
|
||||
import ApiTokenModel from '@/models/apiTokenModel'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
export default class ApiTokenService extends AbstractService<IApiToken> {
|
||||
constructor() {
|
||||
|
|
@ -11,6 +12,19 @@ export default class ApiTokenService extends AbstractService<IApiToken> {
|
|||
})
|
||||
}
|
||||
|
||||
// Disable the default snake_case transform — beforeCreate handles it
|
||||
// manually to preserve the permissions map keys (e.g. "time-entries").
|
||||
autoTransformBeforePut(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
beforeCreate(model: IApiToken) {
|
||||
const permissions = model.permissions
|
||||
const transformed = objectToSnakeCase(model)
|
||||
transformed.permissions = permissions
|
||||
return transformed
|
||||
}
|
||||
|
||||
processModel(model: IApiToken) {
|
||||
return {
|
||||
...model,
|
||||
|
|
@ -18,11 +32,11 @@ export default class ApiTokenService extends AbstractService<IApiToken> {
|
|||
created: new Date(model.created).toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
modelFactory(data: Partial<IApiToken>) {
|
||||
return new ApiTokenModel(data)
|
||||
}
|
||||
|
||||
|
||||
async getAvailableRoutes() {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue