feat: helpdesk pivot — tickets + Entra device inventory #1

Merged
vinnie merged 6 commits from feat/helpdesk-pivot into main 2026-04-28 01:04:20 +00:00
Owner

Pivots vinnie/HelpDesk (forked from openplatform/app-template) into the v1 ESPO IT helpdesk app. Five commits, broken up to be reviewable:

Commit Scope
375ad49 Remove items inventory boilerplate
2971225 Schema, Zod validators, inlined Microsoft Graph client
006c9f3 Tickets + devices API routes
d1c18c7 Helpdesk pages (dashboard, tickets, devices)
69bf35f Components + bun:test coverage (30 cases)

What this app does

Tickets — classic helpdesk ticket model. Anyone signed in can file a ticket (open/in_progress/waiting/resolved/closed × low/normal/high/urgent); only the creator can edit or delete. resolved_at is stamped on transition into a terminal status, cleared on transition back. Owner-only edit/delete in v1; assignee-aware permissions (via Forgejo group membership) is the natural follow-up.

Devices — Intune-managed device inventory cached from Microsoft Graph. POST /api/devices/sync pages through deviceManagement/managedDevices, upserts on entra_id, and records every attempt to device_sync_runs (success/fail + error_message). The /devices page surfaces last-sync metadata above the table.

The Entra integration uses client-credentials against the existing ESPOHelpDesk app registration documented in /Users/vinnieespo/ESPO/HelpDesk/Cloud/FUTURE-HELPDESK-APP.md. Required env vars: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET.

Review-standards checklist

Every API route has been written against 13-review-standards:

  • Parameterised SQL only — no string interpolation of user input
  • Object.create(null) sortable allowlist on every list route
  • ILIKE %/_/\ escape with ESCAPE '\\' on every q-search
  • Session check first on every mutating route (POST/PATCH/DELETE), short-circuit 401
  • No created_by / assignee_id in any client response — joined for display names instead
  • Zod validators tighter than DB constraints (zero-width refine, NUL-byte regex, email format, allowlist enums)
  • No any/as/@ts-ignore without justification

Test coverage (bun:test, full module mocking)

  • tickets-api.test.ts — 23 cases: 401 on every route, validation, prototype-pollution guards, ILIKE escape, allowlist sort, resolved_at stamp/clear, owner-only edit/delete
  • devices-api.test.ts — 7 cases: 401, default-50 pagination, unknown-compliance 400, ILIKE escape, sync run open/close happy path, unknown compliance normalised to unknown, failure path returns 502/SYNC_FAILED + UPDATEs run with succeeded=FALSE + error_message

Caveats

This repo is vinnie/HelpDesk (personal namespace) rather than espo-corp/helpdesk because the platform's create_app MCP tool currently fails with a 403 missing write:organization scope on its Forgejo token (issue openplatform/op-api#94). Consequently:

  • No platform-managed DB / S3 / OAuth provisioning ran.
  • Not registered with Flux / gitops, so no auto-deploy.
  • Once op-api#94 is fixed, the proper move is to recreate this in espo-corp via create_app and replay these commits there.

Test plan

  • bun install && bun typecheck — passes
  • bun test — 30 cases pass
  • Visit / unauthenticated → sign-in card renders
  • Sign in → dashboard renders status counts + recent tickets table
  • File a ticket via /tickets/new → redirects to /tickets/[id]
  • Edit + delete from the detail page works for the owner only
  • /devices shows the empty state until Entra creds are configured + sync runs once
Pivots `vinnie/HelpDesk` (forked from `openplatform/app-template`) into the v1 ESPO IT helpdesk app. Five commits, broken up to be reviewable: | Commit | Scope | |--------|-------| | `375ad49` | Remove items inventory boilerplate | | `2971225` | Schema, Zod validators, inlined Microsoft Graph client | | `006c9f3` | Tickets + devices API routes | | `d1c18c7` | Helpdesk pages (dashboard, tickets, devices) | | `69bf35f` | Components + bun:test coverage (30 cases) | ## What this app does **Tickets** — classic helpdesk ticket model. Anyone signed in can file a ticket (open/in_progress/waiting/resolved/closed × low/normal/high/urgent); only the creator can edit or delete. `resolved_at` is stamped on transition into a terminal status, cleared on transition back. Owner-only edit/delete in v1; assignee-aware permissions (via Forgejo group membership) is the natural follow-up. **Devices** — Intune-managed device inventory cached from Microsoft Graph. `POST /api/devices/sync` pages through `deviceManagement/managedDevices`, upserts on `entra_id`, and records every attempt to `device_sync_runs` (success/fail + `error_message`). The `/devices` page surfaces last-sync metadata above the table. The Entra integration uses client-credentials against the existing `ESPOHelpDesk` app registration documented in `/Users/vinnieespo/ESPO/HelpDesk/Cloud/FUTURE-HELPDESK-APP.md`. Required env vars: `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`. ## Review-standards checklist Every API route has been written against `13-review-standards`: - [x] Parameterised SQL only — no string interpolation of user input - [x] `Object.create(null)` sortable allowlist on every list route - [x] ILIKE `%`/`_`/`\` escape with `ESCAPE '\\'` on every q-search - [x] Session check first on every mutating route (POST/PATCH/DELETE), short-circuit 401 - [x] No `created_by` / `assignee_id` in any client response — joined for display names instead - [x] Zod validators tighter than DB constraints (zero-width refine, NUL-byte regex, email format, allowlist enums) - [x] No `any`/`as`/`@ts-ignore` without justification ## Test coverage (`bun:test`, full module mocking) - `tickets-api.test.ts` — 23 cases: 401 on every route, validation, prototype-pollution guards, ILIKE escape, allowlist sort, resolved_at stamp/clear, owner-only edit/delete - `devices-api.test.ts` — 7 cases: 401, default-50 pagination, unknown-compliance 400, ILIKE escape, sync run open/close happy path, unknown compliance normalised to `unknown`, failure path returns 502/SYNC_FAILED + UPDATEs run with `succeeded=FALSE` + `error_message` ## Caveats This repo is `vinnie/HelpDesk` (personal namespace) rather than `espo-corp/helpdesk` because the platform's `create_app` MCP tool currently fails with a 403 missing `write:organization` scope on its Forgejo token (issue [openplatform/op-api#94](https://forgejo.espodev.com/openplatform/op-api/issues/94)). Consequently: - No platform-managed DB / S3 / OAuth provisioning ran. - Not registered with Flux / gitops, so no auto-deploy. - Once op-api#94 is fixed, the proper move is to recreate this in `espo-corp` via `create_app` and replay these commits there. ## Test plan - [ ] `bun install && bun typecheck` — passes - [ ] `bun test` — 30 cases pass - [ ] Visit `/` unauthenticated → sign-in card renders - [ ] Sign in → dashboard renders status counts + recent tickets table - [ ] File a ticket via `/tickets/new` → redirects to `/tickets/[id]` - [ ] Edit + delete from the detail page works for the owner only - [ ] `/devices` shows the empty state until Entra creds are configured + sync runs once
Pivoting from openplatform/app-template — the items example has served
its purpose proving the stack end-to-end. Removing it cleanly so the
helpdesk pivot in the next commit doesn't carry stale imports.
- schema.sql: tickets (status/priority/category/requester/assignee),
  devices (Intune cache, raw jsonb), device_sync_runs (audit trail).
