Skip to content

About

Topdesk incident widget for Nextcloud dashboard

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

TOPdesk integration for Nextcloud

integration_topdesk — a dashboard widget showing the signed-in user's five most recent TOPdesk tickets, a button to report a new one, and an admin page that connects the server to one TOPdesk SaaS environment.

Version 1.0.0 · Nextcloud 33–35 · PHP 8.1–8.5 · AGPL-3.0-or-later


What it does

Dashboard widget — the user's five latest tickets, newest first, each with its TOPdesk number, processing status and age, linking into the Self-Service Portal. A "Report a ticket" button opens a modal with a short description, a details field and optional category/subcategory, and files a first-line incident without leaving Nextcloud.

The widget is rendered twice over, deliberately: a Vue component for the browser (which the create modal needs), and getItemsV2() on the PHP side so the same five tickets reach the mobile and desktop clients through /apps/dashboard/api/v2/widget-items.

Admin settings (Administration settings → TOPdesk) — the environment URL, the credentials, the matching rule, and a "Save and test connection" button that makes a real authenticated call and explains the failure when there is one.

Personal settings (Personal settings → TOPdesk) — shows the user which TOPdesk person they are matched to and why, and lets them pick one by hand when the automatic match cannot settle it.


Setting up TOPdesk

Two authentication modes; pick whichever the customer's TOPdesk supports.

Application token (works on every TOPdesk 7.11.005 and later)

  1. Modules → Supporting Files → Permission Groups → new group, with read on API access > REST API and write on API access > Use application tokens. The account also needs read on first/second-line incidents, read on Persons, and create permission on first-line incidents.
  2. Link the group to the operator or API account the integration will use.
  3. Create the token: on the operator's own card via My Settings, or on an API account card under Application tokens. Creating one on an API account card additionally needs write on Supporting Files > Login data, and read on Supporting Files > Operator to see API accounts at all.
  4. Copy the token immediately — it is shown once — and note the expiry date. The default is one year out, and nothing warns you when it passes.
  5. In Nextcloud, enter the URL, the login name and the token, then Save and test connection.

OAuth connected application

  1. Modules → Supporting Files → System integrations overview → Connected applications → new application integration (the client-credentials kind; the user-delegated kind is a different flow this version does not use). Managing these needs the Manage connected applications > Manage integration clients permission.
  2. Copy the client id and the one-time secret. A lost secret cannot be retrieved — delete the client and make a new one.
  3. In Nextcloud, switch the auth mode to OAuth, enter both, then Save and test connection.

Access tokens are short-lived (900 s) and minted on demand; there is no refresh token in this grant and nothing to rotate by hand.

What the API account can see

The operator's category, branch and operator filters apply to everything this app reads. If the account is filtered to one branch, users outside it will show no tickets even when the person match is correct — which looks like a matching bug and is not one. Give the API account unfiltered read access to incidents, or expect that asymmetry.


How a Nextcloud user becomes a TOPdesk person

The problem

TOPdesk's incident API has two endpoint families. GET /incidents is operator-only and can read every ticket in the environment; GET /requester/incidents returns only the caller's own tickets but requires the person's own credentials, which a server-to-server integration does not have.

So this app authenticates as an operator and filters. Which means the filter is the only thing standing between a user and everybody else's ticket history — and the filter is only as good as the identity behind it.

The design

The app stores a pair per user: the identifier it matched on, and the TOPdesk person UUID that identifier resolved to. Tickets are then always read with caller.id==<uuid> — never by email address.

That matters because addresses get reused. When somebody leaves and j.doek@acme.nl is handed to their replacement, matching on the address alone shows the newcomer the leaver's entire history. It is not an edge case; it happens on every departure where the address is recycled.

So the pair is re-verified on every login, and whenever it goes stale (one hour by default):

The identifier now resolves to… What happens
the same UUID nothing — carry on
a different UUID the identifier changed hands. The stored UUID is replaced and only the new person's tickets are shown. Logged at warning level
nobody the link is dropped. The widget shows "no TOPdesk person found", not somebody's tickets
several people the link is dropped. The app never guesses between person cards
TOPdesk unreachable the link is kept and marked unverified. A network blip is not evidence of an identity change

That last row is why TopdeskException carries a transient flag: dropping links on a timeout would blank every user's widget during a maintenance window and then re-resolve them all at once afterwards.

