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

113 lines
5.8 KiB
Markdown

# 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](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:admin``App\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](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](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](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.