Some checks failed
Build and push image / build (push) Failing after 3m17s
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>
113 lines
5.8 KiB
Markdown
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.
|