- src/lib/schemas.ts: Ticket + Device + GraphDevice zod schemas.
  Mirrors items pattern: zero-width refine, NUL-byte regex, allowlist
  enums for status/priority/compliance.
- src/lib/entra/: inlined Microsoft Graph client (client-credentials
  flow). Lifted from Rome/packages/entra; doing it in-app per the
  "avoid separate package repos" rule until a second app needs it.
- README.md and docs/overview.md retell what the app does, env vars
  needed, and why we mirror Graph locally instead of querying live.
- package.json: name set to "helpdesk"; seed:demo script removed
  along with the items example.
- /api/tickets    GET (paginated list w/ q + status filter)
                  POST (auth required, Zod-validated, parameterised)
- /api/tickets/:id GET / PATCH / DELETE
                   PATCH stamps resolved_at on terminal status,
                   clears it on transition back. Owner-only edit/delete.
- /api/devices     GET (paginated, q across name/user/serial, compliance filter)
- /api/devices/sync POST: opens device_sync_runs row, pages Graph,
                    upserts every managedDevice on entra_id, closes
                    the run with success/failure + error_message.

All routes follow review-standards: session check first, parameterised
SQL, Object.create(null) sortable allowlist, ILIKE wildcard escape,
no created_by / assignee_id in response projections.
- /                Dashboard. Status counts (open/in_progress/waiting/
                   resolved/closed) + ten most-recent tickets. Replaces
                   the old items home.
- /tickets         Full paginated list with q + status filters and
                   allowlisted sort.
- /tickets/[id]    Detail view. Selects created_by server-side to
                   compute canEdit, strips it before passing to client.
- /tickets/new     Form. Pre-fills requester_email with the signed-in
                   user's email.
- /devices         Cached Intune inventory. Surfaces last sync run
                   summary (timestamp, success/failure, error_message)
                   above the table; SyncDevicesButton triggers a fresh
                   pull.
feat: helpdesk components + bun:test coverage
Some checks failed
docs-lint / lint (pull_request) Successful in 45s
preview / preview (pull_request) Failing after 1m7s
typecheck / typecheck (pull_request) Failing after 1m26s
e2e / smoke (pull_request) Failing after 2m36s
69bf35f20c
Components mirror the items pattern: TicketsTable wraps the shared
DataTable, TicketDetail handles in-place status changes via a Select,
edit/delete dialogs reuse the form. DevicesTable is read-only with a
SyncDevicesButton that fires POST /api/devices/sync and refreshes.

Tests (bun:test, full module mocking of @/lib/db, @/auth, @/lib/entra):

tickets-api.test.ts -- 23 cases covering 401 on every route, 400 on
invalid JSON / empty title / zero-width title / NUL in description /
bad email / unknown priority, POST RETURNING does not leak created_by
or assignee_id, GET projection same, ILIKE wildcard escape, allowlist
sort, prototype-pollution guards, PATCH stamps and clears resolved_at
on terminal-status transitions, NO_OP on empty PATCH, owner-only edit
and delete.

devices-api.test.ts -- 7 cases covering 401, default-50 pagination,
unknown-compliance 400, ILIKE escape, sync run row open/close on the
happy path, unknown compliance state normalized to 'unknown', and
failure path UPDATEs the run with succeeded=FALSE + error_message
returned as 502/SYNC_FAILED.
fix(entra): annotate getAllPages locals to satisfy noImplicitAny
Some checks failed
docs-lint / lint (pull_request) Successful in 37s
preview / preview (pull_request) Failing after 50s
typecheck / typecheck (pull_request) Successful in 1m27s
e2e / smoke (pull_request) Successful in 3m8s
preview-cleanup / cleanup (pull_request) Failing after 32s
2b77572ebb
`tsc --noEmit` failed with TS7022 on the `page` and `link` locals inside
EntraClient.getAllPages. With strict + noImplicitAny TypeScript can't
flow-infer these through the loop; explicit annotations fix it without
changing behaviour.
vinnie merged commit 6ab9b8137a into main 2026-04-28 01:04:20 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
vinnie/HelpDesk!1
No description provided.