Skip to content

Commit b00684c

Browse files
kennethphoughclaude
andcommitted
feat(jwt): expand FK fields in login response data (SESSION_RETURN_FK parity)
HMAC SessionController sets getFKTables = SESSION_RETURN_FK (default true), causing getObject() to recursively expand FK integers into full nested objects. JWT userToArray was returning raw FK ints, so frontend code like 'user.org.org_type' silently failed (org was '2', not {id:2, org_type:'LP'}). Add FK expansion to userToArray: - Walk model struct - For fields with 'fk' definition + non-empty value, retrieve the referenced ModelObject and recursively serialize it - Bounded at depth 3 to prevent runaway recursion on cyclic FKs Respects SESSION_RETURN_FK constant (defaults true, can be disabled per-deployment). retrieve() handles dbswitch automatically based on the FK model's appId, so app-scoped + default-scoped FKs both work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1445ca8 commit b00684c

1 file changed

Lines changed: 37 additions & 6 deletions

File tree

src/Core/Auth/JwtEndpoint.php

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,20 +212,51 @@ private static function login(array $body, string $ip): array
212212
}
213213

214214
/**
215-
* Serialize a ModelObject for the JWT login response. Strips fields
216-
* marked `protected: true` in the model struct (e.g. password hash,
217-
* secret_key) — mirrors what ModelController::getObject does but
218-
* standalone, since we can't invoke a controller from here.
215+
* Serialize a ModelObject for the JWT login response. Mirrors the
216+
* subset of ModelController::getObject that the customer app actually
217+
* consumes:
218+
* - Strip fields marked `protected: true` (password hash, etc.)
219+
* - Expand FK references into nested objects when SESSION_RETURN_FK
220+
* is enabled (default true). Without this, `user.org` would be
221+
* the integer `2` instead of `{id:2, org_type:'LP', ...}` — and
222+
* frontends doing `user.org.org_type` would silently break.
223+
*
224+
* Recursion-bounded at depth 3 to prevent runaway expansion on
225+
* cyclic FKs (rare but possible in customer schemas).
219226
*/
220-
private static function userToArray(ModelObject $user): array
227+
private static function userToArray(ModelObject $user, int $depth = 0): array
221228
{
222229
$params = $user->getAllParams();
223230
$struct = $user->kyte_model['struct'] ?? [];
231+
$expandFks = $depth < 3
232+
&& (!defined('SESSION_RETURN_FK') || SESSION_RETURN_FK);
233+
224234
foreach ($params as $key => $value) {
235+
if (!isset($struct[$key])) {
236+
continue;
237+
}
238+
225239
if (isset($struct[$key]['protected']) && $struct[$key]['protected']) {
226-
$params[$key] = ''; // strip protected fields (password hash, etc.)
240+
$params[$key] = '';
241+
continue;
242+
}
243+
244+
// Expand FK if struct declares one, value is non-empty, and
245+
// SESSION_RETURN_FK allows.
246+
if ($expandFks && isset($struct[$key]['fk'], $value) && !empty($value)) {
247+
$fk = $struct[$key]['fk'];
248+
if (isset($fk['model'], $fk['field']) && defined($fk['model'])) {
249+
$fkModel = constant($fk['model']);
250+
$fkObj = new ModelObject($fkModel);
251+
// retrieve() handles dbswitch automatically based on
252+
// whether the FK model has 'appId' (app-scoped vs default).
253+
if ($fkObj->retrieve($fk['field'], $value, null, null, true)) {
254+
$params[$key] = self::userToArray($fkObj, $depth + 1);
255+
}
256+
}
227257
}
228258
}
259+
229260
// The kyte_model handle itself should never leak to clients.
230261
unset($params['kyte_model']);
231262
return $params;

0 commit comments

Comments
 (0)