A user (or an admin on their behalf) can pin a person by hand for the cases matching cannot settle — shared functional mailboxes, contractors, two cards with one address. A pinned link is an explicit decision and the resolver does not overwrite it, but it is shown in personal settings so it is never invisible.

⚠ Matching on email assumes users cannot change their email

On a stock Nextcloud, a user can change their own email address in personal settings. With email matching, an address they set is an address they get the tickets of — so a user who types a colleague's address into their own account, logs out and back in, sees that colleague's tickets.

Nothing in this app can prevent that, because from its side the change is indistinguishable from a legitimate correction. Choose one of:

  • users provisioned from LDAP / Entra ID / SAML, where the address is not theirs to edit (the common case for a customer running TOPdesk, and the reason email is the default);
  • prevent email changes on the instance;
  • match on the Nextcloud username instead — it is not user-settable. Pair it with the TOPdesk person field that carries the same value, usually networkLoginName when both systems are fed from the same directory.

The admin page says this on screen when email matching is selected.

Troubleshooting a match

occ topdesk:diagnose                 # connection check and the configured match rule
occ topdesk:diagnose <user>          # full trace for one user
occ topdesk:diagnose --all           # one line per user, plus a state summary

Read-only by default — looking at why a user does not match should not quietly change what they are matched to. Pass --repair to write the result.

A trace for one user prints the Nextcloud field and the exact value taken from the account (quoted, with its length, and with leading or trailing whitespace called out by name — a stray space is the most common cause that is invisible in a log), the FIQL query actually sent, every candidate TOPdesk returned, and the decision made.

When nothing matched it then answers why, in the order these are usually the cause:

  1. An archived person card has the value. Archived cards are ignored on purpose — that is the whole reused-address defence — so this means un-archive the card if the person is still around.
  2. The value is on an operator card, not a person card. In TOPdesk those are separate, and only a person can be the caller on an incident. This is the usual reason the very first test fails: whoever sets the integration up is an operator — that is how they got an application token — and often has no person card at all.
  3. Active people whose value merely starts the same way, which catches a typo, a .com where the directory says .nl, or a second account.

If a match succeeds but the person card does not have the Caller role ticked, the trace says so. That combination is easy to misread as a broken match: the identity is correct, but the person can never be a caller on an incident, so the widget stays empty and creating a ticket fails.

Also remember that the API operator's branch, category and operator filters apply to everything this app reads — a correctly matched user in a filtered-out branch shows no tickets.

The admin page has a Log matching details switch for the passive version: every resolution, for every user, written to the Nextcloud log as one multi-line entry. Search the log for TOPdesk match trace. It is off by default and should go back off afterwards, because the trace contains the addresses being matched. Entries are written at warning level deliberately — Nextcloud's default loglevel is 2, so an info entry never reaches the file and the switch would appear to do nothing.

Match fields

Nextcloud side TOPdesk person card
Email address (default) Email address (default)
Username Network login name · Employee number · TOPdesk login name

Reading tasLoginName (TOPdesk login name) needs the Settings > Login Settings permission on the API operator; without it every match comes back empty.


Build

npm install
npm run build

A production build takes about 7 seconds. Two things had to be worked around to get there, both recorded in the config so they are not rediscovered:

  • TypeScript must stay on 6.x (pinned in devDependencies). TypeScript 7 removed an API that vite-plugin-dts still calls through @volar/typescript, and the vite config then fails to load with Cannot read properties of undefined (reading 'useCaseSensitiveFileNames').

  • extractLicenseInformation: false in vite.config.mjs. With createAppConfig's default REUSE license scanning on, the build never finishes here: it hangs in "rendering chunks" and eventually dies with "Ineffective mark-compacts near heap limit" — at an 8 GB heap limit, for three small entries. It is not the module graph. An entry containing nothing but console.log('probe') behaved identically, and the same entry with the plugin off built in 78 ms. It also survived pinning vite, rollup, TypeScript and @nextcloud/vue to the exact versions that build fine in the TeamHub app next door, so it is the plugin and not the dependency tree. The cost is the generated .license sidecar files; the app's own licence is declared in appinfo/info.xml and composer.json. Worth retrying on a future @nextcloud/vite-config.

Components are imported by subpath (@nextcloud/vue/components/NcButton) rather than from the package root. The root is a barrel that re-exports all ~150 components; going through it pulled 1793 modules into the graph where the subpaths pull 1272.

