feat(jira): Cloud support PR-1 — deployment-type + OAuth2 config data model#416
Conversation
…ystem model Data-model foundation for Jira Cloud support (no behaviour change — the factory still builds the OAuth-1 service for every ticket system). Adds a per-ticket- system deployment discriminator and OAuth 2.0 credential storage so a later PR can branch the transport/auth by deployment type: - DeploymentType enum (SERVER | CLOUD), default SERVER. - TicketSystem: deployment_type, oauth2_client_id, oauth2_client_secret, cloud_id. Only the oauth2 client secret joins SECRET_KEYS (stripped from the list/safe view); deployment_type, oauth2_client_id and cloud_id are non-secret. - UserTicketsystem: refresh_token (encrypted later) + token_expires_at, for the OAuth 2.0 refresh lifecycle; tokensecret relaxed to nullable for Cloud rows. - TicketSystemSaveDto maps the new admin-entered fields (not cloud_id, which is server-resolved); SaveTicketSystemAction preserves a blank oauth2 client secret. - Doctrine migration + sql/full.sql kept in sync. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Surface the new ticket-system fields in the SolidJS admin: a deployment-type select (Server/DC vs Cloud) plus the OAuth 2.0 client id/secret, seeded blank in both new-row and edit-row form states. cloud_id is intentionally not editable (server-resolved at first auth). EN/DE labels added. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
There was a problem hiding this comment.
Code Review
This pull request introduces support for Jira Cloud (OAuth 2.0 / 3LO) alongside the existing Jira Server (OAuth 1.0a) implementation. It adds the necessary database columns, updates the backend entities, DTOs, and controllers to manage deployment types and OAuth 2.0 credentials, and updates the frontend administration forms and translation files. While the changes are well-tested, three issues were identified in the review: first, the frontend form currently hardcodes oauth2ClientId to an empty string when editing, which will overwrite the stored database value on save; second, the token_expires_at field should use immutable datetime types to prevent side effects; and third, the migration rollback (down method) should restore tokensecret to its original VARCHAR(50) type rather than TEXT.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #416 +/- ##
============================================
+ Coverage 83.72% 83.89% +0.17%
- Complexity 2712 2746 +34
============================================
Files 184 186 +2
Lines 7442 7521 +79
============================================
+ Hits 6231 6310 +79
Misses 1211 1211
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… expiry - Admin edit-row now reads oauth2_client_id from the row instead of blanking it. It's a non-secret field returned by toSafeArray(), so the previous blank seed would have overwritten the stored client id with '' on save. - token_expires_at uses datetime_immutable / ?DateTimeImmutable (no mutation side effects); no DDL change (same DATETIME column). - The down() migration blanks any NULL tokensecret before restoring NOT NULL, so rolling back can't fail on a Cloud row that wrote a null secret. (It reverts to TEXT NOT NULL — the immediately-preceding state from Version20250901 — not the original VARCHAR(50).) - Collapsed the repetitive nullable-string accessor tests into one explicit method (was tripping the new-code duplication gate). Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
… block The "re-fetch ticket system 1 from a cleared manager" block was repeated in each save test, which tripped the new-code duplication gate. Extract it into a shared helper used by all three so the block exists once. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
|
…ide with screenshots, ADR record, agent rules (#494) ## Summary Full documentation overhaul: a verified-facts rewrite of the guides, a new illustrated user guide, a corrected ADR record, refreshed agent rules, an extended in-app Help page, and a full-structure README. A 5-domain audit against the actual code found that, beyond routine drift, several large docs (DEPLOYMENT_GUIDE, CODE_EXAMPLES, TROUBLESHOOTING, configuration, development, testing, and ADRs 003–013) described **infrastructure this application never had** — Redis layers, JWT auth, `/api/v1`, ParaTest/Panther suites, ~50 invented env vars and `app:*` console commands. Everything replaced here was re-derived from `Makefile`, `composer.json`, `compose.yml`, `.env*`, `config/`, `src/` and the real CI workflows; every command, route, env var and version in the new docs was verified to exist. ## What's in here - **New [user guide](docs/user-guide.md)** covering every feature, with 20 English 1440px screenshots — including the QoL features (dark mode, compact density, left/right sidebar navigation, Alt shortcut badges); [features.md](docs/features.md) and [FAQ.md](docs/FAQ.md) rewritten for the current SolidJS UI - **README expanded to a full project page**: TOC, About, grouped features, screenshot gallery, first-login credentials for the dev stack, key-config table, mermaid architecture diagram, Usage/Testing/Deployment/Contributing/Security sections - **Developer guides rewritten** ([development](docs/development.md), [configuration](docs/configuration.md), [testing](docs/testing.md)) — ~5,300 lines of fiction replaced by ~1,300 verified lines; configuration now documents the real ticket-system admin fields (SERVER OAuth 1.0a vs CLOUD OAuth 2.0) and internal-Jira ticket mirroring - **Ops docs rewritten** ([deployment](docs/DEPLOYMENT_GUIDE.md), [troubleshooting](docs/TROUBLESHOOTING.md)) from `compose.yml` / `docker-bake.hcl` / the publish workflow - **ADR record corrected**: index now lists all 17 ADRs; dated *reality notes* on ADRs whose bodies describe never-built infrastructure (bodies kept as history); new [ADR-015](docs/adr/ADR-015-php-8-5-symfony-8-upgrade.md) (PHP 8.5/Symfony 8), [ADR-016](docs/adr/ADR-016-solidjs-frontend-rewrite.md) (ExtJS→SolidJS, #470/#490), [ADR-017](docs/adr/ADR-017-jira-cloud-oauth2.md) (dual-mode Jira auth, #416) - **API/internals references** (api.md, DTO/EVENT/REPOSITORY docs) aligned with the code — all 65 endpoint headings now map to real routes - **AGENTS.md**: drift fixed (PHPStan 10, PER-CS), new scoped files for `frontend/` and `e2e/`, `CLAUDE.md` symlinks restored at all five levels - **In-app Help** (`/ui/help`): new "The pages" section (EN+DE) and a link to the user guide — lint/typecheck/346 Vitest tests green - **CONTRIBUTING**: the enforced-but-undocumented DCO sign-off requirement is now documented - **Removed**: `README.rst` (stale duplicate), `TASKS.md` (abandoned 2025 notes), `docs/CODE_EXAMPLES.md` (68 KB of examples for services that never existed); all references updated ## Review Copilot raised 2 inline comments (PER-CS leftover in src/AGENTS.md, 404ing v4_EOL release link) — both fixed in 41fee70, threads replied to and resolved. Gemini reviewed with no findings. ## Verification - Every documented `make`/`composer`/`bin/console` command checked against `Makefile`, `composer.json` scripts and `src/Command/` - All relative links and image references across 58 markdown files resolve - ADR PR/commit references verified via GitHub (#416, #470, #490) - Frontend change: `bun run lint`, `bun run typecheck`, `bun run test` (346 passing), page visually verified in the running e2e stack - Host unit suite green (1557 tests) via the CaptainHook pre-commit gate on every commit ## Follow-ups (out of scope, found during verification) - `docker/nginx/default.conf` forwards to `phpfpm:9000` but the prod compose service is named `app` — the deployment guide documents the alias workaround; compose.yml should probably gain a `phpfpm` network alias - The admin Users form still offers a `CTL` user type (`frontend/src/admin/entities.ts`) that `UserType::from()` rejects - Jira Cloud OAuth 2.0: data model + admin form exist (#416), but the runtime 3LO flow is not implemented yet (`JiraAuthenticationService` is OAuth1-only) — ADR-017 records this honestly
…Server/DC (#501) ## Summary Implements the runtime half of the Jira Cloud plan started in #416 ("PR-2..4" of the original series): TimeTracker now syncs worklogs against **both** Jira Server/DC (OAuth 1.0a, unchanged) and **Jira Cloud** (OAuth 2.0 authorization-code / 3LO), selected per ticket system via the deployment type that #416 introduced. ## How it works - **`JiraCloudApiService`** extends the existing service and swaps only the transport/auth layer: 3LO authorize redirect, authorization-code exchange, **rotating refresh tokens** with recorded expiry (via the app's `ClockInterface`, frozen-time safe), automatic **cloudId resolution** from `accessible-resources` (matched by site host), Bearer-authenticated REST through `api.atlassian.com/ex/jira/{cloudId}/rest/api/2/`, and the Cloud-only **`search/jql`** endpoint. All worklog/issue/subticket operations of the base class run unchanged on top. - **`CloudOAuthStateCodec`**: Cloud redirect URIs must match the registered URL exactly, so the ticket-system id cannot ride as `?tsid=` like in the OAuth1 flow — it travels inside the encrypted, user-bound, 10-minute-TTL `state` parameter instead. - **`JiraOAuthApiFactory`** branches on `DeploymentType` — every existing caller (worklog subscriber, subticket sync, exports, admin sync) gets the right transport transparently. - The shared **`/jiraoauthcallback`** route serves both flows (a `state` parameter selects the Cloud branch; `error=access_denied` is reported cleanly). - REST **v2** is pinned deliberately: v3 requires Atlassian Document Format for worklog comments; v2 keeps accepting the plain-text comments the app sends. Docs: ADR-017 updated to "implemented" with the mechanism recorded; Cloud app-registration walkthrough (scopes, exact callback URL) added to `docs/configuration.md`; README feature bullet updated. Context: the Mogic fork's Cloud adaptation (mogic-le/timetracker, TIM-130) confirmed `search/jql` as the required Cloud search endpoint; their fork still uses deprecated OAuth 1.0a against Cloud — this PR provides the supported 3LO path instead. ## Verification - 21 new unit tests: state codec (round-trip, tamper, cross-key, TTL expiry), authorize-URL construction with decodable state, Bearer client against the gateway URL, token refresh & rotation incl. rejected-refresh cleanup + re-authorize, cloudId host matching (case-insensitive) and no-match failure, malformed token responses, `search/jql` payload, OAuth1-callback rejection, bare callback URL - Full unit suite: **1575 tests green** - PHPStan level 10 clean, PHPat clean, PER-CS clean ## Out of scope - A real end-to-end test against a live Atlassian app registration (needs org credentials — the same limitation the #416 plan noted for its PR-2) - The dormant split-stack services (`JiraIntegrationService` etc.) remain excluded from DI, unchanged



What
PR-1 of the Jira Cloud support feature (design:
claudedocs/jira-cloud-design.md). This is the data-model + admin-plumbing foundation with no behaviour change — the factory still builds the existing OAuth-1 service for every ticket system; these are just new persisted + admin-editable fields that later PRs will branch on.Confirmed decisions baked in: per-ticket-system OAuth-2 credentials (B2), a
deployment_typediscriminator onTicketSystem(A), and explicit new token columns (C).Changes
DeploymentTypeenum (SERVER|CLOUD, defaultSERVER).TicketSystem:deployment_type,oauth2_client_id,oauth2_client_secret,cloud_id. Only the oauth2 client secret joinsSECRET_KEYS(stripped from the list/toSafeArray);oauth2_client_id/deployment_type/cloud_idare non-secret.UserTicketsystem:refresh_token(encrypted in a later PR) +token_expires_atfor the OAuth-2 refresh lifecycle;tokensecretrelaxed to nullable for Cloud rows.TicketSystemSaveDtomaps the new admin-entered fields (notcloud_id, which is server-resolved);SaveTicketSystemActionpreserves a blank oauth2 client secret on save.Version20260622_AddJiraCloudSupport, symmetric up/down) +sql/full.sqlkept in sync.frontend/src/admin/entities.ts): deployment-type select + OAuth-2 client id/secret, seeded in both new-row and edit-row states; nocloud_idfield. EN/DE labels.Verification
OptimizedEntryRepositoryCacheTesttwo-suites runner warning). PHPStan no errors, PHP-CS-Fixer clean.bun test(266) / build all green.toSafeArraystripping the oauth2 secret (both spellings) while keeping the non-secrets, the DTO defaults/mapping, and the preserve-on-blank behaviour.Note for reviewers
This is a schema change: the persistent unittest DB volume needs a rebuild (
make reset-test-db) beforemake test— CI builds the DB fresh fromsql/full.sql, so it's unaffected.Follow-up PRs (per the design)
PR-2 OAuth-2 service + callback · PR-3 Cloud read client (
/search/jql) + factory branch · PR-4 Cloud worklog write · PR-5 admin field-visibility polish. (PR-2's real end-to-end test needs the Atlassian app registration — pending.)