Files
servicedesk/ARCHITECTURE.md
Kacper 13a758779d
Some checks failed
Build and push image / build (push) Failing after 3m17s
v1.0.1
Documentation overhaul (TESTING/CONTRIBUTING/ARCHITECTURE/SECURITY/CLAUDE.md,
CHANGELOG.md, drop unmaintained src/README.md) plus CI-built Docker images:
Gitea Actions now builds and pushes the servicedesk image to the Gitea
container registry on Dockerfile changes, and compose.yaml pulls that image
instead of building locally. No application behavior changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 01:29:43 +02:00

5.8 KiB

Architecture

Server-rendered Laravel + Livewire app (no SPA/API-driven frontend for the app itself — the REST API in routes/api.php exists purely for external integrations). See README.md for the feature list and tech stack; this doc covers how the pieces fit together.

Request flow

  1. routes/web.php gates every area behind auth + a role middleware (role:client, role:operator, role:adminApp\Http\Middleware\EnsureRole), which checks the role against $user->roles. A user can hold multiple roles at once; the router just requires one of the listed roles per route group.
  2. Each route resolves to a full-page Livewire component under app/Livewire/{Client,Operator,Admin,Auth}/ — there are no traditional controllers rendering Blade views for these areas (the REST API in routes/api.php is the exception, backed by app/Http/Controllers/Api/).
  3. Livewire components call into app/Services/TicketService.php for anything that mutates ticket state (create/transition/reply/notify) rather than mutating models directly — keep that convention when adding new mutations so notification/history/SLA side effects stay in one place.
  4. App\Providers\AppServiceProvider::boot() runs a settings override pass on every request (applyLdapSettingsOverride, applyMailSettingsOverride, applySessionSettingsOverride, applyTimezoneSettingsOverride) — see "Settings override" below.

Data model

Core tables/models (app/Models/):

Category ─< Subcategory ─< CustomField        (per-subcategory custom fields)
                 │
                 └──< Ticket >── Team          (subcategory routes to a team)
                          │
                          ├──< TicketMessage   (public replies + internal notes)
                          ├──< TicketAttachment
                          ├──< TicketHistory
                          ├── customer/assignee → User
                          ├── status  → Status   (fixed stages: new/open/closed)
                          └── priority → Priority → SlaRule (response/resolution minutes)

User ─< UserFieldValue >─ UserField
ApiClient                                       (Sanctum token owner, ability-scoped)
Setting                                          (single-row-per-key config store, see below)
ReplyQuickAction, ResponseTemplate, EmailTemplate, NotificationSetting

Ticket (app/Models/Ticket.php) is the largest model — it owns SLA math (slaInfo(), isOverdue(), resolutionDeadline()), status/priority display helpers (statusLabel(), tagStyleFromColor()), operator-visibility scoping (scopeVisibleToOperator, isVisibleToOperator — a team member sees their team's queue + unassigned + anything assigned to them, an admin sees everything), and work-timer tracking (timerElapsedSeconds()). Keep ticket-shaped logic here rather than spreading it across Livewire components.

Roles & permissions

Roles are a plain array on the user ($user->roles), not a separate pivot-backed package — checked via EnsureRole at the route level. Every account gets client by default (App\Ldap\Handlers\AssignDefaultRole for LDAP-provisioned accounts); staff switch areas via the header role switcher, but always land on /client first after login.

Authentication

config/auth.php defines the default web guard against an LDAP-backed user provider (LdapRecord); a plain Eloquent provider is kept alongside it only for local tooling/tests that don't hit a directory. In production, LDAP bind is the primary path; the local fallback account (admin@example.com from the seeder) authenticates against a local password when the LDAP bind doesn't match — this is the account used for first login after a fresh install (see install.md).

app/Ldap/Handlers/ hooks into LdapRecord's import/sync events: AssignDefaultRole grants the client role to new LDAP-provisioned accounts, SyncUserFieldsFromLdap keeps UserFieldValue rows in sync with directory attributes.

Settings override ("live config")

App\Support\Settings (app/Support/Settings.php) is a cached key/value reader over the settings table, with hardcoded defaults for every key (company name, LDAP/SMTP connection details, attachment limits, session lifetime, timezone, branding/email HTML, etc.). Admin > Konfiguracja writes to this table, and AppServiceProvider::boot() re-applies the relevant subset of it over config() on every request — meaning Setting rows win over .env for LDAP, mail, session lifetime and timezone once they're non-empty. This is by design (lets an admin reconfigure LDAP/SMTP without a redeploy) but is also the source of the "seeded placeholder overrides real .env values" gotcha documented in install.md — anything touching LDAP/mail/session/ timezone config should go through Settings, not raw config()/.env reads.

SLA

SlaRule holds per-priority response/resolution targets in minutes. The scheduled command tickets:check-sla-breaches (registered in routes/console.php, run every 15 minutes via schedule:run) flags overdue tickets and can notify the assigned operator — see install.md for why this requires an external cron entry (the Docker image ships no cron/supervisor of its own).

API

routes/api.php + app/Http/Controllers/Api/ expose a small ability-scoped REST surface over Sanctum tokens (tickets:read, tickets:write, dictionaries:read, users:read), issued via admin-managed ApiClient records. Rate limiting is configured per-client (120 req/min keyed by client ID) vs. a tighter per-IP limit for unauthenticated requests (AppServiceProvider::configureApiRateLimiting()). Interactive docs are generated by L5-Swagger at /admin/api-docs; there is no static Markdown API reference in-repo.