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>
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
routes/web.phpgates every area behindauth+ 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.- 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 inroutes/api.phpis the exception, backed byapp/Http/Controllers/Api/). - Livewire components call into
app/Services/TicketService.phpfor 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. 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.