Task Issue 990 projectsView Project API Key - sebiboga/testlink-upgraded GitHub Wiki
Port of the legacy project API key row into the modern
Test Project Management screen (gui/templates/projectsView.html).
The 1.9.20 project editor showed the project's API key as read-only text, only when the key was set:
-
lib/functions/testproject.class.php:124-136—create()always generates one:$api_key = md5(rand()) . md5(rand()); ... " options,notes,active,is_public,prefix,api_key) " ...
So in practice every project has a 64-char key unless somebody blanks it.
-
lib/project/projectEdit.php:589—edit()copies it into the gui object alongside the other columns:$k2l = array('color','notes', 'active','is_public','issue_tracker_enabled', 'code_tracker_enabled','reqmgr_integration_enabled','api_key');
-
lib/project/projectEdit.php:239—api_keybelongs to$nullable_keys, so a blank/absent value never aborts the update. -
gui/templates/dashio/project/projectEdit.tpl:269-274— the row itself, inside the Availability block, right after the Active/Public checkboxes and before the button row:{if $gui->api_key != ''} <tr> <td {$tdStyle}>{$labels.api_key}</td> <td>{$gui->api_key}</td> </tr> {/if}
Plain text, no input, no copy button, no edit control.
| Layer | Before | Symptom |
|---|---|---|
| BFF SQL |
api/projects/index.php projectSelect() did not select tp.api_key
|
$row['api_key'] undefined |
| BFF serializer |
formatProject() returned 13 keys, none of them the key |
the field could never reach the browser |
projectsView.html |
the Availability form-section ended at the Public checkbox | nothing to render |
The gap was endpoint-scoped, not repo-wide: the sibling standalone screen
gui/templates/projects/projectEdit.html:208-213,343-344 already rendered the row
correctly against its own endpoint (api/projectedit/index.php:293,
'api_key' => $info['api_key'] ?? ''). So the markup pattern to follow already
existed in 2.0.1 — only the projectsView.html modal path was missing it.
Measured before the change (admin logged in, project with a 64-char key):
GET /api/projects/1 -> data keys: [id, name, prefix, description, isActive, isPublic,
optReq, optPriority, optAutomation, optInventory,
issueTracker, codeTracker, reqMgrSystem] # no api key
editProject(1) -> {hasApiKeyText:false, hasApiKeySection:false}
-
projectSelect()— addedtp.api_keyto the column list. -
formatProject()— added the serialized field, read-only by contract:// Legacy parity: projectEdit.tpl:269-274 shows the project's API key as // read-only text, but only when it is set. testproject::create() always // generates one (testproject.class.php:124), so the empty case is a // hand-cleared key. Never writable through this endpoint (POST/PUT ignore // it) — the key is generated server-side and only used by the lnl.php // anonymous-access links. 'apiKey' => (string)($row['api_key'] ?? ''),
Both response shapes changed together, because listProjects() and
getProject() share formatProject(): GET /api/projects/ and
GET /api/projects/<id>.
createProject() / updateProject() were not touched, so the key stays
server-generated and cannot be set through the API — matching legacy, where
api_key is only ever produced by the manager.
-
Markup —
#apiKeyRow/#apiKeyValinside the Availability form-section, exactly at the legacy position (after Active/Public, before the modal actions), plus a Copy button. No<input>: the key is not editable. -
CSS —
.readonly-val(monospace,#f8f9fapanel,word-break: break-all,user-select: allso the 64-char md5 is selectable in one click) and.copy-btn, in the file's existing teal#4ECDC4palette. -
JS
editProject()— shows the row and writes the value withtextContentonly when the key is non-empty, otherwise clears the value and hides the row. That is the literal{if $gui->api_key != ''}condition; both branches are explicit, so no value can leak from a previously edited project. -
JS create handler — forces the row hidden and empties the value: a
project has no key until
create()INSERTs one, and the legacy create screen never shows the row either. -
JS
copyApiKey(btn)— Clipboard API with anexecCommand('copy')fallback (the Clipboard API needs a secure context, so the fallback is what runs on plain-HTTP hosts); the label swaps toproj.apiKeyCopied/proj.apiKeyCopyFailedfor 2 s. The button istype="button", so it cannot submit the surrounding form.
Four keys added to all 10 locale bundles (de, en, es, fr, it, ja, pt, ro, ru, zh),
inserted in place after proj.apiId (+4/-0 per file — the bundles are only
locally ordered, so re-sorting them must be avoided):
| key | en | ro | de | es | fr | it | ja | pt | ru | zh |
|---|---|---|---|---|---|---|---|---|---|---|
proj.apiKey |
API Key | Cheie API | API-Schlüssel | Clave API | Clé API | Chiave API | APIキー | Chave da API | Ключ API | API 密钥 |
proj.apiKeyCopy |
Copy | Copiază | Kopieren | Copiar | Copier | Copia | コピー | Copiar | Копировать | 复制 |
proj.apiKeyCopied |
API key copied to clipboard | Cheia API a fost copiată în clipboard | API-Schlüssel in die Zwischenablage kopiert | Clave API copiada al portapapeles | Clé API copiée dans le presse-papiers | Chiave API copiata negli appunti | APIキーをクリップボードにコピーしました | Chave da API copiada para a área de transferência | Ключ API скопирован в буфер обмена | API 密钥已复制到剪贴板 |
proj.apiKeyCopyFailed |
Copy failed | Copierea a eșuat | Kopieren fehlgeschlagen | Error al copiar | Échec de la copie | Copia non riuscita | コピーに失敗しました | Falha ao copiar | Не удалось скопировать | 复制失败 |
The label reuses the wording already proven for pedit.apiKey in each locale, so
it is identical to the sibling screen's.
Before — the edit modal with no API key row (pre-change):

After — the read-only API key row rendered in the Availability section:

25 / 25 assertions PASS — full table in tmp/TLU_Test_Cases.md
("Task — Issue #990"), covering:
- BFF: field present in both the single and the list response, 404 and 401 paths
unchanged, key not writable (
PUT {"apiKey":"HACKED"}is ignored while the rest of the same body is applied); - screen: visible only for a keyed project in edit mode; hidden in the create modal and for a blank key; correct behaviour through the create → edit(blank) → edit(keyed) → create sequence; no stale value in the DOM; value injected as text, not markup; copy button cannot submit the form;
- review regressions: the copy label cannot survive a modal reopen, two clicks
1.9 s apart do not race the revert timer, and the row's a11y wiring
(
for,tabindex/role/aria-readonly,aria-live) is present; - i18n:
?locale=ro→Cheie API/Copiază,?locale=ja→APIキー/コピー; - regression: the 4 other consumers of
/api/projects/index.php(tcAssignments.html,planUpdateTC.html,testUrgency.html,platformsAssign.html) still get HTTP 200 and only readid/name; - hygiene:
php -lclean, no console error/warn,eventstable gains no Error/Warning row (sum(log_level>16) = 0).
The project API key is public-share material: it is the anonymous access
credential for the project — lnl.php share links
(lib/functions/common.php:1442) and a direct
metricsDashboard.php?apikey=<key> entry, resolved by
testproject::getByAPIKey() (lib/results/metricsDashboard.php:358-390).
It is exposed only
through api/projects/index.php, which gates every route behind
mgt_modify_product (api/projects/index.php:41) — so no new access path is
created, and an anonymous request still gets 401. The value is a server-generated
md5 pair and is never accepted from the client.
- Stale "copied" message surviving a modal reopen: the button label is now reset
on every open path (
resetApiKeyButton()), which also cancels the pending revert timer, so a second click cannot flip the label back mid-message. - The Copy button reuses the file's existing
.action-btn.info-btnpair instead of a new bespoke.copy-btnrule; the value got a focus outline andtabindex/role/aria-readonly, the label got afor, and the message span gotaria-live="polite"so the feedback is announced. - The copy feedback is selected with
btn.querySelector('[data-i18n]')instead of'span', so it survives a markup change around the icon. - "only used by the lnl.php links" was factually too narrow — the key also
authenticates
metricsDashboard.php?apikey=directly; corrected in the code comment, the docs and the CHANGELOG.
- Sibling screen that already had the row:
gui/templates/projects/projectEdit.html:208-213. - Bug found while regression-testing the other consumers, filed separately and
not fixed here:
api/tcassignments/index.php:348returns an empty HTTP 500 when a project has no test plans → #1648.