UpdateUser was manually json.Marshal'ing user.FrontendSettings and
re-assigning the []byte back to the interface{} field before passing
the user to xorm. The column is declared:
FrontendSettings interface{} `xorm:"json null" json:"-"`
xorm's `json` modifier already marshals the field on write. Pre-marshalling
to []byte and stuffing it back into the interface causes xorm to JSON-encode
a []byte (which serializes as a base64 string). The DB ends up storing
`"bnVsbA=="` (base64 of "null") for users whose FrontendSettings is nil,
or a double-encoded blob for users with real settings.
On read, xorm unmarshals that string back into the interface as a Go
`string`, and the API response then contains:
"frontend_settings": "bnVsbA=="
instead of a JSON object or null. The Flutter mobile client
(go-vikunja/app) typed-casts `frontend_settings` to
`Map<String, dynamic>?` and crashes with:
type 'String' is not a subtype of type 'Map<String, dynamic>?'
which the app surfaces as "could not connect to this server". The web
client tolerates it silently (JS spread over a string).
Trigger path: every OIDC login of an existing user calls UpdateUser
(pkg/modules/auth/openid/openid.go), so the row is corrupted on every
re-login. See go-vikunja/app#265.
Fix: let xorm handle the JSON marshalling as the struct tag advertises.
Drop the now-unused encoding/json import.