Vite's warning that build.outDir is the project root is expected — createAppConfig defaults it there, and emptyOutDir: false keeps a build from wiping the app directory.

CSS is extracted per entry into css/vite-*.chunk.css, and the PHP side loads it by reading each entry's css/<entry>.css manifest stub rather than naming chunks by hand — Rollup renames shared chunks whenever an import graph changes, and the symptom of a stale hand-written name is an unstyled page with no error. See lib/Service/ViteStyles.php and the comment block in vite.config.mjs.

Deploying to the AIO test instance

npm run deploy:aio

Builds, then mirrors the shipped paths into the nextcloud-aio-nextcloud container's custom_apps/integration_topdesk, chowns them to www-data, runs occ upgrade and reloads php-fpm. Modelled on TeamHub's script of the same name.

Flag Effect
--dry-run print every docker command, change nothing
--enable also run occ app:enable — needed once, on a first deploy
--restart restart the container instead of reloading php-fpm
--skip-preflight deploy a bundle the checks reject

Overridable with TOPDESK_AIO_CONTAINER and TOPDESK_AIO_APP_DIR.

Pre-flight refuses to deploy when appinfo/info.xml and package.json disagree on the version, when src/ is newer than js/, when an entry's JS is missing, or when a CSS manifest stub is missing or points at a chunk that is not there. That last one is specific to this app: ViteStyles.php discovers stylesheets by reading css/<entry>.css, so a missing stub renders the page completely unstyled with nothing in any log to explain it. Don't reach for --skip-preflight to make a red deploy green — an afternoon testing a stale bundle is an afternoon of conclusions about code that is not running.

Two things worth knowing about the instance:

  • PHP changes land within ~60 s (opcache.validate_timestamps=On, revalidate_freq=60). If a change looks like it did not land, wait a minute before debugging it. Frontend changes are immediate, because Nextcloud cache-busts js/css URLs on the app version.
  • A changed routes.php deployed under the same version number needs the php-fpm reload the script does at the end. Since NC 30 the router caches the compiled route table in APCu for an hour, keyed on the enabled apps' versions — a version bump invalidates it, a same-version redeploy does not, and the new route answers 404 until then while php -r inside the container matches it perfectly.

Tests

php tests/fiql-test.php

Checks FIQL quoting, which is the one place a crafted email address could otherwise append conditions to the persons query and widen which person card an account resolves to.


Layout

appinfo/info.xml              manifest (validated against the App Store schema)
appinfo/routes.php
lib/Service/
  ConnectionConfig.php        admin connection; secrets via ICrypto
  TopdeskClient.php           HTTP; absorbs TOPdesk's 204-means-empty and 206-means-more
  Fiql.php                    query building and escaping
  PersonResolver.php          the matching rules above
  PersonLink.php              one user's (match value, person UUID) pair
  IncidentService.php         list and create, always filtered on the UUID
  Auth/                       application token · OAuth client credentials
  ViteStyles.php              loads built CSS from the Vite manifest
lib/Dashboard/TicketsWidget.php
lib/Controller/               admin · personal · incident
lib/Listener/                 re-verify at login · forget on account deletion
src/                          Vue 3 components for widget, admin and personal pages
docs/topdesk-api-notes.md     API research: spec URLs, endpoint permissions, quirks

docs/topdesk-api-notes.md is worth reading before extending this — it records which endpoints an operator may call versus a person, the exact callerLookup semantics, the FIQL operator table, and the download URLs for TOPdesk's OpenAPI specs (the developer portal's HTML explorer is JS-rendered and cannot be fetched, but the specs behind it can).


Not in this version

  • Per-user OAuth (TOPdesk's user-delegated integration, authorization-code grant). Each user would connect their own TOPdesk account once and the app would read /requester/incidents with their token — TOPdesk enforces "own tickets" server-side, so no operator credential could over-read and the matching problem above disappears entirely. Costs the OAuth dance, per-user token storage, and a reconnect whenever the 60-day refresh token lapses. IncidentService is the only class that would need a second implementation.
  • Ticket detail inside Nextcloud, replies, attachments.
  • Changes, reservations and assets — separate TOPdesk APIs.
  • An admin overview of who is matched to whom. Links live in oc_preferences (integration_topdesk / person_id, match_value, state, …); a listing page would want them promoted to a table of their own.

About

Topdesk incident widget for Nextcloud dashboard

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages