weitere updates für instanzierbarkeit und sauberes testing
This commit is contained in:
120
CLAUDE.md
Normal file
120
CLAUDE.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# CLAUDE.md — IMVS (ImmobilienMaklerVerkaufssoftware)
|
||||
|
||||
Multi-tenant admin application built on MintyPHP. Manages tenants, users, departments, roles, permissions, address books, mail, CSV imports, scheduled jobs, and Microsoft Entra ID SSO.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime:** PHP 8.5 (FPM) on MintyPHP framework (`mintyphp/core`)
|
||||
- **Web server:** Nginx 1.25 (Alpine)
|
||||
- **Database:** MariaDB 11.4
|
||||
- **Cache:** Memcached 1.6
|
||||
- **Frontend:** Vanilla JS (ES2022 modules), vanilla CSS with `@layer` system — no build step
|
||||
- **Key PHP libs:** PHPMailer 7, Dompdf 3, endroid/qr-code 6, league/commonmark 2
|
||||
- **Containerized:** Docker Compose (dev + prod variants)
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
# Start dev environment
|
||||
docker compose up --build -d
|
||||
# App: http://localhost:8080 | phpMyAdmin: http://localhost:8081
|
||||
|
||||
# Run PHPUnit tests
|
||||
docker compose exec php vendor/bin/phpunit --bootstrap vendor/autoload.php
|
||||
|
||||
# Run PHPStan (level 5)
|
||||
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
|
||||
|
||||
# Run ESLint
|
||||
npx eslint web/js
|
||||
|
||||
# Check unused Composer packages
|
||||
docker compose exec php vendor/bin/composer-unused
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
lib/ # All backend PHP (namespace MintyPHP\)
|
||||
Repository/ # SQL queries, prepared statements, filters/paging
|
||||
Service/ # Business logic, validation, orchestration
|
||||
Http/ # Auth guards, API auth, request helpers
|
||||
Support/ # Crypto, flash, guard, search, helpers
|
||||
pages/ # Actions (controllers) — file-based routing
|
||||
pages/**/*.php # Action files (read input, check permissions, call services)
|
||||
pages/**/*.phtml # View files (rendering only, no DB calls)
|
||||
templates/ # Layouts and partials
|
||||
partials/ # Reusable UI components
|
||||
emails/ # Email HTML templates
|
||||
pdfs/ # PDF templates (Dompdf)
|
||||
config/ # App config (routes, assets, settings cache, themes)
|
||||
web/ # Document root (entry point, CSS, JS, static assets)
|
||||
js/core/ # DOM utilities, Grid.js factory
|
||||
js/components/ # Reusable UI components
|
||||
css/ # Layered CSS (base → components → layout → pages)
|
||||
db/init/init.sql # Full schema + seed data (no migration framework)
|
||||
tests/ # PHPUnit tests (namespace MintyPHP\Tests\)
|
||||
docs/ # German-language documentation (Markdown)
|
||||
i18n/ # Translation files (de, en)
|
||||
bin/ # CLI scripts (scheduler)
|
||||
docker/ # Dockerfiles, Nginx configs, PHP configs
|
||||
```
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
### Strict Layering (enforce in all changes)
|
||||
|
||||
1. **Repository** (`lib/Repository/`): All SQL lives here. No HTTP, session, or rendering.
|
||||
2. **Service** (`lib/Service/`): Business rules and validation. No HTML. Uses factory pattern (`*ServicesFactory`).
|
||||
3. **Action** (`pages/**/*.php`): Reads input, checks permissions + tenant scope, calls services, sets view vars.
|
||||
4. **View** (`pages/**/*.phtml`): Pure rendering. **No DB calls.** HTML escaping via `e(...)`.
|
||||
5. **Template** (`templates/`): Layouts and partials.
|
||||
|
||||
### Routing
|
||||
|
||||
- File-based: `pages/admin/users/edit($id).php` → URL parameter `id`
|
||||
- `...(none).phtml` → no template layout
|
||||
- `data().php` suffix → data/AJAX endpoints
|
||||
- Named aliases in `config/routes.php` with `public` flag for auth guard
|
||||
|
||||
### PHP Conventions
|
||||
|
||||
- PSR-4 autoload: `MintyPHP\` → `lib/`, `MintyPHP\Tests\` → `tests/`
|
||||
- All user-facing text uses `t('key')` translation helper
|
||||
- Permissions + tenant scope enforced in actions/services, never just in UI
|
||||
- Security/integration settings stay in DB, not in `config/settings.php` file cache
|
||||
|
||||
### Frontend Conventions
|
||||
|
||||
- Vanilla ES2022 modules, no bundler
|
||||
- CSS classes prefixed with `app-`
|
||||
- Defensive DOM checks (elements may not exist on every page)
|
||||
- No business logic in frontend
|
||||
- Assets served raw with `assetVersion()` cache-busting (filemtime-based)
|
||||
|
||||
### UI Patterns
|
||||
|
||||
- Edit pages: tabs → `Master data` | `Visibility` | optional `Danger zone`
|
||||
- Titlebar partial for primary actions (Save, Save & close)
|
||||
- Secondary actions in Aside block via `app-details-aside-actions.phtml`
|
||||
- Lists use Grid.js
|
||||
- All visible text wrapped in `t('...')`
|
||||
|
||||
## Database
|
||||
|
||||
- Schema defined in `db/init/init.sql` (applied once on container init)
|
||||
- No migration framework — schema changes for existing installs are idempotent SQL scripts
|
||||
- Settings stored in `settings` DB table (single source of truth); `config/settings.php` is a partial file cache for hot-path UI reads only
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- **PHPStan level 5** — scans `config/`, `lib/`, `pages/`, `tests/`
|
||||
- **PHPUnit 11** — bootstrap: `tests/bootstrap.php`
|
||||
- **ESLint** — ES2022, strict `eqeqeq`, `curly`, `no-unused-vars`, `no-console` (warn, allow warn/error)
|
||||
- **composer-unused** — periodic check for unused dependencies
|
||||
|
||||
## Environment
|
||||
|
||||
- Config via `.env` (gitignored) — copy from `.env.example` for dev, `.env.prod.example` for prod
|
||||
- `config/config.php` (gitignored) — copy from `config/config.php.example`
|
||||
- `APP_CRYPTO_KEY` (64-char hex) needed for AES-256-GCM encryption of SSO secrets
|
||||
@@ -724,6 +724,7 @@ VALUES
|
||||
('permissions.delete', 'Can delete permissions', 1, 1),
|
||||
('settings.view', 'Can view settings', 1, 1),
|
||||
('settings.update', 'Can update settings', 1, 1),
|
||||
('tenant.scope.global', 'Can bypass tenant scope globally', 1, 1),
|
||||
('imports.view', 'Can view imports', 1, 1),
|
||||
('imports.audit.view', 'Can view import audit logs', 1, 1),
|
||||
('jobs.view', 'Can view scheduled jobs', 1, 1),
|
||||
@@ -801,7 +802,7 @@ JOIN permissions p ON p.`key` IN (
|
||||
'departments.view', 'departments.create', 'departments.update', 'departments.delete', 'departments.import',
|
||||
'roles.view', 'roles.create', 'roles.update', 'roles.delete',
|
||||
'permissions.view', 'permissions.create', 'permissions.update', 'permissions.delete',
|
||||
'settings.view', 'settings.update',
|
||||
'settings.view', 'settings.update', 'tenant.scope.global',
|
||||
'imports.view', 'imports.audit.view', 'jobs.view', 'jobs.manage', 'jobs.run_now',
|
||||
'user_lifecycle_audit.view', 'users.import', 'users.import_assignments',
|
||||
'users.lifecycle_restore',
|
||||
|
||||
15
db/updates/2026-02-23-tenant-scope-global.sql
Normal file
15
db/updates/2026-02-23-tenant-scope-global.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Idempotent update: introduce explicit global tenant-scope bypass permission.
|
||||
|
||||
INSERT INTO `permissions` (`key`, `description`, `active`, `is_system`)
|
||||
VALUES ('tenant.scope.global', 'Can bypass tenant scope globally', 1, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`description` = VALUES(`description`),
|
||||
`active` = VALUES(`active`),
|
||||
`is_system` = VALUES(`is_system`);
|
||||
|
||||
INSERT INTO `role_permissions` (`role_id`, `permission_id`, `created`)
|
||||
SELECT r.id, p.id, NOW()
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.`key` = 'tenant.scope.global'
|
||||
WHERE r.description IN ('Admin', 'Administrator') OR r.id = 1
|
||||
ON DUPLICATE KEY UPDATE role_id = role_id;
|
||||
@@ -1,6 +1,6 @@
|
||||
# Anfragelimits (Rate Limiting)
|
||||
|
||||
Letzte Aktualisierung: 2026-02-21
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
Dieses Dokument beschreibt das aktuelle zentrale Rate-Limiting für Login und API.
|
||||
|
||||
@@ -64,6 +64,7 @@ Bei überschreitung:
|
||||
|
||||
Integration:
|
||||
- `/pages/auth/login().php`
|
||||
- `/pages/api/v1/auth/login().php` (public Login ohne Bearer)
|
||||
|
||||
Aktuelle Limits:
|
||||
|
||||
@@ -84,6 +85,27 @@ Verhalten:
|
||||
- UI zeigt generische Meldung: `Too many login attempts. Please wait and try again.`
|
||||
- Bei erfolgreichem Passwort-Login wird der Passwort-Scope für diesen Key zurückgesetzt.
|
||||
|
||||
### API-Login (`/api/v1/auth/login`)
|
||||
|
||||
Zusätzlich zum allgemeinen API-Limit in `ApiBootstrap` gelten eigene Login-Limits:
|
||||
|
||||
1. Schritt `login_ip` (IP)
|
||||
- Scope: `api.auth.login.ip`
|
||||
- Key: `<ip>`
|
||||
- Limit: `10` Fehlversuche pro `600` Sekunden
|
||||
- Block: `300` Sekunden
|
||||
|
||||
2. Schritt `login_email_ip` (email + ip)
|
||||
- Scope: `api.auth.login.email_ip`
|
||||
- Key: `<lowercase-email>|<ip>`
|
||||
- Limit: `5` Fehlversuche pro `900` Sekunden
|
||||
- Block: `900` Sekunden
|
||||
|
||||
Verhalten:
|
||||
- Endpoint bleibt public (kein Bearer erforderlich).
|
||||
- Bei Block: HTTP `429` + `Retry-After`.
|
||||
- Bei erfolgreichem API-Login wird `api.auth.login.email_ip` für den Key zurückgesetzt.
|
||||
|
||||
## Datenfluss pro Request
|
||||
|
||||
1. Key bilden (`scope` + Subject)
|
||||
|
||||
@@ -1,201 +1,58 @@
|
||||
# Architektur
|
||||
# Architektur-Kompass
|
||||
|
||||
Letzte Aktualisierung: 2026-02-22 (geprüft)
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
## 1) Zielbild
|
||||
Kurze, verbindliche Architekturübersicht für den aktuellen Stand.
|
||||
|
||||
Die Anwendung ist eine servergerenderte Multi-Tenant-Admin-Anwendung mit klarer Trennung von:
|
||||
## Begriffe (kurz)
|
||||
|
||||
- HTTP/Request-Flow (`/web/index.php`, `lib/Http/*`)
|
||||
- Action-Layer (`/pages/**`)
|
||||
- Business-Logik (`/lib/Service/**`)
|
||||
- Datenzugriff (`/lib/Repository/**`)
|
||||
- Rendering (`/templates/**`, `*.phtml`)
|
||||
- Frontend-Verhalten (`/web/js/**`)
|
||||
- `Tenant`: Mandant, isolierter Daten- und Berechtigungsraum.
|
||||
- `Department`: optionale organisatorische Untereinheit innerhalb eines Tenants.
|
||||
- `Role`: Bündel von Berechtigungen.
|
||||
- `Permission`: einzelne Berechtigung wie `users.view`.
|
||||
- `Policy`: zentrale Autorisierungsregel für eine konkrete Fähigkeit.
|
||||
- `Ability`: technischer Name einer Policy-Regel, z. B. `admin.users.edit.submit`.
|
||||
- `AuthorizationDecision`: Ergebnis einer Policy (`allow/deny`, HTTP-Status, Fehlercode, Attribute).
|
||||
- `Tenant-Scope`: Regel, ob ein User eine Ressource im Ziel-Tenant sehen oder ändern darf.
|
||||
- `Request`: einzelner HTTP-Aufruf.
|
||||
- `Action`: Datei unter `pages/**`, verarbeitet den Request.
|
||||
- `Service`: Fachlogik unter `lib/Service/**`.
|
||||
- `Repository`: SQL- und Mapping-Schicht unter `lib/Repository/**`.
|
||||
- `UUID-first`: externe API gibt nur UUIDs aus, keine internen IDs.
|
||||
- `Contract-Test`: Test für Architektur- und Schnittstellenregeln.
|
||||
|
||||
## 2) Request-Lebenszyklus
|
||||
## Zielbild (Stand heute)
|
||||
|
||||
Zentraler Entry Point ist `/web/index.php`.
|
||||
- Server-gerenderte App plus REST-API unter `/api/v1`.
|
||||
- Strikte Multi-Tenant-Isolation.
|
||||
- Zentrale Autorisierung über `AuthorizationService` und Policies.
|
||||
- API-Verträge sind UUID-first.
|
||||
|
||||
Ablauf pro Request:
|
||||
## Request-Flow (verbindlich)
|
||||
|
||||
1. Bootstrap (`vendor/autoload.php`, `config/config.php`, `config/router.php`, Helper)
|
||||
2. Firewall + Session Start
|
||||
3. Locale-Ermittlung (URL/Session/Cookie/default)
|
||||
4. Remember-Me Auto-Login
|
||||
5. Optionales Tenant-Snapshot-Refresh in Session
|
||||
6. Redirect auf locale-prefixed URL (falls nötig)
|
||||
7. Access Guard (`AccessControl`) für Public/Protected Paths
|
||||
8. Action-Layer aus `pages/**` laden
|
||||
9. View + Template rendern
|
||||
10. Session beenden, DB schließen
|
||||
1. `web/index.php` lädt Bootstrap, Routing und Konfiguration.
|
||||
2. Firewall, Session, Locale und Access-Control laufen.
|
||||
3. Action unter `pages/**` nimmt Request an.
|
||||
4. Action delegiert Fachlogik an Services.
|
||||
5. Services nutzen Repositories für Datenzugriff.
|
||||
6. Response wird als HTML, JSON oder Datei ausgegeben.
|
||||
|
||||
## 3) Routing-Modell
|
||||
## Schichtregeln (MUSS)
|
||||
|
||||
- Route-Definitionen (Pfad-Aliase) liegen in `/config/routes.php`.
|
||||
- Registrierung erfolgt in `/config/router.php` (liest nur `path` und `target`; das `public`-Feld wird vom Router nicht ausgewertet).
|
||||
- Welche Pfade ohne Login erreichbar sind, steuert `lib/Http/AccessControl.php`:
|
||||
- Hartkodierte `ALWAYS_PUBLIC_PREFIXES` (z. B. `auth/microsoft/`, `api/`, `branding/`, `flash/`)
|
||||
- `APP_PUBLIC_PATHS`-Konstante (gesetzt in `config/config.php`)
|
||||
- `pages/**`: Input, Auth/AuthZ, Service-Aufruf, Response. Keine duplizierte Business-Logik.
|
||||
- `lib/Service/**`: Fachlogik und Orchestrierung. Keine Superglobals, keine Header/Redirect-Ausgabe.
|
||||
- `lib/Repository/**`: SQL, Filter, Paging, Mapping. Keine HTTP-, Session- oder Policy-Logik.
|
||||
- `templates/**`: nur Rendering.
|
||||
|
||||
MintyPHP-Konvention (Dateinamensrouting):
|
||||
## Sicherheits- und API-Regeln (MUSS)
|
||||
|
||||
- `pages/admin/users/edit($id).php` erwartet URL-Parameter `id`
|
||||
- `pages/admin/users/data().php` für Datenendpunkte
|
||||
- `...(none).phtml` für responses ohne Template-Layout
|
||||
- Binärausgaben (z. B. Onboarding-PDF/ZIP) setzen `MINTY_ALLOW_OUTPUT` im Action-Endpoint.
|
||||
- Autorisierung nur zentral über Policies.
|
||||
- Tenant-Scope nicht lokal nachbauen.
|
||||
- Scope-Denials pro Endpunkt-Semantik als `403` oder `404`.
|
||||
- API-Fehler einheitlich: `error`, optional `errors`.
|
||||
- OpenAPI wird bei API-Änderungen im selben Merge aktualisiert.
|
||||
|
||||
## 4) Schichten und Verantwortungen
|
||||
## Merge-Mindestchecks
|
||||
|
||||
### Action-Layer (`/pages`)
|
||||
|
||||
- Eingaben lesen/validieren
|
||||
- Permissions und Tenant-Scope prüfen
|
||||
- Services aufrufen
|
||||
- View-Daten vorbereiten
|
||||
|
||||
### Service-Layer (`/lib/Service`)
|
||||
|
||||
- Geschäftsregeln
|
||||
- Cross-Cutting-Logik (z. B. Auth, Settings, Branding)
|
||||
- Orchestrierung mehrerer Repositories
|
||||
- Scheduler/Lifecycle sind instanzbasiert über `lib/Service/Scheduler/SchedulerServicesFactory.php` verdrahtet.
|
||||
- Import/Import-Audit sind instanzbasiert über `lib/Service/Import/ImportServicesFactory.php` verdrahtet.
|
||||
- User-Domain ist instanzbasiert über `lib/Service/User/UserServicesFactory.php` verdrahtet
|
||||
(Services: `UserAccountService`, `UserAssignmentService`, `UserTenantContextService`, `UserPasswordService`;
|
||||
Repositories: `UserReadRepository`, `UserWriteRepository`, `UserListQueryRepository` + Assignment-Repositories).
|
||||
- Auth-/SSO-Domain ist instanzbasiert über `lib/Service/Auth/AuthServicesFactory.php` verdrahtet
|
||||
(u. a. `AuthService`, `RememberMeService`, `SsoUserLinkService`, `TenantSsoService`, `MicrosoftOidcService`).
|
||||
- Auth-Infra-Repositories (`ApiTokenRepository`, `RememberTokenRepository`, `PasswordResetRepository`,
|
||||
`EmailVerificationRepository`) werden ebenfalls instanzbasiert über die Factory injiziert.
|
||||
|
||||
### Repository-Layer (`/lib/Repository`)
|
||||
|
||||
- SQL und Mapping
|
||||
- Filter-/Paging-Mechaniken
|
||||
- Keine UI-/HTTP-Logik
|
||||
|
||||
### Template/View-Layer (`/templates`, `*.phtml`)
|
||||
|
||||
- Nur Rendering
|
||||
- Keine DB-Zugriffe
|
||||
- Wiederkehrende UI-Teile als Partials
|
||||
|
||||
## 5) Datenmodell (fachlich)
|
||||
|
||||
Kernobjekte:
|
||||
|
||||
- `users`
|
||||
- `tenants`
|
||||
- `departments`
|
||||
- `roles`
|
||||
- `permissions`
|
||||
|
||||
Verknüpfungen:
|
||||
|
||||
- `user_tenants`, `user_departments`, `user_roles`
|
||||
- `role_permissions`
|
||||
- `tenant_auth_microsoft`, `user_external_identities`
|
||||
- `tenant_custom_field_definitions`, `tenant_custom_field_options`
|
||||
- `user_custom_field_values`, `user_custom_field_value_options`
|
||||
|
||||
Hinweis:
|
||||
|
||||
- `departments` ist 1:1 an `tenants` gebunden über `departments.tenant_id` (kein M:N-Mapping).
|
||||
- In der User-Organisation wird die Department-Auswahl tenantweise gerendert (ein Multi-Select je zugewiesenem Tenant), gespeichert wird weiterhin flach über `user_departments`.
|
||||
- Tenant-spezifische User-Zusatzfelder werden separat modelliert:
|
||||
- Definitionen/Optionen pro Tenant in `tenant_custom_field_*`
|
||||
- Werte pro User in `user_custom_field_*`
|
||||
- Filterung im Address Book über dynamische Query-Keys `cf_*`, `cfm_*`, `cfd_*`.
|
||||
- `field_key` bleibt intern (tenant-weit eindeutig), wird im Tenant-UI automatisch aus dem Label erzeugt und nicht manuell gepflegt.
|
||||
- `is_filterable` ist fachlich nur für `select`, `multiselect`, `boolean`, `date` aktiv.
|
||||
- User-Zusatzfeldwerte sind in V1 optional (keine Pflichtvalidierung).
|
||||
- Tenant-Theme-Overrides werden direkt auf `tenants` gespeichert:
|
||||
- `tenants.default_theme` (`NULL` = globales `app_theme`)
|
||||
- `tenants.allow_user_theme` (`NULL` = globales `app_theme_user`).
|
||||
- Auflösung immer über `$_SESSION['current_tenant']` und damit tenant-spezifisch für Multi-Tenant-User.
|
||||
- Tenant-SSO für Microsoft Entra ID:
|
||||
- Konfiguration pro Tenant in `tenant_auth_microsoft`.
|
||||
- Externe Identitäts-Verknüpfung stabil über `user_external_identities` (`provider + tid + oid`).
|
||||
- Login-Endpunkte: `login` (optional `?tenant={slug}`), `auth/microsoft/start`, `auth/microsoft/callback`.
|
||||
- Shared-App-Credentials liegen global in `settings`; optionale Tenant-Overrides sind möglich.
|
||||
- Profil-Sync wird tenant-spezifisch über `tenant_auth_microsoft.sync_profile_on_login` und
|
||||
`tenant_auth_microsoft.sync_profile_fields` gesteuert.
|
||||
- Erlaubte Sync-Felder in V1: `first_name`, `last_name`, `phone`, `mobile`, `avatar`.
|
||||
- `phone/mobile/avatar` kommen über Microsoft Graph (`/me`, `/me/photo/$value`) und sind fail-open
|
||||
(Graph-Fehler blockieren den Login nicht).
|
||||
|
||||
Sicherheits-/Audit-nahe Tabellen:
|
||||
|
||||
- `user_remember_tokens`
|
||||
- `password_resets`
|
||||
- `email_verifications`
|
||||
- `mail_log`
|
||||
|
||||
## 6) Sicherheits- und Scope-Modell
|
||||
|
||||
- Auth Guard: protected by default
|
||||
- Permission Checks: feature-spezifisch in Actions/Services
|
||||
- Tenant Scope: datenabhängig über Tenant-Matches
|
||||
- Strictness steuerbar über `TENANT_SCOPE_STRICT`
|
||||
|
||||
Details siehe:
|
||||
`/docs/sicherheitsmodell.md`
|
||||
|
||||
## 7) Frontend-Architektur
|
||||
|
||||
JS-Struktur:
|
||||
|
||||
- `/web/js/core` Basismodule (DOM/Grid Factory)
|
||||
- `/web/js/components` UI-Komponenten
|
||||
- `/web/js/pages` seitenspezifische Helfer
|
||||
|
||||
Bootstrap:
|
||||
|
||||
- `app-boot.js` früh (no-js/js state, localStorage UI state)
|
||||
- `app-init.js` für Default-Template
|
||||
- `app-login-init.js` für Login-Template
|
||||
|
||||
CSS-Struktur:
|
||||
|
||||
- Layer-Entrypoint: `/web/css/app-layers.css`
|
||||
- Basen/Variablen in `/web/css/base`
|
||||
- Layout in `/web/css/layout`
|
||||
- Komponenten in `/web/css/components`
|
||||
- Seitenstile in `/web/css/pages`
|
||||
- Vendor-Overrides in `/web/css/vendor-overrides`
|
||||
|
||||
## 8) Asset-Steuerung
|
||||
|
||||
Konfiguration liegt in `/config/assets.php`.
|
||||
|
||||
- Template-Gruppen: `default`, `login`
|
||||
- Seiten-Gruppen optional via `Buffer::set('style_groups', ...)`
|
||||
- Rendering über Helper: `renderTemplateStyles()` und `renderPageStyles()`
|
||||
|
||||
## 9) Erweiterungsstrategie
|
||||
|
||||
Pragmatischer Weg für neue Features:
|
||||
|
||||
1. Repository erweitern (SQL)
|
||||
2. Service-Regel bauen
|
||||
3. Action anbinden
|
||||
4. View/Partial integrieren
|
||||
5. JS nur falls interaktiv nötig
|
||||
6. Tests + Lint/Analyse laufen lassen
|
||||
|
||||
So bleibt die Trennung stabil und Refactoring kontrollierbar.
|
||||
|
||||
## 10) Settings-Architektur (DB + Datei-Cache)
|
||||
|
||||
- `settings` (DB) ist die alleinige fachliche Quelle für globale Settings.
|
||||
- `config/settings.php` ist ein technischer Teil-Cache für sehr häufig gelesene UI-Basiswerte.
|
||||
- Zugriffsmuster:
|
||||
- `SettingGateway` (verdrahtet über `SettingServicesFactory`) -> `SettingService` -> DB
|
||||
- `appSetting(...)` -> `SettingServicesFactory` -> `SettingCacheService` (Datei-Cache)
|
||||
- Konsequenz:
|
||||
- Es ist korrekt, dass nicht alle DB-Settings im Datei-Cache stehen (z. B. SMTP/SSO/API-Details).
|
||||
- Der Cache muss nur die Keys enthalten, die im Runtime-Helper `appSetting(...)` verwendet werden.
|
||||
|
||||
Details:
|
||||
`/docs/einstellungen-speicherung.md`
|
||||
- `docker compose exec php vendor/bin/phpunit`
|
||||
- `docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress`
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Entwickler-Checkliste
|
||||
|
||||
Letzte Aktualisierung: 2026-02-22
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
## Vor dem Coden
|
||||
|
||||
- [ ] Betroffene Schicht klar? (`Repository`, `Service`, `pages`, `templates`, `web/js`)
|
||||
- [ ] Permission- und Tenant-Scope-Auswirkungen verstanden?
|
||||
- [ ] Bestehendes Partial/Helper wiederverwendbar?
|
||||
- [ ] Bei `lib/**`-Änderungen Regeln in `/docs/lib-standards.md` geprüft
|
||||
|
||||
## Beim Implementieren
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Ein neuer Entwickler soll den ersten Change strukturiert und reproduzierbar umse
|
||||
3. Optional Service/Repository ergänzen
|
||||
4. `docs/openapi.yaml` aktualisieren
|
||||
5. `docs/api.md` mit Beispiel aktualisieren
|
||||
6. Bei neuen Berechtigungen: `PermissionService` + `init.sql` synchron halten, für Bestandsumgebungen idempotentes SQL-Update bereitstellen
|
||||
6. Bei neuen Berechtigungen: `PermissionService` + `init.sql` synchron halten, für Bestandsumgebungen idempotentes SQL-Update in `db/updates/*.sql` bereitstellen
|
||||
|
||||
### Done-Kriterien
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fehlerbehebung
|
||||
|
||||
Letzte Aktualisierung: 2026-02-21
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
## App reagiert nicht wie erwartet
|
||||
|
||||
@@ -22,10 +22,11 @@ docker compose restart php nginx
|
||||
|
||||
### Checks
|
||||
|
||||
1. Authorization Header korrekt gesetzt (`Bearer <selector:secret>`)
|
||||
2. Token nicht revoked/abgelaufen
|
||||
3. Token hat erforderliche Permission
|
||||
4. Bei tenant-scoped Token: Zugriff nur auf passende Tenant-Ressourcen
|
||||
1. Für geschützte Endpunkte Authorization Header korrekt gesetzt (`Bearer <selector:secret>`)
|
||||
2. Ausnahme: `/api/v1/auth/login` ist public und benötigt keinen Bearer-Header
|
||||
3. Token nicht revoked/abgelaufen
|
||||
4. Token hat erforderliche Permission
|
||||
5. Bei tenant-scoped Token: Zugriff nur auf passende Tenant-Ressourcen
|
||||
|
||||
## Migration oder Schema-Probleme
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dokumentation
|
||||
|
||||
Letzte Aktualisierung: 2026-02-22
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
Diese Dokumente sind für Entwickler gedacht, die die Anwendung warten oder erweitern.
|
||||
|
||||
@@ -20,11 +20,13 @@ Diese Dokumente sind für Entwickler gedacht, die die Anwendung warten oder erwe
|
||||
## Architektur und Regeln
|
||||
|
||||
- `/docs/architektur.md`
|
||||
- Request-Lebenszyklus, Schichtentrennung, Frontend-Aufbau.
|
||||
- Startpunkt mit Glossar, Zielbild, Schichtregeln und Sicherheitsleitplanken.
|
||||
- `/docs/sicherheitsmodell.md`
|
||||
- Auth, Permissions, Tenant-Scope, Public/API-Zugriffe.
|
||||
- `/docs/konventionen.md`
|
||||
- Coding- und Strukturkonventionen für PHP, Templates, CSS, JS.
|
||||
- Kurze, verbindliche Arbeitsregeln für Code, API und Qualität.
|
||||
- `/docs/lib-standards.md`
|
||||
- Konkrete Do/Don't-Regeln für `lib/**`.
|
||||
|
||||
## Frontend
|
||||
|
||||
|
||||
@@ -1,129 +1,41 @@
|
||||
# Konventionen
|
||||
# Konventionen (kurz und verbindlich)
|
||||
|
||||
Letzte Aktualisierung: 2026-02-22
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
Diese Regeln sind projektweit verbindlich, um Lesbarkeit und Wartbarkeit stabil zu halten.
|
||||
## 1) Code
|
||||
|
||||
## 1) Allgemein
|
||||
- Kleine, fokussierte Änderungen statt Misch-Commits.
|
||||
- Kein toter Code, keine ungenutzten Helpers.
|
||||
- ASCII als Standard, Unicode nur wenn Datei es bereits nutzt.
|
||||
|
||||
- ASCII als Standard, außer bestehende Datei nutzt bereits Unicode.
|
||||
- Keine toten Pfade/Codezweige einbauen.
|
||||
- Wiederholung lieber zentralisieren (Partial/Helper/Utility) als kopieren.
|
||||
- Kleine, fokussierte Commits/Changesets bevorzugen.
|
||||
## 2) Schichten
|
||||
|
||||
## 2) PHP-Schichten
|
||||
- SQL nur in `lib/Repository/**`.
|
||||
- Fachlogik nur in `lib/Service/**`.
|
||||
- `pages/**` bleibt dünn: Input, Auth/AuthZ, Service-Aufruf, Response.
|
||||
- Neue Domain-Logik als Instanz-Service mit Factory-Verdrahtung.
|
||||
|
||||
### Repository
|
||||
## 3) Autorisierung
|
||||
|
||||
- Enthalten SQL und Filterlogik.
|
||||
- Keine HTTP-/Session-/Render-Logik.
|
||||
- Prepared Statements und bestehende Query-Utilities nutzen.
|
||||
- Nur zentrale Policy-Abilities nutzen.
|
||||
- Keine verteilten `userHas(...)`-Checks als Hauptlogik.
|
||||
- Tenant-Scope nicht in Templates oder JavaScript nachbauen.
|
||||
|
||||
### Service
|
||||
## 4) API
|
||||
|
||||
- Geschäftsregeln, Validierung, Orchestrierung.
|
||||
- Keine HTML-Ausgabe.
|
||||
- Keine direkten View-Annahmen.
|
||||
- Bei globalen Settings bleibt DB die Quelle (`SettingService`); Datei-Cache nur gezielt für `appSetting(...)`.
|
||||
- Für neue oder refaktorierte Domain-Module Instanz-Services + Factory verwenden (z. B. `...ServicesFactory`) statt statischer Domain-Methoden.
|
||||
- Keine domain-spezifischen Service-Locator-Helper in `lib/Support/helpers/app.php`; Composition per Factory im Aufrufer.
|
||||
- UUID-first für externe Ressourcen.
|
||||
- Interne IDs nicht nach außen geben.
|
||||
- Fehlerantworten einheitlich halten (`error`, optional `errors`).
|
||||
- OpenAPI bei jeder API-Änderung im selben Merge aktualisieren.
|
||||
|
||||
### Action (`pages/**.php`)
|
||||
## 5) Frontend
|
||||
|
||||
- Request lesen, Zugriff prüfen, Service aufrufen, Variablen für View setzen.
|
||||
- Redirects/Flash-Messages hier zentral steuern.
|
||||
- Templates nur für Rendering.
|
||||
- UI-Texte über `t('...')`.
|
||||
- Strukturregeln in `docs/frontend-css.md` und `docs/frontend-javascript.md`.
|
||||
|
||||
### Views (`*.phtml`)
|
||||
## 6) Mindestchecks vor Merge
|
||||
|
||||
- Reines Rendering.
|
||||
- Keine DB-Calls.
|
||||
- HTML escapen (`e(...)`) außer bewusst gewünscht.
|
||||
|
||||
## 3) UI-Konventionen
|
||||
|
||||
### Edit-Seiten
|
||||
|
||||
- Einheitliche Titelzeile mit `/templates/partials/app-details-titlebar.phtml`.
|
||||
- In der Titelzeile nur Primäraktionen halten (typisch: `Save`, `Save & close`).
|
||||
- Sekundäraktionen nicht im Drei-Punkte-Menü verstecken, sondern im Aside als einklappbarer Block `Actions` über `/templates/partials/app-details-aside-actions.phtml`.
|
||||
- Partial-Naming klar trennen:
|
||||
- linkes Haupt-Aside: `app-main-aside*`
|
||||
- rechtes Details-Aside: `app-details-aside*`
|
||||
- Tab-Reihenfolge:
|
||||
- erster Tab `Master data`
|
||||
- später `Visibility`
|
||||
- optional letzter Tab `Danger zone`
|
||||
- Statusfeld über `/templates/partials/app-visibility-status-field.phtml`.
|
||||
- Delete-Aktion über `/templates/partials/app-danger-zone-delete-field.phtml`.
|
||||
- Aside-Metadaten über:
|
||||
- `/templates/partials/app-details-aside-audit.phtml`
|
||||
- `/templates/partials/app-details-aside-ids.phtml`
|
||||
- Tenant-Zusatzfelder:
|
||||
- Definitionen werden im Tenant-Tab `Custom fields` über eigene POST-Actions gepflegt (nicht über Tenant-Hauptsave).
|
||||
- `field_key` wird nicht im UI gepflegt; serverseitig aus Label erzeugt und tenant-weit eindeutig gehalten.
|
||||
- `is_filterable` nur für `select`, `multiselect`, `boolean`, `date` anbieten.
|
||||
- User-Werte werden im User-Tab `Custom fields` tenantweise gruppiert gerendert.
|
||||
- User-Zusatzfelder sind optional; keine Pflichtvalidierung in V1.
|
||||
|
||||
### Listen
|
||||
|
||||
- Grid.js als Standard.
|
||||
- Filter/Toolbar konsistent halten.
|
||||
- Aktionsspalten nur wenn wirklich nötig; sonst Double-Click/Row-Action nutzen.
|
||||
- Dynamische Address-Book-Filter für Zusatzfelder folgen festen Parametern:
|
||||
- `cf_<definition_uuid>` (scalar/select/bool)
|
||||
- `cfm_<definition_uuid>` (multiselect als CSV)
|
||||
- `cfd_<definition_uuid>_from|to` (date range)
|
||||
|
||||
## 4) CSS
|
||||
|
||||
- Prefix `app-` für eigene Klassen/Dateien.
|
||||
- Vendor-Anpassungen in `/web/css/vendor-overrides`.
|
||||
- Layering über `/web/css/app-layers.css`.
|
||||
- Theme-/Contrast-Werte über zentrale Variablen in `/web/css/base`.
|
||||
|
||||
## 5) JavaScript
|
||||
|
||||
- Struktur:
|
||||
- `core/` für Basismodule
|
||||
- `components/` für wiederverwendbare UI-Module
|
||||
- `pages/` für seitenspezifische Logik
|
||||
- Initialisierung zentral in `app-init.js` bzw. `app-login-init.js`.
|
||||
- Defensive DOM-Checks (Element kann auf manchen Seiten fehlen).
|
||||
- Keine Business-Logik im Frontend verstecken.
|
||||
|
||||
## 6) i18n
|
||||
|
||||
- Alle sichtbaren UI-Texte über `t('...')`.
|
||||
- Übersetzungs-Keys in `i18n/default_de.json` und `i18n/default_en.json` pflegen.
|
||||
- Test nutzen: `tests/I18n/TranslationKeysTest.php`.
|
||||
|
||||
## 7) Qualitätssicherung
|
||||
|
||||
Vor Merge mindestens:
|
||||
|
||||
- PHPUnit
|
||||
- PHPStan
|
||||
- ESLint (web/js)
|
||||
- Betroffene Kern-Userflows manuell testen
|
||||
|
||||
Checkliste siehe:
|
||||
`/docs/entwickler-checkliste.md`
|
||||
|
||||
### Dependency-Hygiene (periodisch, nicht bei jedem Commit)
|
||||
|
||||
Ungenutzte Composer-Pakete prüfen:
|
||||
|
||||
```bash
|
||||
docker compose exec php vendor/bin/composer-unused
|
||||
```
|
||||
|
||||
Ausführen bei: Feature-Entfernung, größerem Refactoring oder vor Releases. Pakete die fälschlicherweise als unused gemeldet werden (z.B. dynamisch geladen), können in `composer.json` unter `extra.unused` ignoriert werden.
|
||||
|
||||
## 8) Settings-Konvention
|
||||
|
||||
- Neue Settings immer zuerst sauber im `SettingService` modellieren (Validierung + Read/Write).
|
||||
- Aufrufer nutzen `SettingGateway` aus `SettingServicesFactory`; kein globaler `settingGateway()`-Helper.
|
||||
- Nur dann in den `SettingCacheService` aufnehmen, wenn der Key wirklich über `appSetting(...)` im Hot Path genutzt wird.
|
||||
- Keine Sicherheits-/Integrationssettings (SMTP, SSO-Secrets, API-Policies) als Pflicht in den Datei-Cache drücken.
|
||||
- Bei manuellen DB-Änderungen an gecachten Keys den Cache gezielt aktualisieren (z. B. über Settings-Save-Flow).
|
||||
- `docker compose exec php vendor/bin/phpunit`
|
||||
- `docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress`
|
||||
- bei JS-Änderungen: `npx eslint web/js`
|
||||
|
||||
39
docs/lib-standards.md
Normal file
39
docs/lib-standards.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# lib-Standards
|
||||
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
Praktische Regeln nur für `lib/**`. Ergänzt `docs/architektur.md`.
|
||||
|
||||
## 1) Repository-Regeln
|
||||
|
||||
- Verantwortlich für SQL, Filter, Paging und Mapping.
|
||||
- Keine HTTP-, Session-, Redirect- oder Policy-Logik.
|
||||
|
||||
## 2) Service-Regeln
|
||||
|
||||
- Verantwortlich für Fachlogik und Orchestrierung.
|
||||
- Keine Superglobals (`$_POST`, `$_GET`, `$_SESSION`).
|
||||
- Keine Header- oder Redirect-Ausgabe.
|
||||
- Abhängigkeiten per Constructor Injection.
|
||||
|
||||
## 3) Instanziierung
|
||||
|
||||
- Domain-Abhängigkeiten mit `new` nur in Factories/Bootstrap.
|
||||
- Für neue Domain-Logik keine statischen Utility-Klassen.
|
||||
|
||||
## 4) Autorisierung und Scope
|
||||
|
||||
- Zentral über `AuthorizationService` und Policy-Abilities.
|
||||
- Keine verteilten Einzelchecks als primäre Zugriffslogik.
|
||||
|
||||
## 5) API-Contract
|
||||
|
||||
- Extern UUID-first.
|
||||
- Interne IDs nur intern auflösen und mappen.
|
||||
|
||||
## 6) Definition of Done für `lib/**`
|
||||
|
||||
1. Schichtgrenzen eingehalten.
|
||||
2. AuthZ zentral.
|
||||
3. Unit-Tests ergänzt, bei Strukturänderung zusätzlich Contract-Test.
|
||||
4. `phpunit` und `phpstan` grün.
|
||||
@@ -25,7 +25,7 @@ docker compose restart php nginx
|
||||
|
||||
- Basis-Schema + Seeds: `db/init/init.sql`
|
||||
- Das Repo nutzt aktuell ein konsolidiertes Init-Schema (keine separaten Migrationsdateien in `db/init/`).
|
||||
- Für Bestandsumgebungen Schema-/Permission-Änderungen als idempotentes SQL-Update bereitstellen und manuell ausführen.
|
||||
- Für Bestandsumgebungen Schema-/Permission-Änderungen als idempotentes SQL-Update in `db/updates/*.sql` bereitstellen und manuell ausführen.
|
||||
|
||||
Beispiel (bestehende Umgebung, SQL-Update manuell ausführen):
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ tags:
|
||||
- name: tenants
|
||||
- name: departments
|
||||
- name: roles
|
||||
- name: permissions
|
||||
- name: settings
|
||||
paths:
|
||||
/auth/login:
|
||||
post:
|
||||
@@ -88,6 +90,133 @@ paths:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
put:
|
||||
tags: [me]
|
||||
summary: Update authenticated user profile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MeUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated profile
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MeResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'422':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
patch:
|
||||
tags: [me]
|
||||
summary: Patch authenticated user profile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MeUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated profile
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MeResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'422':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/me/password:
|
||||
post:
|
||||
tags: [me]
|
||||
summary: Change password for authenticated user
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MePasswordRequest'
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'422':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/me/avatar:
|
||||
get:
|
||||
tags: [me]
|
||||
summary: Get avatar image for authenticated user
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/AvatarSize'
|
||||
responses:
|
||||
'200':
|
||||
description: Avatar binary
|
||||
content:
|
||||
image/jpeg: {}
|
||||
image/png: {}
|
||||
image/webp: {}
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
post:
|
||||
tags: [me]
|
||||
summary: Upload avatar for authenticated user
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [avatar]
|
||||
properties:
|
||||
avatar:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
'201':
|
||||
$ref: '#/components/responses/DataObject'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'422':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
delete:
|
||||
tags: [me]
|
||||
summary: Delete avatar for authenticated user
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/me/tokens:
|
||||
get:
|
||||
@@ -350,6 +479,29 @@ paths:
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/users/avatar/{uuid}:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UuidPath'
|
||||
- $ref: '#/components/parameters/AvatarSize'
|
||||
get:
|
||||
tags: [users]
|
||||
summary: Get avatar image by user UUID
|
||||
responses:
|
||||
'200':
|
||||
description: Avatar binary
|
||||
content:
|
||||
image/jpeg: {}
|
||||
image/png: {}
|
||||
image/webp: {}
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/tenants:
|
||||
get:
|
||||
tags: [tenants]
|
||||
@@ -474,6 +626,29 @@ paths:
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/tenants/avatar/{uuid}:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UuidPath'
|
||||
- $ref: '#/components/parameters/AvatarSize'
|
||||
get:
|
||||
tags: [tenants]
|
||||
summary: Get avatar image by tenant UUID
|
||||
responses:
|
||||
'200':
|
||||
description: Avatar binary
|
||||
content:
|
||||
image/jpeg: {}
|
||||
image/png: {}
|
||||
image/webp: {}
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/departments:
|
||||
get:
|
||||
tags: [departments]
|
||||
@@ -722,6 +897,112 @@ paths:
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/permissions:
|
||||
get:
|
||||
tags: [permissions]
|
||||
summary: List permissions
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit25'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Search'
|
||||
- $ref: '#/components/parameters/OrderPermissions'
|
||||
- $ref: '#/components/parameters/Dir'
|
||||
- $ref: '#/components/parameters/ActiveFilter'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PermissionListResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/settings:
|
||||
get:
|
||||
tags: [settings]
|
||||
summary: Get application settings snapshot
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SettingsResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
put:
|
||||
tags: [settings]
|
||||
summary: Update application settings
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SettingsUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated settings snapshot
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SettingsResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'422':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
patch:
|
||||
tags: [settings]
|
||||
summary: Patch application settings
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SettingsUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated settings snapshot
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SettingsResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'422':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
/settings/public:
|
||||
get:
|
||||
tags: [settings]
|
||||
summary: Get public app settings for unauthenticated clients
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericDataResponse'
|
||||
'429':
|
||||
$ref: '#/components/responses/RateLimited'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
@@ -777,6 +1058,13 @@ components:
|
||||
schema:
|
||||
type: string
|
||||
description: "Accepted aliases: 1, 0, true, false, active, inactive"
|
||||
AvatarSize:
|
||||
name: size
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 16
|
||||
maximum: 1024
|
||||
OrderUsers:
|
||||
name: order
|
||||
in: query
|
||||
@@ -805,6 +1093,13 @@ components:
|
||||
type: string
|
||||
enum: [id, uuid, description, code, active, created, modified]
|
||||
default: description
|
||||
OrderPermissions:
|
||||
name: order
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [key, description, active, created]
|
||||
default: key
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Unauthorized
|
||||
@@ -930,6 +1225,195 @@ components:
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: UTC expiry timestamp of the token
|
||||
MeUpdateRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
first_name:
|
||||
type: string
|
||||
last_name:
|
||||
type: string
|
||||
profile_description:
|
||||
type: string
|
||||
job_title:
|
||||
type: string
|
||||
phone:
|
||||
type: string
|
||||
mobile:
|
||||
type: string
|
||||
short_dial:
|
||||
type: string
|
||||
address:
|
||||
type: string
|
||||
postal_code:
|
||||
type: string
|
||||
city:
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
region:
|
||||
type: string
|
||||
locale:
|
||||
type: string
|
||||
theme:
|
||||
type: string
|
||||
current_tenant_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
MePasswordRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [current_password, password, password2]
|
||||
properties:
|
||||
current_password:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
password2:
|
||||
type: string
|
||||
PermissionListItem:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
active:
|
||||
type: boolean
|
||||
is_system:
|
||||
type: boolean
|
||||
PermissionListResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PermissionListItem'
|
||||
total:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
SettingsObject:
|
||||
type: object
|
||||
properties:
|
||||
app_title:
|
||||
type: string
|
||||
app_locale:
|
||||
type: string
|
||||
app_theme:
|
||||
type: string
|
||||
app_theme_user_allowed:
|
||||
type: boolean
|
||||
app_registration_enabled:
|
||||
type: boolean
|
||||
app_primary_color:
|
||||
type: string
|
||||
api_token_default_ttl_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
api_token_max_ttl_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
api_cors_allowed_origins:
|
||||
type: string
|
||||
default_tenant_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
default_role_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
default_department_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
user_inactivity_deactivate_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
user_inactivity_delete_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
microsoft_shared_client_id:
|
||||
type: string
|
||||
microsoft_authority:
|
||||
type: string
|
||||
smtp_host:
|
||||
type: string
|
||||
smtp_port:
|
||||
type: integer
|
||||
nullable: true
|
||||
smtp_user:
|
||||
type: string
|
||||
smtp_secure:
|
||||
type: string
|
||||
smtp_from:
|
||||
type: string
|
||||
smtp_from_name:
|
||||
type: string
|
||||
SettingsResponse:
|
||||
type: object
|
||||
required: [data]
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SettingsObject'
|
||||
SettingsUpdateRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
app_title:
|
||||
type: string
|
||||
app_locale:
|
||||
type: string
|
||||
app_theme:
|
||||
type: string
|
||||
app_theme_user_allowed:
|
||||
type: boolean
|
||||
app_registration_enabled:
|
||||
type: boolean
|
||||
app_primary_color:
|
||||
type: string
|
||||
api_token_default_ttl_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
api_token_max_ttl_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
api_cors_allowed_origins:
|
||||
type: string
|
||||
default_tenant_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
default_role_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
default_department_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
user_inactivity_deactivate_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
user_inactivity_delete_days:
|
||||
type: integer
|
||||
nullable: true
|
||||
microsoft_shared_client_id:
|
||||
type: string
|
||||
microsoft_authority:
|
||||
type: string
|
||||
smtp_host:
|
||||
type: string
|
||||
smtp_port:
|
||||
type: integer
|
||||
nullable: true
|
||||
smtp_user:
|
||||
type: string
|
||||
smtp_password:
|
||||
type: string
|
||||
smtp_secure:
|
||||
type: string
|
||||
smtp_from:
|
||||
type: string
|
||||
smtp_from_name:
|
||||
type: string
|
||||
RoleWriteRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -946,14 +1430,14 @@ components:
|
||||
DepartmentWriteRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [description, tenant_id]
|
||||
required: [description, tenant_uuid]
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
minLength: 1
|
||||
tenant_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
tenant_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
code:
|
||||
type: string
|
||||
cost_center:
|
||||
@@ -1066,24 +1550,24 @@ components:
|
||||
type: string
|
||||
password2:
|
||||
type: string
|
||||
tenant_ids:
|
||||
tenant_uuids:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
minimum: 1
|
||||
primary_tenant_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
role_ids:
|
||||
type: string
|
||||
format: uuid
|
||||
primary_tenant_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
role_uuids:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
minimum: 1
|
||||
department_ids:
|
||||
type: string
|
||||
format: uuid
|
||||
department_uuids:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
minimum: 1
|
||||
type: string
|
||||
format: uuid
|
||||
UserUpdateRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -1133,24 +1617,24 @@ components:
|
||||
type: string
|
||||
password2:
|
||||
type: string
|
||||
tenant_ids:
|
||||
tenant_uuids:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
minimum: 1
|
||||
primary_tenant_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
role_ids:
|
||||
type: string
|
||||
format: uuid
|
||||
primary_tenant_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
role_uuids:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
minimum: 1
|
||||
department_ids:
|
||||
type: string
|
||||
format: uuid
|
||||
department_uuids:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
minimum: 1
|
||||
type: string
|
||||
format: uuid
|
||||
TenantSummary:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1436,6 +1920,8 @@ components:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
profile_description:
|
||||
type: string
|
||||
job_title:
|
||||
type: string
|
||||
phone:
|
||||
@@ -1452,12 +1938,32 @@ components:
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
region:
|
||||
type: string
|
||||
hire_date:
|
||||
type: string
|
||||
format: date
|
||||
locale:
|
||||
type: string
|
||||
theme:
|
||||
type: string
|
||||
email_verified_at:
|
||||
type: string
|
||||
nullable: true
|
||||
last_login_at:
|
||||
type: string
|
||||
nullable: true
|
||||
last_login_provider:
|
||||
type: string
|
||||
nullable: true
|
||||
active:
|
||||
type: boolean
|
||||
created:
|
||||
type: string
|
||||
modified:
|
||||
type: string
|
||||
current_tenant:
|
||||
$ref: '#/components/schemas/TenantSummary'
|
||||
primary_tenant:
|
||||
$ref: '#/components/schemas/TenantSummary'
|
||||
permissions:
|
||||
@@ -1520,12 +2026,39 @@ components:
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
region:
|
||||
type: string
|
||||
vat_id:
|
||||
type: string
|
||||
tax_number:
|
||||
type: string
|
||||
phone:
|
||||
type: string
|
||||
fax:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
support_email:
|
||||
type: string
|
||||
support_phone:
|
||||
type: string
|
||||
billing_email:
|
||||
type: string
|
||||
website:
|
||||
type: string
|
||||
privacy_url:
|
||||
type: string
|
||||
imprint_url:
|
||||
type: string
|
||||
primary_color:
|
||||
type: string
|
||||
primary_color_use_default:
|
||||
type: boolean
|
||||
default_theme:
|
||||
type: string
|
||||
allow_user_theme_mode:
|
||||
type: string
|
||||
enum: ['', '0', '1']
|
||||
created:
|
||||
type: string
|
||||
modified:
|
||||
@@ -1574,11 +2107,13 @@ components:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
cost_center:
|
||||
type: string
|
||||
active:
|
||||
type: boolean
|
||||
tenant_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
tenant_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
created:
|
||||
type: string
|
||||
modified:
|
||||
@@ -1625,6 +2160,10 @@ components:
|
||||
type: string
|
||||
active:
|
||||
type: boolean
|
||||
permission_keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
created:
|
||||
type: string
|
||||
modified:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sicherheitsmodell
|
||||
|
||||
Letzte Aktualisierung: 2026-02-22
|
||||
Letzte Aktualisierung: 2026-02-23
|
||||
|
||||
## 1) Grundprinzip
|
||||
|
||||
@@ -33,8 +33,16 @@ Kritisch:
|
||||
|
||||
- Public Routes werden in `/config/routes.php` mit `public => true` definiert.
|
||||
- Daraus wird zur Laufzeit `APP_PUBLIC_PATHS` aufgebaut.
|
||||
- Zusätzlich gibt es technische Always-Public-Präfixe in `lib/Http/AccessControl.php`
|
||||
(u. a. `api/`, `auth/microsoft/`, `branding/`, `flash/`).
|
||||
- Alles andere ist loginpflichtig.
|
||||
|
||||
API-Besonderheit:
|
||||
|
||||
- `/api/v1/auth/login` ist bewusst public (ohne Bearer-Header).
|
||||
- Laufzeitverhalten: `pages/api/v1/auth/login().php` nutzt `ApiBootstrap::init(false)`.
|
||||
- Alle anderen geschützten API-Endpunkte bleiben bei `ApiBootstrap::init()` (Auth erforderlich).
|
||||
|
||||
## 4) Permission-System
|
||||
|
||||
- Berechtigungen sind feingranular (`permissions` + `role_permissions`).
|
||||
@@ -53,6 +61,9 @@ Empfehlung:
|
||||
- Theme-Wechsel für User wird tenant-spezifisch abgesichert:
|
||||
- effektive Regel kommt aus `current_tenant.allow_user_theme` (Fallback global `app_theme_user`)
|
||||
- Endpoint `admin/users/theme` blockt bei deaktivierter Regel mit `403` (`theme_disabled`).
|
||||
- Globaler Tenant-Scope-Bypass ist explizit und separat:
|
||||
- nur Permission `tenant.scope.global`.
|
||||
- `settings.update` oder `tenants.update` alleine geben **keinen** Scope-Bypass.
|
||||
- Tenant-Microsoft-SSO ist separat abgesichert:
|
||||
- `tenants.sso_manage` für Pflege der Tenant-SSO-Konfiguration.
|
||||
- Shared-App-Credentials bleiben unter `settings.update`.
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"Choose a new password for your account.": "Wähle ein neues Passwort für dein Konto.",
|
||||
"Password updated": "Passwort aktualisiert",
|
||||
"Password can not be updated": "Passwort kann nicht aktualisiert werden",
|
||||
"Profile can not be updated": "Profil kann nicht aktualisiert werden",
|
||||
"Reset request not found": "Zurücksetz-Anfrage nicht gefunden",
|
||||
"Reset request already used": "Zurücksetz-Anfrage wurde bereits verwendet",
|
||||
"Reset request expired": "Zurücksetz-Anfrage ist abgelaufen",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"Choose a new password for your account.": "Choose a new password for your account.",
|
||||
"Password updated": "Password updated",
|
||||
"Password can not be updated": "Password can not be updated",
|
||||
"Profile can not be updated": "Profile can not be updated",
|
||||
"Reset request not found": "Reset request not found",
|
||||
"Reset request already used": "Reset request already used",
|
||||
"Reset request expired": "Reset request expired",
|
||||
|
||||
@@ -27,9 +27,9 @@ class ApiBootstrap
|
||||
* Initialize the API request context.
|
||||
*
|
||||
* Call this at the top of every API page action.
|
||||
* Sets MINTY_ALLOW_OUTPUT, CORS headers, authenticates via Bearer token.
|
||||
* Sets MINTY_ALLOW_OUTPUT, CORS headers and optional Bearer auth.
|
||||
*/
|
||||
public static function init(): void
|
||||
public static function init(bool $requireAuth = true): void
|
||||
{
|
||||
if (self::$initialized) {
|
||||
return;
|
||||
@@ -51,9 +51,11 @@ class ApiBootstrap
|
||||
|
||||
self::enforceRateLimit();
|
||||
|
||||
$authenticated = ApiAuth::authenticate();
|
||||
if (!$authenticated) {
|
||||
ApiResponse::unauthorized();
|
||||
if ($requireAuth) {
|
||||
$authenticated = ApiAuth::authenticate();
|
||||
if (!$authenticated) {
|
||||
ApiResponse::unauthorized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace MintyPHP\Service\Access;
|
||||
use MintyPHP\Repository\Access\PermissionRepository;
|
||||
use MintyPHP\Repository\Access\RolePermissionRepository;
|
||||
use MintyPHP\Repository\Access\UserRoleRepository;
|
||||
use MintyPHP\Service\Directory\DirectoryScopeGateway;
|
||||
use MintyPHP\Service\Directory\DirectoryServicesFactory;
|
||||
|
||||
class AccessServicesFactory
|
||||
{
|
||||
@@ -13,6 +15,15 @@ class AccessServicesFactory
|
||||
private ?UserRoleRepository $userRoleRepository = null;
|
||||
private ?PermissionService $permissionService = null;
|
||||
private ?PermissionGateway $permissionGateway = null;
|
||||
private ?DirectoryServicesFactory $directoryServicesFactory = null;
|
||||
private ?DirectoryScopeGateway $directoryScopeGateway = null;
|
||||
private ?UserAuthorizationPolicy $userAuthorizationPolicy = null;
|
||||
private ?RoleAuthorizationPolicy $roleAuthorizationPolicy = null;
|
||||
private ?PermissionAuthorizationPolicy $permissionAuthorizationPolicy = null;
|
||||
private ?SettingsAuthorizationPolicy $settingsAuthorizationPolicy = null;
|
||||
private ?TenantAuthorizationPolicy $tenantAuthorizationPolicy = null;
|
||||
private ?DepartmentAuthorizationPolicy $departmentAuthorizationPolicy = null;
|
||||
private ?AuthorizationService $authorizationService = null;
|
||||
|
||||
public function createPermissionRepository(): PermissionRepository
|
||||
{
|
||||
@@ -42,4 +53,71 @@ class AccessServicesFactory
|
||||
{
|
||||
return $this->permissionGateway ??= new PermissionGateway($this->createPermissionService());
|
||||
}
|
||||
|
||||
public function createUserAuthorizationPolicy(): UserAuthorizationPolicy
|
||||
{
|
||||
return $this->userAuthorizationPolicy ??= new UserAuthorizationPolicy(
|
||||
$this->createPermissionGateway(),
|
||||
$this->createDirectoryScopeGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createRoleAuthorizationPolicy(): RoleAuthorizationPolicy
|
||||
{
|
||||
return $this->roleAuthorizationPolicy ??= new RoleAuthorizationPolicy(
|
||||
$this->createPermissionGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createPermissionAuthorizationPolicy(): PermissionAuthorizationPolicy
|
||||
{
|
||||
return $this->permissionAuthorizationPolicy ??= new PermissionAuthorizationPolicy(
|
||||
$this->createPermissionGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createSettingsAuthorizationPolicy(): SettingsAuthorizationPolicy
|
||||
{
|
||||
return $this->settingsAuthorizationPolicy ??= new SettingsAuthorizationPolicy(
|
||||
$this->createPermissionGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createTenantAuthorizationPolicy(): TenantAuthorizationPolicy
|
||||
{
|
||||
return $this->tenantAuthorizationPolicy ??= new TenantAuthorizationPolicy(
|
||||
$this->createPermissionGateway(),
|
||||
$this->createDirectoryScopeGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createDepartmentAuthorizationPolicy(): DepartmentAuthorizationPolicy
|
||||
{
|
||||
return $this->departmentAuthorizationPolicy ??= new DepartmentAuthorizationPolicy(
|
||||
$this->createPermissionGateway(),
|
||||
$this->createDirectoryScopeGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createAuthorizationService(): AuthorizationService
|
||||
{
|
||||
return $this->authorizationService ??= new AuthorizationService([
|
||||
$this->createUserAuthorizationPolicy(),
|
||||
$this->createRoleAuthorizationPolicy(),
|
||||
$this->createPermissionAuthorizationPolicy(),
|
||||
$this->createSettingsAuthorizationPolicy(),
|
||||
$this->createTenantAuthorizationPolicy(),
|
||||
$this->createDepartmentAuthorizationPolicy(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createDirectoryScopeGateway(): DirectoryScopeGateway
|
||||
{
|
||||
return $this->directoryScopeGateway ??= $this->directoryServicesFactory()->createDirectoryScopeGateway();
|
||||
}
|
||||
|
||||
private function directoryServicesFactory(): DirectoryServicesFactory
|
||||
{
|
||||
return $this->directoryServicesFactory ??= new DirectoryServicesFactory();
|
||||
}
|
||||
}
|
||||
|
||||
50
lib/Service/Access/AuthorizationDecision.php
Normal file
50
lib/Service/Access/AuthorizationDecision.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
class AuthorizationDecision
|
||||
{
|
||||
public function __construct(
|
||||
private readonly bool $allowed,
|
||||
private readonly int $status = 200,
|
||||
private readonly string $error = '',
|
||||
private readonly array $attributes = []
|
||||
) {
|
||||
}
|
||||
|
||||
public static function allow(array $attributes = []): self
|
||||
{
|
||||
return new self(true, 200, '', $attributes);
|
||||
}
|
||||
|
||||
public static function deny(int $status = 403, string $error = 'forbidden', array $attributes = []): self
|
||||
{
|
||||
return new self(false, $status, $error, $attributes);
|
||||
}
|
||||
|
||||
public function isAllowed(): bool
|
||||
{
|
||||
return $this->allowed;
|
||||
}
|
||||
|
||||
public function status(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function error(): string
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function attribute(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return array_key_exists($key, $this->attributes) ? $this->attributes[$key] : $default;
|
||||
}
|
||||
}
|
||||
|
||||
11
lib/Service/Access/AuthorizationPolicyInterface.php
Normal file
11
lib/Service/Access/AuthorizationPolicyInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
interface AuthorizationPolicyInterface
|
||||
{
|
||||
public function supports(string $ability): bool;
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision;
|
||||
}
|
||||
|
||||
41
lib/Service/Access/AuthorizationService.php
Normal file
41
lib/Service/Access/AuthorizationService.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class AuthorizationService
|
||||
{
|
||||
/** @var array<int, AuthorizationPolicyInterface> */
|
||||
private array $policies = [];
|
||||
|
||||
/**
|
||||
* @param array<int, AuthorizationPolicyInterface> $policies
|
||||
*/
|
||||
public function __construct(array $policies)
|
||||
{
|
||||
foreach ($policies as $policy) {
|
||||
if ($policy instanceof AuthorizationPolicyInterface) {
|
||||
$this->policies[] = $policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
if (!$policy->supports($ability)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
return $policy->authorize($ability, $context);
|
||||
} catch (Throwable) {
|
||||
return AuthorizationDecision::deny(500, 'authorization_failed');
|
||||
}
|
||||
}
|
||||
|
||||
return AuthorizationDecision::deny(500, 'authorization_policy_missing');
|
||||
}
|
||||
}
|
||||
|
||||
160
lib/Service/Access/DepartmentAuthorizationPolicy.php
Normal file
160
lib/Service/Access/DepartmentAuthorizationPolicy.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
use MintyPHP\Service\Directory\DirectoryScopeGateway;
|
||||
|
||||
class DepartmentAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||
{
|
||||
public const ABILITY_ADMIN_DEPARTMENTS_VIEW = 'admin.departments.view';
|
||||
public const ABILITY_ADMIN_DEPARTMENTS_CREATE = 'admin.departments.create';
|
||||
public const ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT = 'admin.departments.edit.context';
|
||||
public const ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT = 'admin.departments.edit.submit';
|
||||
public const ABILITY_ADMIN_DEPARTMENTS_DELETE = 'admin.departments.delete';
|
||||
|
||||
public function __construct(
|
||||
private readonly PermissionGateway $permissionGateway,
|
||||
private readonly DirectoryScopeGateway $scopeGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(string $ability): bool
|
||||
{
|
||||
return in_array($ability, [
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_VIEW,
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_CREATE,
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT,
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT,
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_DELETE,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
return match ($ability) {
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_VIEW => $this->authorizeAdminDepartmentsView($context),
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_CREATE => $this->authorizeAdminDepartmentsCreate($context),
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT => $this->authorizeAdminDepartmentsEditContext($context),
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT => $this->authorizeAdminDepartmentsEditSubmit($context),
|
||||
self::ABILITY_ADMIN_DEPARTMENTS_DELETE => $this->authorizeAdminDepartmentsDelete($context),
|
||||
default => AuthorizationDecision::deny(500, 'authorization_ability_not_supported'),
|
||||
};
|
||||
}
|
||||
|
||||
private function authorizeAdminDepartmentsView(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$allowedTenantIds = $this->scopeGateway->getUserTenantIds($actorUserId);
|
||||
$isStrictScope = $this->scopeGateway->isStrict();
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_create_department' => $this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_CREATE),
|
||||
'allowed_tenant_ids' => $allowedTenantIds,
|
||||
'is_strict_scope' => $isStrictScope,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminDepartmentsCreate(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_CREATE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$allowedTenantIds = $this->scopeGateway->getUserTenantIds($actorUserId);
|
||||
$isStrictScope = $this->scopeGateway->isStrict();
|
||||
if ($isStrictScope && !$allowedTenantIds) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'allowed_tenant_ids' => $allowedTenantIds,
|
||||
'is_strict_scope' => $isStrictScope,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminDepartmentsEditContext(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetDepartmentId = $this->targetDepartmentId($context);
|
||||
if ($actorUserId <= 0 || $targetDepartmentId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
if (!$this->scopeGateway->canAccess('departments', $targetDepartmentId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_update_department' => $this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_UPDATE),
|
||||
'allowed_tenant_ids' => $this->scopeGateway->getUserTenantIds($actorUserId),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminDepartmentsEditSubmit(array $context): AuthorizationDecision
|
||||
{
|
||||
$contextDecision = $this->authorizeAdminDepartmentsEditContext($context);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
return $contextDecision;
|
||||
}
|
||||
|
||||
$capabilities = $this->capabilitiesFromDecision($contextDecision);
|
||||
if (!($capabilities['can_update_department'] ?? false)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow(['capabilities' => $capabilities]);
|
||||
}
|
||||
|
||||
private function authorizeAdminDepartmentsDelete(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetDepartmentId = $this->targetDepartmentId($context);
|
||||
if ($actorUserId <= 0 || $targetDepartmentId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_DELETE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
if (!$this->scopeGateway->canAccess('departments', $targetDepartmentId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function actorUserId(array $context): int
|
||||
{
|
||||
return (int) ($context['actor_user_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function targetDepartmentId(array $context): int
|
||||
{
|
||||
return (int) ($context['target_department_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function hasPermission(int $userId, string $permissionKey): bool
|
||||
{
|
||||
return $userId > 0 && $this->permissionGateway->userHas($userId, $permissionKey);
|
||||
}
|
||||
|
||||
private function capabilitiesFromDecision(AuthorizationDecision $decision): array
|
||||
{
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
return is_array($capabilities) ? $capabilities : [];
|
||||
}
|
||||
}
|
||||
129
lib/Service/Access/PermissionAuthorizationPolicy.php
Normal file
129
lib/Service/Access/PermissionAuthorizationPolicy.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
class PermissionAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||
{
|
||||
public const ABILITY_ADMIN_PERMISSIONS_VIEW = 'admin.permissions.view';
|
||||
public const ABILITY_ADMIN_PERMISSIONS_CREATE = 'admin.permissions.create';
|
||||
public const ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT = 'admin.permissions.edit.context';
|
||||
public const ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT = 'admin.permissions.edit.submit';
|
||||
public const ABILITY_ADMIN_PERMISSIONS_DELETE = 'admin.permissions.delete';
|
||||
|
||||
public function __construct(
|
||||
private readonly PermissionGateway $permissionGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(string $ability): bool
|
||||
{
|
||||
return in_array($ability, [
|
||||
self::ABILITY_ADMIN_PERMISSIONS_VIEW,
|
||||
self::ABILITY_ADMIN_PERMISSIONS_CREATE,
|
||||
self::ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT,
|
||||
self::ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT,
|
||||
self::ABILITY_ADMIN_PERMISSIONS_DELETE,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
return match ($ability) {
|
||||
self::ABILITY_ADMIN_PERMISSIONS_VIEW => $this->authorizeAdminPermissionsView($context),
|
||||
self::ABILITY_ADMIN_PERMISSIONS_CREATE => $this->authorizeAdminPermissionsCreate($context),
|
||||
self::ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT => $this->authorizeAdminPermissionsEditContext($context),
|
||||
self::ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT => $this->authorizeAdminPermissionsEditSubmit($context),
|
||||
self::ABILITY_ADMIN_PERMISSIONS_DELETE => $this->authorizeAdminPermissionsDelete($context),
|
||||
default => AuthorizationDecision::deny(500, 'authorization_ability_not_supported'),
|
||||
};
|
||||
}
|
||||
|
||||
private function authorizeAdminPermissionsView(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::PERMISSIONS_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_create_permission' => $this->hasPermission($actorUserId, PermissionService::PERMISSIONS_CREATE),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminPermissionsCreate(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::PERMISSIONS_CREATE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function authorizeAdminPermissionsEditContext(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetPermissionId = (int) ($context['target_permission_id'] ?? 0);
|
||||
if ($actorUserId <= 0 || $targetPermissionId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::PERMISSIONS_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$targetIsSystem = (bool) ($context['target_is_system'] ?? false);
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_update_permission' => $this->hasPermission($actorUserId, PermissionService::PERMISSIONS_UPDATE),
|
||||
'can_delete_permission' => $this->hasPermission($actorUserId, PermissionService::PERMISSIONS_DELETE) && !$targetIsSystem,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminPermissionsEditSubmit(array $context): AuthorizationDecision
|
||||
{
|
||||
$contextDecision = $this->authorizeAdminPermissionsEditContext($context);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
return $contextDecision;
|
||||
}
|
||||
|
||||
$capabilities = $this->capabilitiesFromDecision($contextDecision);
|
||||
if (!($capabilities['can_update_permission'] ?? false)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow(['capabilities' => $capabilities]);
|
||||
}
|
||||
|
||||
private function authorizeAdminPermissionsDelete(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::PERMISSIONS_DELETE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function actorUserId(array $context): int
|
||||
{
|
||||
return (int) ($context['actor_user_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function hasPermission(int $userId, string $permissionKey): bool
|
||||
{
|
||||
return $userId > 0 && $this->permissionGateway->userHas($userId, $permissionKey);
|
||||
}
|
||||
|
||||
private function capabilitiesFromDecision(AuthorizationDecision $decision): array
|
||||
{
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
return is_array($capabilities) ? $capabilities : [];
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ class PermissionService
|
||||
public const PERMISSIONS_DELETE = 'permissions.delete';
|
||||
public const SETTINGS_VIEW = 'settings.view';
|
||||
public const SETTINGS_UPDATE = 'settings.update';
|
||||
public const TENANT_SCOPE_GLOBAL = 'tenant.scope.global';
|
||||
public const IMPORTS_VIEW = 'imports.view';
|
||||
public const IMPORTS_AUDIT_VIEW = 'imports.audit.view';
|
||||
public const JOBS_VIEW = 'jobs.view';
|
||||
|
||||
127
lib/Service/Access/RoleAuthorizationPolicy.php
Normal file
127
lib/Service/Access/RoleAuthorizationPolicy.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
class RoleAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||
{
|
||||
public const ABILITY_ADMIN_ROLES_VIEW = 'admin.roles.view';
|
||||
public const ABILITY_ADMIN_ROLES_CREATE = 'admin.roles.create';
|
||||
public const ABILITY_ADMIN_ROLES_EDIT_CONTEXT = 'admin.roles.edit.context';
|
||||
public const ABILITY_ADMIN_ROLES_EDIT_SUBMIT = 'admin.roles.edit.submit';
|
||||
public const ABILITY_ADMIN_ROLES_DELETE = 'admin.roles.delete';
|
||||
|
||||
public function __construct(
|
||||
private readonly PermissionGateway $permissionGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(string $ability): bool
|
||||
{
|
||||
return in_array($ability, [
|
||||
self::ABILITY_ADMIN_ROLES_VIEW,
|
||||
self::ABILITY_ADMIN_ROLES_CREATE,
|
||||
self::ABILITY_ADMIN_ROLES_EDIT_CONTEXT,
|
||||
self::ABILITY_ADMIN_ROLES_EDIT_SUBMIT,
|
||||
self::ABILITY_ADMIN_ROLES_DELETE,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
return match ($ability) {
|
||||
self::ABILITY_ADMIN_ROLES_VIEW => $this->authorizeAdminRolesView($context),
|
||||
self::ABILITY_ADMIN_ROLES_CREATE => $this->authorizeAdminRolesCreate($context),
|
||||
self::ABILITY_ADMIN_ROLES_EDIT_CONTEXT => $this->authorizeAdminRolesEditContext($context),
|
||||
self::ABILITY_ADMIN_ROLES_EDIT_SUBMIT => $this->authorizeAdminRolesEditSubmit($context),
|
||||
self::ABILITY_ADMIN_ROLES_DELETE => $this->authorizeAdminRolesDelete($context),
|
||||
default => AuthorizationDecision::deny(500, 'authorization_ability_not_supported'),
|
||||
};
|
||||
}
|
||||
|
||||
private function authorizeAdminRolesView(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::ROLES_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_create_role' => $this->hasPermission($actorUserId, PermissionService::ROLES_CREATE),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminRolesCreate(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::ROLES_CREATE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function authorizeAdminRolesEditContext(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetRoleId = (int) ($context['target_role_id'] ?? 0);
|
||||
if ($actorUserId <= 0 || $targetRoleId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::ROLES_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_edit_role' => $this->hasPermission($actorUserId, PermissionService::ROLES_UPDATE),
|
||||
'can_delete_role' => $this->hasPermission($actorUserId, PermissionService::ROLES_DELETE),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminRolesEditSubmit(array $context): AuthorizationDecision
|
||||
{
|
||||
$contextDecision = $this->authorizeAdminRolesEditContext($context);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
return $contextDecision;
|
||||
}
|
||||
|
||||
$capabilities = $this->capabilitiesFromDecision($contextDecision);
|
||||
if (!($capabilities['can_edit_role'] ?? false)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow(['capabilities' => $capabilities]);
|
||||
}
|
||||
|
||||
private function authorizeAdminRolesDelete(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::ROLES_DELETE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function actorUserId(array $context): int
|
||||
{
|
||||
return (int) ($context['actor_user_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function hasPermission(int $userId, string $permissionKey): bool
|
||||
{
|
||||
return $userId > 0 && $this->permissionGateway->userHas($userId, $permissionKey);
|
||||
}
|
||||
|
||||
private function capabilitiesFromDecision(AuthorizationDecision $decision): array
|
||||
{
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
return is_array($capabilities) ? $capabilities : [];
|
||||
}
|
||||
}
|
||||
75
lib/Service/Access/SettingsAuthorizationPolicy.php
Normal file
75
lib/Service/Access/SettingsAuthorizationPolicy.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
class SettingsAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||
{
|
||||
public const ABILITY_ADMIN_SETTINGS_VIEW = 'admin.settings.view';
|
||||
public const ABILITY_ADMIN_SETTINGS_UPDATE = 'admin.settings.update';
|
||||
public const ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE = 'admin.settings.branding.update';
|
||||
public const ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE = 'admin.settings.tokens.revoke';
|
||||
public const ABILITY_ADMIN_SETTINGS_USER_LIFECYCLE_RUN = 'admin.settings.user_lifecycle.run';
|
||||
|
||||
public function __construct(
|
||||
private readonly PermissionGateway $permissionGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(string $ability): bool
|
||||
{
|
||||
return in_array($ability, [
|
||||
self::ABILITY_ADMIN_SETTINGS_VIEW,
|
||||
self::ABILITY_ADMIN_SETTINGS_UPDATE,
|
||||
self::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE,
|
||||
self::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE,
|
||||
self::ABILITY_ADMIN_SETTINGS_USER_LIFECYCLE_RUN,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
return match ($ability) {
|
||||
self::ABILITY_ADMIN_SETTINGS_VIEW => $this->authorizeAdminSettingsView($context),
|
||||
self::ABILITY_ADMIN_SETTINGS_UPDATE,
|
||||
self::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE,
|
||||
self::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE,
|
||||
self::ABILITY_ADMIN_SETTINGS_USER_LIFECYCLE_RUN => $this->authorizeSettingsUpdatePermission($context),
|
||||
default => AuthorizationDecision::deny(500, 'authorization_ability_not_supported'),
|
||||
};
|
||||
}
|
||||
|
||||
private function authorizeAdminSettingsView(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::SETTINGS_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_update_settings' => $this->hasPermission($actorUserId, PermissionService::SETTINGS_UPDATE),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeSettingsUpdatePermission(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::SETTINGS_UPDATE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function actorUserId(array $context): int
|
||||
{
|
||||
return (int) ($context['actor_user_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function hasPermission(int $userId, string $permissionKey): bool
|
||||
{
|
||||
return $userId > 0 && $this->permissionGateway->userHas($userId, $permissionKey);
|
||||
}
|
||||
}
|
||||
233
lib/Service/Access/TenantAuthorizationPolicy.php
Normal file
233
lib/Service/Access/TenantAuthorizationPolicy.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
use MintyPHP\Service\Directory\DirectoryScopeGateway;
|
||||
|
||||
class TenantAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||
{
|
||||
public const ABILITY_ADMIN_TENANTS_VIEW = 'admin.tenants.view';
|
||||
public const ABILITY_ADMIN_TENANTS_CREATE = 'admin.tenants.create';
|
||||
public const ABILITY_ADMIN_TENANTS_EDIT_CONTEXT = 'admin.tenants.edit.context';
|
||||
public const ABILITY_ADMIN_TENANTS_EDIT_SUBMIT = 'admin.tenants.edit.submit';
|
||||
public const ABILITY_ADMIN_TENANTS_DELETE = 'admin.tenants.delete';
|
||||
public const ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE = 'admin.tenants.custom_fields.manage';
|
||||
public const ABILITY_ADMIN_TENANTS_AVATAR_VIEW = 'admin.tenants.avatar.view';
|
||||
public const ABILITY_ADMIN_TENANTS_MEDIA_UPDATE = 'admin.tenants.media.update';
|
||||
|
||||
public function __construct(
|
||||
private readonly PermissionGateway $permissionGateway,
|
||||
private readonly DirectoryScopeGateway $scopeGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(string $ability): bool
|
||||
{
|
||||
return in_array($ability, [
|
||||
self::ABILITY_ADMIN_TENANTS_VIEW,
|
||||
self::ABILITY_ADMIN_TENANTS_CREATE,
|
||||
self::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT,
|
||||
self::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT,
|
||||
self::ABILITY_ADMIN_TENANTS_DELETE,
|
||||
self::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE,
|
||||
self::ABILITY_ADMIN_TENANTS_AVATAR_VIEW,
|
||||
self::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
return match ($ability) {
|
||||
self::ABILITY_ADMIN_TENANTS_VIEW => $this->authorizeAdminTenantsView($context),
|
||||
self::ABILITY_ADMIN_TENANTS_CREATE => $this->authorizeAdminTenantsCreate($context),
|
||||
self::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT => $this->authorizeAdminTenantsEditContext($context),
|
||||
self::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT => $this->authorizeAdminTenantsEditSubmit($context),
|
||||
self::ABILITY_ADMIN_TENANTS_DELETE => $this->authorizeAdminTenantsDelete($context),
|
||||
self::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE => $this->authorizeAdminTenantCustomFieldsManage($context),
|
||||
self::ABILITY_ADMIN_TENANTS_AVATAR_VIEW => $this->authorizeAdminTenantAvatarView($context),
|
||||
self::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE => $this->authorizeAdminTenantMediaUpdate($context),
|
||||
default => AuthorizationDecision::deny(500, 'authorization_ability_not_supported'),
|
||||
};
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantsEditContext(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetTenantId = $this->targetTenantId($context);
|
||||
if ($actorUserId <= 0 || $targetTenantId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
if (!$this->scopeGateway->canAccess('tenants', $targetTenantId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
$canViewTenant = $this->hasPermission($actorUserId, PermissionService::TENANTS_VIEW);
|
||||
$canUpdateTenant = $this->hasPermission($actorUserId, PermissionService::TENANTS_UPDATE);
|
||||
$canViewPage = $canViewTenant || $canUpdateTenant;
|
||||
if (!$canViewPage) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$capabilities = [
|
||||
'can_view_page' => $canViewPage,
|
||||
'can_update_tenant' => $canUpdateTenant,
|
||||
'can_manage_custom_fields' => $this->hasPermission($actorUserId, PermissionService::CUSTOM_FIELDS_MANAGE),
|
||||
'can_manage_sso' => $this->hasPermission($actorUserId, PermissionService::TENANTS_SSO_MANAGE),
|
||||
'can_delete_tenant' => $this->hasPermission($actorUserId, PermissionService::TENANTS_DELETE),
|
||||
];
|
||||
|
||||
return AuthorizationDecision::allow(['capabilities' => $capabilities]);
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantsView(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::TENANTS_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_view_page' => true,
|
||||
'can_create_tenant' => $this->hasPermission($actorUserId, PermissionService::TENANTS_CREATE),
|
||||
'can_manage_custom_fields' => $this->hasPermission($actorUserId, PermissionService::CUSTOM_FIELDS_MANAGE),
|
||||
'can_manage_sso' => $this->hasPermission($actorUserId, PermissionService::TENANTS_SSO_MANAGE),
|
||||
'allowed_tenant_ids' => $this->scopeGateway->getUserTenantIds($actorUserId),
|
||||
'is_strict_scope' => $this->scopeGateway->isStrict(),
|
||||
'has_global_access' => $this->scopeGateway->hasGlobalAccess($actorUserId),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantsCreate(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::TENANTS_CREATE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow([
|
||||
'capabilities' => [
|
||||
'can_manage_custom_fields' => $this->hasPermission($actorUserId, PermissionService::CUSTOM_FIELDS_MANAGE),
|
||||
'can_manage_sso' => $this->hasPermission($actorUserId, PermissionService::TENANTS_SSO_MANAGE),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantsEditSubmit(array $context): AuthorizationDecision
|
||||
{
|
||||
$contextDecision = $this->authorizeAdminTenantsEditContext($context);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
return $contextDecision;
|
||||
}
|
||||
|
||||
$capabilities = $this->capabilitiesFromDecision($contextDecision);
|
||||
if (!($capabilities['can_update_tenant'] ?? false)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$input = is_array($context['input'] ?? null) ? $context['input'] : [];
|
||||
if ($this->containsSsoFields($input) && !($capabilities['can_manage_sso'] ?? false)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow(['capabilities' => $capabilities]);
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantsDelete(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeTenantWithPermission($context, PermissionService::TENANTS_DELETE);
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantCustomFieldsManage(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeTenantWithPermission($context, PermissionService::CUSTOM_FIELDS_MANAGE);
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantAvatarView(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::TENANTS_VIEW)
|
||||
&& !$this->hasPermission($actorUserId, PermissionService::TENANTS_UPDATE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return $this->authorizeTenantScopeOnly($context);
|
||||
}
|
||||
|
||||
private function authorizeAdminTenantMediaUpdate(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeTenantWithPermission($context, PermissionService::TENANTS_UPDATE);
|
||||
}
|
||||
|
||||
private function authorizeTenantWithPermission(array $context, string $permissionKey): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, $permissionKey)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return $this->authorizeTenantScopeOnly($context);
|
||||
}
|
||||
|
||||
private function authorizeTenantScopeOnly(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetTenantId = $this->targetTenantId($context);
|
||||
if ($actorUserId <= 0 || $targetTenantId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
if (!$this->scopeGateway->canAccess('tenants', $targetTenantId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function containsSsoFields(array $input): bool
|
||||
{
|
||||
$ssoFields = [
|
||||
'microsoft_enabled',
|
||||
'enforce_microsoft_login',
|
||||
'sync_profile_on_login',
|
||||
'sync_profile_fields',
|
||||
'entra_tenant_id',
|
||||
'allowed_domains',
|
||||
'use_shared_app',
|
||||
'client_id_override',
|
||||
'client_secret_override',
|
||||
'clear_client_secret_override',
|
||||
];
|
||||
|
||||
foreach ($ssoFields as $field) {
|
||||
if (array_key_exists($field, $input)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function actorUserId(array $context): int
|
||||
{
|
||||
return (int) ($context['actor_user_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function targetTenantId(array $context): int
|
||||
{
|
||||
return (int) ($context['target_tenant_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function hasPermission(int $userId, string $permissionKey): bool
|
||||
{
|
||||
return $userId > 0 && $this->permissionGateway->userHas($userId, $permissionKey);
|
||||
}
|
||||
|
||||
private function capabilitiesFromDecision(AuthorizationDecision $decision): array
|
||||
{
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
return is_array($capabilities) ? $capabilities : [];
|
||||
}
|
||||
}
|
||||
455
lib/Service/Access/UserAuthorizationPolicy.php
Normal file
455
lib/Service/Access/UserAuthorizationPolicy.php
Normal file
@@ -0,0 +1,455 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Access;
|
||||
|
||||
use MintyPHP\Service\Directory\DirectoryScopeGateway;
|
||||
|
||||
class UserAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||
{
|
||||
public const ABILITY_ADMIN_USERS_VIEW = 'admin.users.view';
|
||||
public const ABILITY_ADMIN_USERS_CREATE = 'admin.users.create';
|
||||
public const ABILITY_ADMIN_USERS_EDIT_CONTEXT = 'admin.users.edit.context';
|
||||
public const ABILITY_ADMIN_USERS_EDIT_SUBMIT = 'admin.users.edit.submit';
|
||||
public const ABILITY_ADMIN_USERS_ACTIVATE = 'admin.users.activate';
|
||||
public const ABILITY_ADMIN_USERS_DEACTIVATE = 'admin.users.deactivate';
|
||||
public const ABILITY_ADMIN_USERS_DELETE = 'admin.users.delete';
|
||||
public const ABILITY_ADMIN_USERS_BULK = 'admin.users.bulk';
|
||||
public const ABILITY_ADMIN_USERS_ACCESS_PDF = 'admin.users.access_pdf';
|
||||
public const ABILITY_ADMIN_USERS_ACCESS_PDF_BULK = 'admin.users.access_pdf_bulk';
|
||||
public const ABILITY_ADMIN_USERS_AVATAR_UPLOAD = 'admin.users.avatar.upload';
|
||||
public const ABILITY_ADMIN_USERS_AVATAR_DELETE = 'admin.users.avatar.delete';
|
||||
public const ABILITY_ADMIN_USERS_AVATAR_VIEW = 'admin.users.avatar.view';
|
||||
public const ABILITY_ADMIN_USERS_API_TOKENS_MANAGE = 'admin.users.api_tokens_manage';
|
||||
public const ABILITY_ADMIN_USERS_SEND_ACCESS = 'admin.users.send_access';
|
||||
public const ABILITY_ADMIN_USERS_FORGET_TOKENS = 'admin.users.forget_tokens';
|
||||
public const ABILITY_API_USERS_INDEX_GET = 'api.users.index.get';
|
||||
public const ABILITY_API_USERS_INDEX_POST = 'api.users.index.post';
|
||||
public const ABILITY_API_USERS_SHOW_GET = 'api.users.show.get';
|
||||
public const ABILITY_API_USERS_SHOW_UPDATE = 'api.users.show.update';
|
||||
public const ABILITY_API_USERS_SHOW_DELETE = 'api.users.show.delete';
|
||||
|
||||
public function __construct(
|
||||
private readonly PermissionGateway $permissionGateway,
|
||||
private readonly DirectoryScopeGateway $scopeGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(string $ability): bool
|
||||
{
|
||||
return in_array($ability, [
|
||||
self::ABILITY_ADMIN_USERS_VIEW,
|
||||
self::ABILITY_ADMIN_USERS_CREATE,
|
||||
self::ABILITY_ADMIN_USERS_EDIT_CONTEXT,
|
||||
self::ABILITY_ADMIN_USERS_EDIT_SUBMIT,
|
||||
self::ABILITY_ADMIN_USERS_ACTIVATE,
|
||||
self::ABILITY_ADMIN_USERS_DEACTIVATE,
|
||||
self::ABILITY_ADMIN_USERS_DELETE,
|
||||
self::ABILITY_ADMIN_USERS_BULK,
|
||||
self::ABILITY_ADMIN_USERS_ACCESS_PDF,
|
||||
self::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK,
|
||||
self::ABILITY_ADMIN_USERS_AVATAR_UPLOAD,
|
||||
self::ABILITY_ADMIN_USERS_AVATAR_DELETE,
|
||||
self::ABILITY_ADMIN_USERS_AVATAR_VIEW,
|
||||
self::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE,
|
||||
self::ABILITY_ADMIN_USERS_SEND_ACCESS,
|
||||
self::ABILITY_ADMIN_USERS_FORGET_TOKENS,
|
||||
self::ABILITY_API_USERS_INDEX_GET,
|
||||
self::ABILITY_API_USERS_INDEX_POST,
|
||||
self::ABILITY_API_USERS_SHOW_GET,
|
||||
self::ABILITY_API_USERS_SHOW_UPDATE,
|
||||
self::ABILITY_API_USERS_SHOW_DELETE,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
return match ($ability) {
|
||||
self::ABILITY_ADMIN_USERS_VIEW => $this->authorizeAdminUsersView($context),
|
||||
self::ABILITY_ADMIN_USERS_CREATE => $this->authorizeAdminUsersCreate($context),
|
||||
self::ABILITY_ADMIN_USERS_EDIT_CONTEXT => $this->authorizeAdminUsersEditContext($context),
|
||||
self::ABILITY_ADMIN_USERS_EDIT_SUBMIT => $this->authorizeAdminUsersEditSubmit($context),
|
||||
self::ABILITY_ADMIN_USERS_ACTIVATE,
|
||||
self::ABILITY_ADMIN_USERS_DEACTIVATE => $this->authorizeAdminUserStatusUpdate($context),
|
||||
self::ABILITY_ADMIN_USERS_DELETE => $this->authorizeAdminUserDelete($context),
|
||||
self::ABILITY_ADMIN_USERS_BULK => $this->authorizeAdminUsersBulk($context),
|
||||
self::ABILITY_ADMIN_USERS_ACCESS_PDF => $this->authorizeAdminUserAccessPdf($context),
|
||||
self::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK => $this->authorizeAdminUsersAccessPdfBulk($context),
|
||||
self::ABILITY_ADMIN_USERS_AVATAR_UPLOAD,
|
||||
self::ABILITY_ADMIN_USERS_AVATAR_DELETE => $this->authorizeAdminUsersAvatarMutate($context),
|
||||
self::ABILITY_ADMIN_USERS_AVATAR_VIEW => $this->authorizeAdminUsersAvatarView($context),
|
||||
self::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE => $this->authorizeAdminUsersApiTokensManage($context),
|
||||
self::ABILITY_ADMIN_USERS_SEND_ACCESS,
|
||||
self::ABILITY_ADMIN_USERS_FORGET_TOKENS => $this->authorizeAdminUserSelfOrUpdate($context),
|
||||
self::ABILITY_API_USERS_INDEX_GET => $this->authorizeApiUsersIndexGet($context),
|
||||
self::ABILITY_API_USERS_INDEX_POST => $this->authorizeApiUsersIndexPost($context),
|
||||
self::ABILITY_API_USERS_SHOW_GET => $this->authorizeApiUsersShow($context, PermissionService::USERS_VIEW),
|
||||
self::ABILITY_API_USERS_SHOW_UPDATE => $this->authorizeApiUsersUpdate($context),
|
||||
self::ABILITY_API_USERS_SHOW_DELETE => $this->authorizeApiUsersShow($context, PermissionService::USERS_DELETE),
|
||||
default => AuthorizationDecision::deny(500, 'authorization_ability_not_supported'),
|
||||
};
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersView(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeAdminUserWithPermission(PermissionService::USERS_VIEW, $context, false);
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersCreate(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeAdminUserWithPermission(PermissionService::USERS_CREATE, $context, false);
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersEditContext(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->buildEditContextDecision($context);
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersEditSubmit(array $context): AuthorizationDecision
|
||||
{
|
||||
$contextDecision = $this->buildEditContextDecision($context);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
return $contextDecision;
|
||||
}
|
||||
|
||||
$capabilities = $this->capabilitiesFromDecision($contextDecision);
|
||||
if (!($capabilities['can_edit_user'] ?? false)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow(['capabilities' => $capabilities]);
|
||||
}
|
||||
|
||||
private function authorizeAdminUserStatusUpdate(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeAdminUserWithPermission(PermissionService::USERS_UPDATE, $context);
|
||||
}
|
||||
|
||||
private function authorizeAdminUserDelete(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeAdminUserWithPermission(PermissionService::USERS_DELETE, $context);
|
||||
}
|
||||
|
||||
private function authorizeAdminUserAccessPdf(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeAdminUserWithPermission(PermissionService::USERS_ACCESS_PDF, $context);
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersAccessPdfBulk(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeAdminUserWithPermission(PermissionService::USERS_ACCESS_PDF, $context, false);
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersApiTokensManage(array $context): AuthorizationDecision
|
||||
{
|
||||
return $this->authorizeAdminUserWithPermission(PermissionService::API_TOKENS_MANAGE, $context);
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersAvatarMutate(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetUserId = (int) ($context['target_user_id'] ?? 0);
|
||||
if ($actorUserId <= 0 || $targetUserId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$isOwnAccount = $actorUserId === $targetUserId;
|
||||
if (!$isOwnAccount && !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
$canEditUser = $this->canEditUserForTarget($actorUserId, $targetUserId);
|
||||
if (!$canEditUser) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersAvatarView(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetUserId = (int) ($context['target_user_id'] ?? 0);
|
||||
if ($actorUserId <= 0 || $targetUserId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
if ($actorUserId === $targetUserId) {
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
if (!$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
$canEditUser = $this->canEditUserForTarget($actorUserId, $targetUserId);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::USERS_VIEW)
|
||||
&& !$this->hasPermission($actorUserId, PermissionService::ADDRESS_BOOK_VIEW)
|
||||
&& !$canEditUser) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function authorizeAdminUserSelfOrUpdate(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetUserId = (int) ($context['target_user_id'] ?? 0);
|
||||
if ($targetUserId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
if ($this->canEditUserForTarget($actorUserId, $targetUserId)) {
|
||||
if ($targetUserId !== $actorUserId
|
||||
&& !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
private function authorizeAdminUsersBulk(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$action = strtolower(trim((string) ($context['bulk_action'] ?? '')));
|
||||
|
||||
$permission = match ($action) {
|
||||
'delete' => PermissionService::USERS_DELETE,
|
||||
'activate', 'deactivate', 'send-access' => PermissionService::USERS_UPDATE,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($permission === null) {
|
||||
return AuthorizationDecision::deny(400, 'invalid_action');
|
||||
}
|
||||
if (!$this->hasPermission($actorUserId, $permission)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function authorizeApiUsersIndexGet(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::USERS_VIEW)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function authorizeApiUsersIndexPost(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, PermissionService::USERS_CREATE)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$input = is_array($context['input'] ?? null) ? $context['input'] : [];
|
||||
$scopedTenantId = (int) ($context['scoped_tenant_id'] ?? 0);
|
||||
if ($scopedTenantId <= 0) {
|
||||
return AuthorizationDecision::allow(['input' => $input]);
|
||||
}
|
||||
|
||||
if ($this->containsOutOfScopeTenantId($input['tenant_ids'] ?? [], $scopedTenantId)) {
|
||||
return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
|
||||
}
|
||||
|
||||
$primaryTenantId = (int) ($input['primary_tenant_id'] ?? 0);
|
||||
if ($primaryTenantId > 0 && $primaryTenantId !== $scopedTenantId) {
|
||||
return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
|
||||
}
|
||||
|
||||
$input['tenant_ids'] = [$scopedTenantId];
|
||||
$input['primary_tenant_id'] = $scopedTenantId;
|
||||
|
||||
return AuthorizationDecision::allow(['input' => $input]);
|
||||
}
|
||||
|
||||
private function authorizeApiUsersUpdate(array $context): AuthorizationDecision
|
||||
{
|
||||
$decision = $this->authorizeApiUsersShow($context, PermissionService::USERS_UPDATE);
|
||||
if (!$decision->isAllowed()) {
|
||||
return $decision;
|
||||
}
|
||||
|
||||
$input = is_array($context['input'] ?? null) ? $context['input'] : [];
|
||||
$scopedTenantId = (int) ($context['scoped_tenant_id'] ?? 0);
|
||||
if ($scopedTenantId <= 0) {
|
||||
return AuthorizationDecision::allow(['input' => $input]);
|
||||
}
|
||||
|
||||
if (array_key_exists('tenant_ids', $input)
|
||||
&& $this->containsOutOfScopeTenantId($input['tenant_ids'], $scopedTenantId)) {
|
||||
return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
|
||||
}
|
||||
|
||||
if (array_key_exists('primary_tenant_id', $input)) {
|
||||
$primaryTenantId = (int) ($input['primary_tenant_id'] ?? 0);
|
||||
if ($primaryTenantId > 0 && $primaryTenantId !== $scopedTenantId) {
|
||||
return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow(['input' => $input]);
|
||||
}
|
||||
|
||||
private function authorizeApiUsersShow(array $context, string $permissionKey): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, $permissionKey)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$targetUserId = (int) ($context['target_user_id'] ?? 0);
|
||||
if ($targetUserId <= 0) {
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
if (!$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(404, 'not_found');
|
||||
}
|
||||
|
||||
$scopedTenantId = (int) ($context['scoped_tenant_id'] ?? 0);
|
||||
if ($scopedTenantId > 0 && !$this->scopeGateway->resourceBelongsToTenant('users', $targetUserId, $scopedTenantId)) {
|
||||
return AuthorizationDecision::deny(404, 'not_found');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
private function containsOutOfScopeTenantId(mixed $tenantIdsRaw, int $scopedTenantId): bool
|
||||
{
|
||||
$tenantIds = $this->normalizeTenantIds($tenantIdsRaw);
|
||||
if (!$tenantIds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_diff($tenantIds, [$scopedTenantId]) !== [];
|
||||
}
|
||||
|
||||
private function normalizeTenantIds(mixed $tenantIdsRaw): array
|
||||
{
|
||||
if (!is_array($tenantIdsRaw)) {
|
||||
$tenantIdsRaw = [$tenantIdsRaw];
|
||||
}
|
||||
|
||||
$tenantIds = [];
|
||||
foreach ($tenantIdsRaw as $value) {
|
||||
$tenantId = (int) $value;
|
||||
if ($tenantId > 0) {
|
||||
$tenantIds[$tenantId] = $tenantId;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($tenantIds);
|
||||
}
|
||||
|
||||
private function actorUserId(array $context): int
|
||||
{
|
||||
return (int) ($context['actor_user_id'] ?? 0);
|
||||
}
|
||||
|
||||
private function hasPermission(int $userId, string $permissionKey): bool
|
||||
{
|
||||
return $userId > 0 && $this->permissionGateway->userHas($userId, $permissionKey);
|
||||
}
|
||||
|
||||
private function canEditUserForTarget(int $actorUserId, int $targetUserId): bool
|
||||
{
|
||||
if ($actorUserId <= 0 || $targetUserId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$canUpdateUser = $this->hasPermission($actorUserId, PermissionService::USERS_UPDATE);
|
||||
if ($targetUserId === $actorUserId) {
|
||||
return $canUpdateUser || $this->hasPermission($actorUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
}
|
||||
|
||||
return $canUpdateUser;
|
||||
}
|
||||
|
||||
private function buildEditContextDecision(array $context): AuthorizationDecision
|
||||
{
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
$targetUserId = (int) ($context['target_user_id'] ?? 0);
|
||||
if ($actorUserId <= 0 || $targetUserId <= 0) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$isOwnAccount = $targetUserId === $actorUserId;
|
||||
if (!$isOwnAccount && !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
$canViewUsers = $this->hasPermission($actorUserId, PermissionService::USERS_VIEW);
|
||||
$canEditUser = $this->canEditUserForTarget($actorUserId, $targetUserId);
|
||||
$canViewPage = $canViewUsers || $canEditUser;
|
||||
if (!$canViewPage) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
$canManageTenants = $this->hasPermission($actorUserId, PermissionService::TENANTS_UPDATE);
|
||||
$allowedTenantIds = $canManageTenants ? null : $this->scopeGateway->getUserTenantIds($actorUserId);
|
||||
$canViewUserAudit = $this->hasPermission($actorUserId, PermissionService::USERS_VIEW_AUDIT);
|
||||
|
||||
$capabilities = [
|
||||
'can_view_page' => $canViewPage,
|
||||
'can_view_users' => $canViewUsers,
|
||||
'can_edit_user' => $canEditUser,
|
||||
'can_edit_assignments' => $canEditUser && $this->hasPermission($actorUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS),
|
||||
'can_manage_tenants' => $canManageTenants,
|
||||
'can_edit_custom_field_values' => $canEditUser && (
|
||||
$this->hasPermission($actorUserId, PermissionService::CUSTOM_FIELDS_EDIT_VALUES)
|
||||
|| ($isOwnAccount && $this->hasPermission($actorUserId, PermissionService::USERS_SELF_UPDATE))
|
||||
),
|
||||
'can_manage_api_tokens' => $canEditUser && $this->hasPermission($actorUserId, PermissionService::API_TOKENS_MANAGE),
|
||||
'can_view_address_book' => $this->hasPermission($actorUserId, PermissionService::ADDRESS_BOOK_VIEW),
|
||||
'can_view_user_meta' => $this->hasPermission($actorUserId, PermissionService::USERS_VIEW_META),
|
||||
'can_view_user_audit' => $canViewUserAudit,
|
||||
'can_access_pdf' => $this->hasPermission($actorUserId, PermissionService::USERS_ACCESS_PDF),
|
||||
'can_delete_user' => !$isOwnAccount && $this->hasPermission($actorUserId, PermissionService::USERS_DELETE),
|
||||
'can_view_security_artifacts' => $isOwnAccount || $canViewUserAudit,
|
||||
'can_view_permissions_table' => $this->hasPermission($actorUserId, PermissionService::PERMISSIONS_VIEW),
|
||||
'is_own_account' => $isOwnAccount,
|
||||
'allowed_tenant_ids' => is_array($allowedTenantIds)
|
||||
? array_values(array_unique(array_map('intval', $allowedTenantIds)))
|
||||
: null,
|
||||
];
|
||||
|
||||
return AuthorizationDecision::allow(['capabilities' => $capabilities]);
|
||||
}
|
||||
|
||||
private function capabilitiesFromDecision(AuthorizationDecision $decision): array
|
||||
{
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
return is_array($capabilities) ? $capabilities : [];
|
||||
}
|
||||
|
||||
private function authorizeAdminUserWithPermission(
|
||||
string $permissionKey,
|
||||
array $context,
|
||||
bool $checkTenantScope = true
|
||||
): AuthorizationDecision {
|
||||
$actorUserId = $this->actorUserId($context);
|
||||
if (!$this->hasPermission($actorUserId, $permissionKey)) {
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
}
|
||||
|
||||
if (!$checkTenantScope) {
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
$targetUserId = (int) ($context['target_user_id'] ?? 0);
|
||||
if ($targetUserId > 0
|
||||
&& $targetUserId !== $actorUserId
|
||||
&& !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
|
||||
return AuthorizationDecision::deny(403, 'permission_denied');
|
||||
}
|
||||
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,6 @@ class TenantScopeService
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->permissionGateway->userHas($userId, PermissionService::TENANTS_UPDATE)
|
||||
|| $this->permissionGateway->userHas($userId, PermissionService::SETTINGS_UPDATE);
|
||||
return $this->permissionGateway->userHas($userId, PermissionService::TENANT_SCOPE_GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,6 +443,104 @@ class UserAccountService
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-service profile update (subset of fields, no admin-only changes).
|
||||
*
|
||||
* @param int $userId
|
||||
* @param array $input
|
||||
* @return array{ok: bool, errors?: list<string>, form?: array<string, mixed>}
|
||||
*/
|
||||
public function updateSelfProfile(int $userId, array $input): array
|
||||
{
|
||||
$existing = $this->userReadRepository->find($userId);
|
||||
if (!$existing) {
|
||||
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
||||
}
|
||||
|
||||
$allowedFields = [
|
||||
'first_name', 'last_name', 'profile_description', 'job_title',
|
||||
'phone', 'mobile', 'short_dial',
|
||||
'address', 'postal_code', 'city', 'country', 'region',
|
||||
];
|
||||
|
||||
$form = [];
|
||||
foreach ($allowedFields as $field) {
|
||||
$form[$field] = array_key_exists($field, $input)
|
||||
? trim((string) $input[$field])
|
||||
: ($existing[$field] ?? '');
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
if ($form['first_name'] === '') {
|
||||
$errors[] = t('First name cannot be empty');
|
||||
}
|
||||
if ($form['last_name'] === '') {
|
||||
$errors[] = t('Last name cannot be empty');
|
||||
}
|
||||
|
||||
$locale = array_key_exists('locale', $input)
|
||||
? $this->normalizeLocale($input['locale'])
|
||||
: ($existing['locale'] ?? '');
|
||||
$theme = array_key_exists('theme', $input)
|
||||
? $this->normalizeTheme($input['theme'])
|
||||
: ($existing['theme'] ?? 'light');
|
||||
|
||||
// Tenant switch
|
||||
$currentTenantId = null;
|
||||
if (array_key_exists('current_tenant_uuid', $input)) {
|
||||
$tenantUuid = trim((string) ($input['current_tenant_uuid'] ?? ''));
|
||||
if ($tenantUuid !== '') {
|
||||
$tenantService = directoryServicesFactory()->createTenantService();
|
||||
$tenant = $tenantService->findByUuid($tenantUuid);
|
||||
if (!$tenant || !isset($tenant['id'])) {
|
||||
$errors[] = t('Tenant not found');
|
||||
} else {
|
||||
$userTenantIds = $this->userAssignmentService->buildAssignmentsForUser($userId)['tenants'] ?? [];
|
||||
$userTenantIds = array_map(static fn(array $t): int => (int) ($t['id'] ?? 0), $userTenantIds);
|
||||
$tenantId = (int) $tenant['id'];
|
||||
if (!in_array($tenantId, $userTenantIds, true)) {
|
||||
$errors[] = t('No access to this tenant');
|
||||
} else {
|
||||
$currentTenantId = $tenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
return ['ok' => false, 'errors' => $errors, 'form' => $form];
|
||||
}
|
||||
|
||||
$updateData = [
|
||||
'first_name' => $form['first_name'],
|
||||
'last_name' => $form['last_name'],
|
||||
'profile_description' => $form['profile_description'] !== '' ? $form['profile_description'] : null,
|
||||
'job_title' => $form['job_title'] !== '' ? $form['job_title'] : null,
|
||||
'phone' => $form['phone'] !== '' ? $form['phone'] : null,
|
||||
'mobile' => $form['mobile'] !== '' ? $form['mobile'] : null,
|
||||
'short_dial' => $form['short_dial'] !== '' ? $form['short_dial'] : null,
|
||||
'address' => $form['address'] !== '' ? $form['address'] : null,
|
||||
'postal_code' => $form['postal_code'] !== '' ? $form['postal_code'] : null,
|
||||
'city' => $form['city'] !== '' ? $form['city'] : null,
|
||||
'country' => $form['country'] !== '' ? $form['country'] : null,
|
||||
'region' => $form['region'] !== '' ? $form['region'] : null,
|
||||
'locale' => $locale,
|
||||
'theme' => $theme,
|
||||
'modified_by' => $userId,
|
||||
];
|
||||
|
||||
$updated = $this->userWriteRepository->update($userId, $updateData);
|
||||
if (!$updated) {
|
||||
return ['ok' => false, 'errors' => [t('Profile can not be updated')]];
|
||||
}
|
||||
|
||||
if ($currentTenantId !== null) {
|
||||
$this->userWriteRepository->setCurrentTenant($userId, $currentTenantId);
|
||||
}
|
||||
|
||||
return ['ok' => true, 'form' => $form];
|
||||
}
|
||||
|
||||
private function filterUuidsByTenantScope(array $uuids, int $currentUserId): array
|
||||
{
|
||||
$allowed = [];
|
||||
|
||||
@@ -2,19 +2,24 @@
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_CREATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$allowedTenantIds = directoryServicesFactory()->createDirectoryScopeGateway()->getUserTenantIds($currentUserId);
|
||||
if (directoryServicesFactory()->createDirectoryScopeGateway()->isStrict() && !$allowedTenantIds) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$allowedTenantIds = is_array($capabilities) ? array_values(array_unique(array_map('intval', (array) ($capabilities['allowed_tenant_ids'] ?? [])))) : [];
|
||||
$isStrictScope = is_array($capabilities) ? (bool) ($capabilities['is_strict_scope'] ?? false) : false;
|
||||
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
@@ -31,7 +36,7 @@ if ($allowedTenantIds) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (directoryServicesFactory()->createDirectoryScopeGateway()->isStrict()) {
|
||||
} elseif ($isStrictScope) {
|
||||
$tenants = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$tenantIds = directoryServicesFactory()->createDirectoryScopeGateway()->getUserTenantIds($currentUserId);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$tenantIds = is_array($capabilities) ? array_values(array_unique(array_map('intval', (array) ($capabilities['allowed_tenant_ids'] ?? [])))) : [];
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_DELETE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if ($uuid === '') {
|
||||
Router::redirect('admin/departments');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$department = $uuid !== '' ? directoryServicesFactory()->createDepartmentService()->findByUuid($uuid) : null;
|
||||
if ($department && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('departments', (int) ($department['id'] ?? 0), $currentUserId)) {
|
||||
$department = directoryServicesFactory()->createDepartmentService()->findByUuid($uuid);
|
||||
if (!$department) {
|
||||
Flash::error('Department not found', 'admin/departments', 'department_not_found');
|
||||
Router::redirect('admin/departments');
|
||||
return;
|
||||
}
|
||||
|
||||
$departmentId = (int) ($department['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_DELETE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_department_id' => $departmentId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
@@ -25,6 +38,7 @@ $result = directoryServicesFactory()->createDepartmentService()->deleteByUuid($u
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Flash::error('Department not found', 'admin/departments', 'department_not_found');
|
||||
Router::redirect('admin/departments');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($currentUserId > 0) {
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId);
|
||||
}
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
$allowedTenantIds = directoryServicesFactory()->createDirectoryScopeGateway()->getUserTenantIds($currentUserId);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$department = $uuid !== '' ? directoryServicesFactory()->createDepartmentService()->findByUuid($uuid) : null;
|
||||
@@ -25,10 +25,30 @@ if (!$department) {
|
||||
}
|
||||
|
||||
$departmentId = (int) ($department['id'] ?? 0);
|
||||
if (!directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('departments', $departmentId, $currentUserId)) {
|
||||
$contextDecision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_department_id' => $departmentId,
|
||||
]);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$capabilities = $contextDecision->attribute('capabilities', []);
|
||||
if (!is_array($capabilities)) {
|
||||
$capabilities = [];
|
||||
}
|
||||
$canViewPage = (bool) ($capabilities['can_view_page'] ?? false);
|
||||
$canUpdateDepartment = (bool) ($capabilities['can_update_department'] ?? false);
|
||||
$allowedTenantIdsRaw = $capabilities['allowed_tenant_ids'] ?? [];
|
||||
$allowedTenantIds = is_array($allowedTenantIdsRaw)
|
||||
? array_values(array_unique(array_filter(array_map('intval', $allowedTenantIdsRaw), static fn (int $tenantId): bool => $tenantId > 0)))
|
||||
: [];
|
||||
if (!$canViewPage) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$creatorId = (int) ($department['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
@@ -66,13 +86,31 @@ if ($allowedTenantIds) {
|
||||
}
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
if (!permissionGateway()->userHas($currentUserId, PermissionService::DEPARTMENTS_UPDATE)) {
|
||||
$submitDecision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_department_id' => $departmentId,
|
||||
'input' => $_POST,
|
||||
]);
|
||||
if (!$submitDecision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$submitCapabilities = $submitDecision->attribute('capabilities', []);
|
||||
if (is_array($submitCapabilities)) {
|
||||
$canUpdateDepartment = (bool) ($submitCapabilities['can_update_department'] ?? $canUpdateDepartment);
|
||||
}
|
||||
if (!$canUpdateDepartment) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$selectedTenantId = (int) ($_POST['tenant_id'] ?? 0);
|
||||
$selectedTenantIds = directoryServicesFactory()->createDirectoryScopeGateway()->filterTenantIdsForUser([$selectedTenantId], $currentUserId);
|
||||
$selectedTenantId = (int) ($selectedTenantIds[0] ?? 0);
|
||||
if ($allowedTenantIds) {
|
||||
$selectedTenantId = in_array($selectedTenantId, $allowedTenantIds, true) ? $selectedTenantId : 0;
|
||||
} elseif (directoryServicesFactory()->createDirectoryScopeGateway()->isStrict()) {
|
||||
$selectedTenantId = 0;
|
||||
}
|
||||
$input = $_POST;
|
||||
$input['tenant_id'] = $selectedTenantId;
|
||||
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$allowedTenantIds = is_array($capabilities) ? array_values(array_unique(array_map('intval', (array) ($capabilities['allowed_tenant_ids'] ?? [])))) : [];
|
||||
$isStrictScope = is_array($capabilities) ? (bool) ($capabilities['is_strict_scope'] ?? false) : false;
|
||||
$canCreateDepartments = is_array($capabilities) ? (bool) ($capabilities['can_create_department'] ?? false) : false;
|
||||
|
||||
$tenants = directoryServicesFactory()->createTenantService()->list();
|
||||
$allowedTenantIds = directoryServicesFactory()->createDirectoryScopeGateway()->getUserTenantIds($currentUserId);
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (directoryServicesFactory()->createDirectoryScopeGateway()->isStrict()) {
|
||||
} elseif ($isStrictScope) {
|
||||
$tenants = [];
|
||||
}
|
||||
usort($tenants, static function ($a, $b) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreateDepartments = can('departments.create');
|
||||
$canCreateDepartments = (bool) ($canCreateDepartments ?? false);
|
||||
$activeTenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
$activeStatus = trim((string) ($_GET['active'] ?? ''));
|
||||
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
|
||||
|
||||
@@ -4,10 +4,19 @@ use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_CREATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_CREATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
|
||||
if (!isset($_SESSION['user'])) {
|
||||
http_response_code(401);
|
||||
Router::json(['data' => [], 'total' => 0]);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
|
||||
@@ -3,14 +3,24 @@
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_DELETE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_DELETE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
$id = (int) ($id ?? 0);
|
||||
if ($id <= 0) {
|
||||
Router::redirect('admin/permissions');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = permissionGateway()->deleteById($id);
|
||||
|
||||
@@ -4,17 +4,15 @@ use MintyPHP\Buffer;
|
||||
use MintyPHP\Repository\Access\RoleRepository;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId);
|
||||
}
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$authorizationService = $accessServicesFactory->createAuthorizationService();
|
||||
|
||||
$id = (int) ($id ?? 0);
|
||||
$permission = $id > 0 ? permissionGateway()->find($id) : null;
|
||||
@@ -22,18 +20,33 @@ if (!$permission) {
|
||||
Flash::error('Permission not found', 'admin/permissions', 'permission_not_found');
|
||||
Router::redirect('admin/permissions');
|
||||
}
|
||||
$isSystemPermission = (int) ($permission['is_system'] ?? 0) === 1;
|
||||
$contextDecision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_permission_id' => $id,
|
||||
'target_is_system' => $isSystemPermission,
|
||||
]);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $contextDecision->attribute('capabilities', []);
|
||||
$canUpdatePermission = is_array($capabilities) ? (bool) ($capabilities['can_update_permission'] ?? false) : false;
|
||||
$canDeletePermission = is_array($capabilities) ? (bool) ($capabilities['can_delete_permission'] ?? false) : false;
|
||||
|
||||
$errors = [];
|
||||
$form = $permission;
|
||||
$roles = (new RoleRepository())->listActive();
|
||||
$rolePermissionRepository = (new AccessServicesFactory())->createRolePermissionRepository();
|
||||
$rolePermissionRepository = $accessServicesFactory->createRolePermissionRepository();
|
||||
$selectedRoleIds = $rolePermissionRepository->listRoleIdsByPermissionId($id);
|
||||
|
||||
if (isset($_POST['key'])) {
|
||||
if (!permissionGateway()->userHas($currentUserId, PermissionService::PERMISSIONS_UPDATE)) {
|
||||
Flash::error('Permission denied', "admin/permissions/edit/{$id}", 'permission_denied');
|
||||
Router::redirect("admin/permissions/edit/{$id}");
|
||||
return;
|
||||
$submitDecision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_permission_id' => $id,
|
||||
'target_is_system' => $isSystemPermission,
|
||||
]);
|
||||
if (!$submitDecision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$result = permissionGateway()->updateFromAdmin($id, $_POST);
|
||||
$form = $result['form'] ?? $form;
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $permission
|
||||
* @var bool $canUpdatePermission
|
||||
* @var bool $canDeletePermission
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$canUpdatePermission = (bool) ($canUpdatePermission ?? false);
|
||||
$canDeletePermission = (bool) ($canDeletePermission ?? false);
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
@@ -22,7 +27,7 @@ use MintyPHP\Session;
|
||||
'title' => t('Edit permission'),
|
||||
'backHref' => 'admin/permissions',
|
||||
'backTitle' => t('Cancel'),
|
||||
'actions' => can('permissions.update') ? [
|
||||
'actions' => $canUpdatePermission ? [
|
||||
[
|
||||
'form' => 'permission-form',
|
||||
'name' => 'action',
|
||||
@@ -52,10 +57,10 @@ use MintyPHP\Session;
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? $permission ?? [];
|
||||
$isReadOnly = !can('permissions.update');
|
||||
$isReadOnly = !$canUpdatePermission;
|
||||
$detailsOpenAll = true;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
$showDangerZone = can('permissions.delete') && empty($permission['is_system']);
|
||||
$showDangerZone = $canDeletePermission;
|
||||
$dangerZoneDeleteFormId = $showDangerZone ? 'permission-delete-form' : '';
|
||||
$dangerZoneWarning = t('This will permanently delete this permission.');
|
||||
$dangerZoneActionLabel = t('Delete permission');
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId);
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$authorizationService = $accessServicesFactory->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$canCreatePermissions = is_array($capabilities) ? (bool) ($capabilities['can_create_permission'] ?? false) : false;
|
||||
|
||||
Buffer::set('title', t('Permissions'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreatePermissions = can('permissions.create');
|
||||
$canUpdatePermissions = can('permissions.update');
|
||||
$canCreatePermissions = (bool) ($canCreatePermissions ?? false);
|
||||
?>
|
||||
<?php
|
||||
$breadcrumbs = [
|
||||
|
||||
@@ -3,12 +3,21 @@
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_CREATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$authorizationService = $accessServicesFactory->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_CREATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
@@ -16,14 +25,12 @@ $form = [
|
||||
'code' => '',
|
||||
'active' => 1,
|
||||
];
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$permissionRepository = $accessServicesFactory->createPermissionRepository();
|
||||
$rolePermissionRepository = $accessServicesFactory->createRolePermissionRepository();
|
||||
$permissions = $permissionRepository->listActive();
|
||||
$selectedPermissionIds = [];
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$result = directoryServicesFactory()->createRoleService()->createFromAdmin($_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$authorizationService = $accessServicesFactory->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
@@ -29,7 +38,7 @@ $result = directoryServicesFactory()->createRoleService()->listPaged([
|
||||
$settingsFactory = new SettingServicesFactory();
|
||||
$settingGateway = $settingsFactory->createSettingGateway();
|
||||
$defaultRoleId = $settingGateway->getDefaultRoleId();
|
||||
$rolePermissionRepository = (new AccessServicesFactory())->createRolePermissionRepository();
|
||||
$rolePermissionRepository = $accessServicesFactory->createRolePermissionRepository();
|
||||
$roleIds = [];
|
||||
foreach ($result['rows'] as $roleRow) {
|
||||
$roleId = (int) ($roleRow['id'] ?? 0);
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_DELETE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_DELETE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if ($uuid === '') {
|
||||
Router::redirect('admin/roles');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = directoryServicesFactory()->createRoleService()->deleteByUuid($uuid);
|
||||
|
||||
@@ -3,19 +3,16 @@
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId);
|
||||
}
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$authorizationService = $accessServicesFactory->createAuthorizationService();
|
||||
$permissionRepository = $accessServicesFactory->createPermissionRepository();
|
||||
$rolePermissionRepository = $accessServicesFactory->createRolePermissionRepository();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
@@ -28,6 +25,17 @@ if (!$role) {
|
||||
}
|
||||
|
||||
$roleId = (int) ($role['id'] ?? 0);
|
||||
$contextDecision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_CONTEXT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_role_id' => $roleId,
|
||||
]);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $contextDecision->attribute('capabilities', []);
|
||||
$canUpdateRole = is_array($capabilities) ? (bool) ($capabilities['can_edit_role'] ?? false) : false;
|
||||
$canDeleteRole = is_array($capabilities) ? (bool) ($capabilities['can_delete_role'] ?? false) : false;
|
||||
|
||||
$creatorId = (int) ($role['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
@@ -53,10 +61,12 @@ $permissions = $permissionRepository->listActive();
|
||||
$selectedPermissionIds = $rolePermissionRepository->listPermissionIdsByRoleId($roleId);
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
if (!permissionGateway()->userHas($currentUserId, PermissionService::ROLES_UPDATE)) {
|
||||
Flash::error('Permission denied', "admin/roles/edit/{$uuid}", 'permission_denied');
|
||||
Router::redirect("admin/roles/edit/{$uuid}");
|
||||
return;
|
||||
$submitDecision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_SUBMIT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_role_id' => $roleId,
|
||||
]);
|
||||
if (!$submitDecision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$result = directoryServicesFactory()->createRoleService()->updateFromAdmin($roleId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $role
|
||||
* @var bool $canUpdateRole
|
||||
* @var bool $canDeleteRole
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $form ?? $role ?? [];
|
||||
$isReadOnly = !can('roles.update');
|
||||
$canUpdateRole = (bool) ($canUpdateRole ?? false);
|
||||
$canDeleteRole = (bool) ($canDeleteRole ?? false);
|
||||
$isReadOnly = !$canUpdateRole;
|
||||
|
||||
?>
|
||||
|
||||
@@ -26,7 +30,7 @@ $isReadOnly = !can('roles.update');
|
||||
'title' => t('Edit role'),
|
||||
'backHref' => 'admin/roles',
|
||||
'backTitle' => t('Cancel'),
|
||||
'actions' => can('roles.update') ? [
|
||||
'actions' => $canUpdateRole ? [
|
||||
[
|
||||
'form' => 'role-form',
|
||||
'name' => 'action',
|
||||
@@ -60,7 +64,7 @@ $isReadOnly = !can('roles.update');
|
||||
<?php
|
||||
$detailsOpenAll = true;
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
$showDangerZone = can('roles.delete');
|
||||
$showDangerZone = $canDeleteRole;
|
||||
$dangerZoneDeleteFormId = $showDangerZone ? 'role-delete-form' : '';
|
||||
$dangerZoneWarning = t('This will permanently delete this role.');
|
||||
$dangerZoneActionLabel = t('Delete role');
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::ROLES_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId);
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$authorizationService = $accessServicesFactory->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$canCreateRoles = is_array($capabilities) ? (bool) ($capabilities['can_create_role'] ?? false) : false;
|
||||
|
||||
Buffer::set('title', t('Roles'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreateRoles = can('roles.create');
|
||||
$canCreateRoles = (bool) ($canCreateRoles ?? false);
|
||||
?>
|
||||
<?php
|
||||
$breadcrumbs = [
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Branding\BrandingServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Branding\BrandingServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
|
||||
@@ -4,14 +4,25 @@ use MintyPHP\Buffer;
|
||||
use MintyPHP\Repository\Auth\ApiTokenRepository;
|
||||
use MintyPHP\Repository\Auth\RememberTokenRepository;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Settings\SettingService;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$viewDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$viewDecision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$viewCapabilities = $viewDecision->attribute('capabilities', []);
|
||||
$canUpdateSettings = is_array($viewCapabilities) ? (bool) ($viewCapabilities['can_update_settings'] ?? false) : false;
|
||||
|
||||
$rememberTokenRepository = new RememberTokenRepository();
|
||||
$apiTokenRepository = new ApiTokenRepository();
|
||||
@@ -88,7 +99,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!isset($_POST['settings_submit'])) {
|
||||
Router::redirect('admin/settings');
|
||||
}
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
$updateDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$updateDecision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$defaultTenantId = (int) ($_POST['default_tenant_id'] ?? 0);
|
||||
$defaultRoleId = (int) ($_POST['default_role_id'] ?? 0);
|
||||
$defaultDepartmentId = (int) ($_POST['default_department_id'] ?? 0);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* @var array $departments
|
||||
* @var array $values
|
||||
* @var array $settings
|
||||
* @var bool $canUpdateSettings
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
@@ -66,7 +67,7 @@ $locales = defined('APP_LOCALES') ? APP_LOCALES : [];
|
||||
$brandingServicesFactory = new BrandingServicesFactory();
|
||||
$hasLogo = $brandingServicesFactory->createBrandingLogoService()->hasLogo();
|
||||
$hasFavicon = $brandingServicesFactory->createBrandingFaviconService()->hasFavicon();
|
||||
$canUpdateSettings = can('settings.update');
|
||||
$canUpdateSettings = (bool) ($canUpdateSettings ?? false);
|
||||
$isReadOnly = !$canUpdateSettings;
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Branding\BrandingServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Branding\BrandingServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Scheduler\SchedulerServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_USER_LIFECYCLE_RUN, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
Router::redirect('admin/settings');
|
||||
}
|
||||
|
||||
$factory = new SchedulerServicesFactory();
|
||||
$result = $factory->createUserLifecycleService()->run((int) ($_SESSION['user']['id'] ?? 0));
|
||||
$result = $factory->createUserLifecycleService()->run($currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = (string) ($result['error'] ?? 'unexpected_error');
|
||||
if ($error === 'lock_not_acquired') {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$tenantAvatarService = (new TenantServicesFactory())->createTenantAvatarService();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -22,6 +23,18 @@ if (!$tenantAvatarService->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $tenantAvatarService->saveUpload($uuid, $_FILES['avatar'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$tenantAvatarService = (new TenantServicesFactory())->createTenantAvatarService();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -22,6 +23,18 @@ if (!$tenantAvatarService->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantAvatarService->delete($uuid);
|
||||
Flash::success('Avatar removed', "admin/tenants/edit/{$uuid}", 'tenant_avatar_removed');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
define('MINTY_ALLOW_OUTPUT', true);
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$tenantAvatarService = (new TenantServicesFactory())->createTenantAvatarService();
|
||||
|
||||
$uuid = trim((string) ($_GET['uuid'] ?? ''));
|
||||
@@ -22,7 +25,11 @@ if ($tenantId <= 0) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
if (!directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('tenants', $tenantId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_AVATAR_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
http_response_code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,25 @@
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_CREATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$canManageSso = permissionGateway()->userHas($currentUserId, PermissionService::TENANTS_SSO_MANAGE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$canManageSso = is_array($capabilities) ? (bool) ($capabilities['can_manage_sso'] ?? false) : false;
|
||||
$canManageCustomFields = is_array($capabilities) ? (bool) ($capabilities['can_manage_custom_fields'] ?? false) : false;
|
||||
$tenantSsoService = (new AuthServicesFactory())->createTenantSsoService();
|
||||
|
||||
$errors = [];
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var bool $canManageCustomFields
|
||||
* @var bool $canManageSso
|
||||
*/
|
||||
|
||||
?>
|
||||
@@ -50,8 +52,8 @@
|
||||
<?php
|
||||
$values = $form ?? [];
|
||||
$detailsOpenAll = true;
|
||||
$showCustomFieldsTab = can('custom_fields.manage');
|
||||
$showSsoTab = can('tenants.sso_manage');
|
||||
$showCustomFieldsTab = (bool) ($canManageCustomFields ?? false);
|
||||
$showSsoTab = (bool) ($canManageSso ?? false);
|
||||
$customFieldTenantUuid = '';
|
||||
$customFieldDefinitions = [];
|
||||
require __DIR__ . '/_form.phtml';
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\CustomField\TenantCustomFieldService;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::CUSTOM_FIELDS_MANAGE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
|
||||
$tenantUuid = trim((string) ($id ?? ''));
|
||||
$tenant = $tenantUuid !== '' ? directoryServicesFactory()->createTenantService()->findByUuid($tenantUuid) : null;
|
||||
@@ -20,7 +21,11 @@ if (!$tenant) {
|
||||
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if (!directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('tenants', $tenantId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\CustomField\TenantCustomFieldService;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::CUSTOM_FIELDS_MANAGE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
|
||||
$fieldUuid = trim((string) ($id ?? ''));
|
||||
$tenantId = TenantCustomFieldService::definitionTenantIdByUuid($fieldUuid);
|
||||
@@ -19,7 +20,11 @@ if ($tenantId <= 0) {
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if (!directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('tenants', $tenantId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\CustomField\TenantCustomFieldService;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::CUSTOM_FIELDS_MANAGE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
|
||||
$fieldUuid = trim((string) ($id ?? ''));
|
||||
$tenantId = TenantCustomFieldService::definitionTenantIdByUuid($fieldUuid);
|
||||
@@ -19,7 +20,11 @@ if ($tenantId <= 0) {
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if (!directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('tenants', $tenantId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,26 @@
|
||||
|
||||
use MintyPHP\Repository\Tenant\TenantMicrosoftAuthRepository;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$allowedTenantIds = is_array($capabilities) ? array_values(array_unique(array_map('intval', (array) ($capabilities['allowed_tenant_ids'] ?? [])))) : [];
|
||||
$isStrictScope = is_array($capabilities) ? (bool) ($capabilities['is_strict_scope'] ?? false) : false;
|
||||
$hasGlobalAccess = is_array($capabilities) ? (bool) ($capabilities['has_global_access'] ?? false) : false;
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
@@ -100,21 +113,30 @@ if ($globalDefaultTheme === null || !isset($themes[$globalDefaultTheme])) {
|
||||
$globalDefaultTheme = isset($themes[$envTheme]) ? $envTheme : 'light';
|
||||
}
|
||||
|
||||
$result = directoryServicesFactory()->createTenantService()->listPaged([
|
||||
$listOptions = [
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => in_array($order, $computedOrderKeys, true) ? 'description' : $order,
|
||||
'dir' => $dir,
|
||||
]);
|
||||
];
|
||||
if (!$hasGlobalAccess) {
|
||||
if ($allowedTenantIds) {
|
||||
$listOptions['tenantIds'] = $allowedTenantIds;
|
||||
} elseif ($isStrictScope) {
|
||||
$listOptions['tenantIds'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
$result = directoryServicesFactory()->createTenantService()->listPaged($listOptions);
|
||||
|
||||
$rows = [];
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
if (in_array($order, $computedOrderKeys, true)) {
|
||||
$fullResult = directoryServicesFactory()->createTenantService()->listPaged([
|
||||
...$listOptions,
|
||||
'limit' => max(1, $total),
|
||||
'offset' => 0,
|
||||
'search' => $search,
|
||||
'order' => 'description',
|
||||
'dir' => 'asc',
|
||||
]);
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_DELETE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if ($uuid === '') {
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
|
||||
if (!$tenant) {
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_DELETE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = directoryServicesFactory()->createTenantService()->deleteByUuid($uuid);
|
||||
@@ -19,13 +39,14 @@ if (!($result['ok'] ?? false)) {
|
||||
if (($result['error'] ?? '') === 'tenant_has_departments') {
|
||||
Flash::error('Tenant can not be deleted while departments are assigned', 'admin/tenants', 'tenant_has_departments');
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
Flash::error('Tenant not found', 'admin/tenants', 'tenant_not_found');
|
||||
Router::redirect('admin/tenants');
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh session tenant data (removes deleted tenant, switches if it was current)
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
(new AuthServicesFactory())->createAuthService()->loadTenantDataIntoSession($currentUserId);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\CustomField\TenantCustomFieldService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
@@ -27,8 +27,30 @@ if (!$tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$authServicesFactory = new AuthServicesFactory();
|
||||
$tenantSsoService = $authServicesFactory->createTenantSsoService();
|
||||
$canManageCustomFields = permissionGateway()->userHas($currentUserId, PermissionService::CUSTOM_FIELDS_MANAGE);
|
||||
$canManageSso = permissionGateway()->userHas($currentUserId, PermissionService::TENANTS_SSO_MANAGE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$contextDecision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$capabilities = $contextDecision->attribute('capabilities', []);
|
||||
if (!is_array($capabilities)) {
|
||||
$capabilities = [];
|
||||
}
|
||||
|
||||
$canViewPage = (bool) ($capabilities['can_view_page'] ?? false);
|
||||
$canUpdateTenant = (bool) ($capabilities['can_update_tenant'] ?? false);
|
||||
$canManageCustomFields = (bool) ($capabilities['can_manage_custom_fields'] ?? false);
|
||||
$canManageSso = (bool) ($capabilities['can_manage_sso'] ?? false);
|
||||
if (!$canViewPage) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$customFieldDefinitions = $canManageCustomFields
|
||||
? TenantCustomFieldService::listForTenant($tenantId)
|
||||
: [];
|
||||
@@ -66,28 +88,27 @@ $errors = [];
|
||||
$form = array_merge($tenant, $ssoConfig);
|
||||
|
||||
if (isset($_POST['description'])) {
|
||||
if (!permissionGateway()->userHas($currentUserId, PermissionService::TENANTS_UPDATE)) {
|
||||
$submitDecision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
'input' => $_POST,
|
||||
]);
|
||||
if (!$submitDecision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$ssoFields = [
|
||||
'microsoft_enabled',
|
||||
'enforce_microsoft_login',
|
||||
'sync_profile_on_login',
|
||||
'sync_profile_fields',
|
||||
'entra_tenant_id',
|
||||
'allowed_domains',
|
||||
'use_shared_app',
|
||||
'client_id_override',
|
||||
'client_secret_override',
|
||||
'clear_client_secret_override',
|
||||
];
|
||||
foreach ($ssoFields as $ssoField) {
|
||||
if (array_key_exists($ssoField, $_POST) && !$canManageSso) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$submitCapabilities = $submitDecision->attribute('capabilities', []);
|
||||
if (is_array($submitCapabilities)) {
|
||||
$canUpdateTenant = (bool) ($submitCapabilities['can_update_tenant'] ?? $canUpdateTenant);
|
||||
$canManageCustomFields = (bool) ($submitCapabilities['can_manage_custom_fields'] ?? $canManageCustomFields);
|
||||
$canManageSso = (bool) ($submitCapabilities['can_manage_sso'] ?? $canManageSso);
|
||||
}
|
||||
if (!$canUpdateTenant) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = directoryServicesFactory()->createTenantService()->updateFromAdmin($tenantId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$tenantFaviconService = (new TenantServicesFactory())->createTenantFaviconService();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -22,6 +23,18 @@ if (!$tenantFaviconService->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $tenantFaviconService->saveUpload($uuid, $_FILES['favicon'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_UPDATE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$tenantFaviconService = (new TenantServicesFactory())->createTenantFaviconService();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -22,6 +23,18 @@ if (!$tenantFaviconService->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => $tenantId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantFaviconService->delete($uuid);
|
||||
Flash::success('Favicon removed', "admin/tenants/edit/{$uuid}", 'tenant_favicon_removed');
|
||||
Router::redirect("admin/tenants/edit/{$uuid}");
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::TENANTS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Guard::deny();
|
||||
}
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
$canCreateTenants = is_array($capabilities) ? (bool) ($capabilities['can_create_tenant'] ?? false) : false;
|
||||
|
||||
Buffer::set('title', t('Tenants'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$canCreateTenants = can('tenants.create');
|
||||
$canCreateTenants = (bool) ($canCreateTenants ?? false);
|
||||
?>
|
||||
<?php
|
||||
$breadcrumbs = [
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserAccessPdfService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
@@ -10,7 +11,7 @@ define('MINTY_ALLOW_OUTPUT', true);
|
||||
|
||||
// Hard gate: user must be logged in and explicitly allowed to generate access PDFs.
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_ACCESS_PDF);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
|
||||
// Support both route param and POSTed uuid (POST avoids exposing uuid in URL/history).
|
||||
@@ -26,8 +27,11 @@ if (!$user) {
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
// Enforce tenant scope on the target user before rendering any sensitive document.
|
||||
if ($userId <= 0 || !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserAccessPdfService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
@@ -11,7 +12,14 @@ define('MINTY_ALLOW_OUTPUT', true);
|
||||
|
||||
// Hard gate: user must be logged in and explicitly allowed to generate access PDFs.
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_ACCESS_PDF);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK, [
|
||||
'actor_user_id' => (int) ($_SESSION['user']['id'] ?? 0),
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
|
||||
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'POST') {
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -17,13 +20,17 @@ $uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
http_response_code($decision->status());
|
||||
Router::json(['error' => $decision->error()]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
$result = $userAccountService->setActiveByUuid($uuid, true, $currentUserId);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::API_TOKENS_MANAGE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
$apiTokenService = (new AuthServicesFactory())->createApiTokenService();
|
||||
|
||||
@@ -20,6 +21,15 @@ if (!$user) {
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$name = trim((string) ($_POST['token_name'] ?? ''));
|
||||
|
||||
if ($name === '') {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::API_TOKENS_MANAGE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
$apiTokenService = (new AuthServicesFactory())->createApiTokenService();
|
||||
|
||||
@@ -18,6 +19,17 @@ if (!$user) {
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$tokenId = (int) ($_POST['token_id'] ?? 0);
|
||||
|
||||
if ($tokenId > 0) {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAccountService = $userServicesFactory->createUserAccountService();
|
||||
$userAvatarService = $userServicesFactory->createUserAvatarService();
|
||||
@@ -25,7 +28,11 @@ if (!$userAvatarService->isValidUuid($uuid)) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_UPLOAD, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAccountService = $userServicesFactory->createUserAccountService();
|
||||
$userAvatarService = $userServicesFactory->createUserAvatarService();
|
||||
@@ -25,7 +28,11 @@ if (!$userAvatarService->isValidUuid($uuid)) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_DELETE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
define('MINTY_ALLOW_OUTPUT', true);
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAccountService = $userServicesFactory->createUserAccountService();
|
||||
$userAvatarService = $userServicesFactory->createUserAvatarService();
|
||||
@@ -20,7 +23,11 @@ if (!$userAvatarService->isValidUuid($uuid)) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
http_response_code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\Mail\MailServicesFactory;
|
||||
use MintyPHP\Service\User\UserAccessTemplateService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
@@ -14,12 +15,6 @@ $allowedActions = ['activate', 'deactivate', 'delete', 'send-access'];
|
||||
if (!in_array($action, $allowedActions, true)) {
|
||||
Router::json(['ok' => false, 'error' => 'invalid_action']);
|
||||
}
|
||||
if ($action === 'delete') {
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_DELETE);
|
||||
}
|
||||
if ($action === 'send-access') {
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_UPDATE);
|
||||
}
|
||||
|
||||
$raw = $_POST['uuids'] ?? '';
|
||||
$uuids = [];
|
||||
@@ -34,6 +29,16 @@ if (!$uuids) {
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_BULK, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'bulk_action' => $action,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
http_response_code($decision->status());
|
||||
Router::json(['ok' => false, 'error' => $decision->error()]);
|
||||
}
|
||||
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
$mailService = (new MailServicesFactory())->createMailService();
|
||||
|
||||
|
||||
@@ -2,15 +2,31 @@
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_CREATE);
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE, [
|
||||
'actor_user_id' => (int) ($_SESSION['user']['id'] ?? 0),
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code($decision->status());
|
||||
Router::json(['error' => $decision->error()]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAccountService = $userServicesFactory->createUserAccountService();
|
||||
$userAssignmentService = $userServicesFactory->createUserAssignmentService();
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW, [
|
||||
'actor_user_id' => (int) ($_SESSION['user']['id'] ?? 0),
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code($decision->status());
|
||||
Router::json(['error' => $decision->error()]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAccountService = $userServicesFactory->createUserAccountService();
|
||||
$userAvatarService = $userServicesFactory->createUserAvatarService();
|
||||
|
||||
if (!isset($_SESSION['user'])) {
|
||||
http_response_code(401);
|
||||
Router::json(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -18,13 +21,17 @@ $uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DEACTIVATE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
http_response_code($decision->status());
|
||||
Router::json(['error' => $decision->error()]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
$result = $userAccountService->setActiveByUuid($uuid, false, $currentUserId);
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_DELETE);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -20,13 +21,17 @@ $uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DELETE, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
http_response_code($decision->status());
|
||||
Router::json(['error' => $decision->error()]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
$result = $userAccountService->deleteByUuid($uuid, $currentUserId);
|
||||
|
||||
@@ -7,7 +7,7 @@ use MintyPHP\Repository\Auth\PasswordResetRepository;
|
||||
use MintyPHP\Repository\Auth\RememberTokenRepository;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
@@ -24,6 +24,7 @@ $userDepartmentRepository = $userServicesFactory->createUserDepartmentRepository
|
||||
$userPasswordPolicyService = $userServicesFactory->createUserPasswordPolicyService();
|
||||
$authServiceFactory = new AuthServicesFactory();
|
||||
$accessServicesFactory = new AccessServicesFactory();
|
||||
$authorizationService = $accessServicesFactory->createAuthorizationService();
|
||||
$rolePermissionRepository = $accessServicesFactory->createRolePermissionRepository();
|
||||
$authService = $authServiceFactory->createAuthService();
|
||||
$apiTokenService = $authServiceFactory->createApiTokenService();
|
||||
@@ -33,10 +34,6 @@ $passwordMinLength = $userPasswordPolicyService->minLength();
|
||||
$passwordHints = $userPasswordPolicyService->hints();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
permissionGateway()->getUserPermissions($currentUserId, true);
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
@@ -45,65 +42,91 @@ if (!$user) {
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$canViewUsers = permissionGateway()->userHas($currentUserId, PermissionService::USERS_VIEW);
|
||||
$canUpdateUser = permissionGateway()->userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = permissionGateway()->userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
$canUpdateAssignments = permissionGateway()->userHas($currentUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS);
|
||||
$isOwnAccount = $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
$canEditAssignments = $canEditUser && $canUpdateAssignments;
|
||||
if (!$isOwnAccount && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$contextDecision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$contextDecision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$canViewUsers && !$canEditUser) {
|
||||
|
||||
$capabilities = $contextDecision->attribute('capabilities', []);
|
||||
if (!is_array($capabilities)) {
|
||||
$capabilities = [];
|
||||
}
|
||||
$canViewUsers = (bool) ($capabilities['can_view_users'] ?? false);
|
||||
$canViewPage = (bool) ($capabilities['can_view_page'] ?? false);
|
||||
$canEditUser = (bool) ($capabilities['can_edit_user'] ?? false);
|
||||
$canEditAssignments = (bool) ($capabilities['can_edit_assignments'] ?? false);
|
||||
$canManageTenants = (bool) ($capabilities['can_manage_tenants'] ?? false);
|
||||
$canEditCustomFieldValues = (bool) ($capabilities['can_edit_custom_field_values'] ?? false);
|
||||
$canManageApiTokens = (bool) ($capabilities['can_manage_api_tokens'] ?? false);
|
||||
$canViewAddressBook = (bool) ($capabilities['can_view_address_book'] ?? false);
|
||||
$canViewUserMeta = (bool) ($capabilities['can_view_user_meta'] ?? false);
|
||||
$canViewUserAudit = (bool) ($capabilities['can_view_user_audit'] ?? false);
|
||||
$canAccessPdf = (bool) ($capabilities['can_access_pdf'] ?? false);
|
||||
$canDeleteUser = (bool) ($capabilities['can_delete_user'] ?? false);
|
||||
$canViewSecurityArtifacts = (bool) ($capabilities['can_view_security_artifacts'] ?? false);
|
||||
$canViewPermissionsTable = (bool) ($capabilities['can_view_permissions_table'] ?? false);
|
||||
$isOwnAccount = (bool) ($capabilities['is_own_account'] ?? false);
|
||||
$allowedTenantIdsRaw = $capabilities['allowed_tenant_ids'] ?? null;
|
||||
$allowedTenantIds = is_array($allowedTenantIdsRaw)
|
||||
? array_values(array_unique(array_filter(array_map('intval', $allowedTenantIdsRaw), static fn (int $tenantId): bool => $tenantId > 0)))
|
||||
: null;
|
||||
|
||||
if (!$canViewPage) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$creatorId = (int) ($user['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$user['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$user['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($user['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = $userAccountService->findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$user['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$user['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$activeChangedById = (int) ($user['active_changed_by'] ?? 0);
|
||||
if ($activeChangedById > 0) {
|
||||
$activeChanger = $userAccountService->findById($activeChangedById);
|
||||
if ($activeChanger) {
|
||||
$activeChangerName = trim(($activeChanger['first_name'] ?? '') . ' ' . ($activeChanger['last_name'] ?? ''));
|
||||
$user['active_changed_by_label'] = $activeChangerName !== '' ? $activeChangerName : ($activeChanger['email'] ?? '');
|
||||
$user['active_changed_by_uuid'] = $activeChanger['uuid'] ?? null;
|
||||
|
||||
if ($canViewUserMeta || $canViewUserAudit) {
|
||||
$creatorId = (int) ($user['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$user['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$user['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($user['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = $userAccountService->findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$user['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$user['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$activeChangedById = (int) ($user['active_changed_by'] ?? 0);
|
||||
if ($activeChangedById > 0) {
|
||||
$activeChanger = $userAccountService->findById($activeChangedById);
|
||||
if ($activeChanger) {
|
||||
$activeChangerName = trim(($activeChanger['first_name'] ?? '') . ' ' . ($activeChanger['last_name'] ?? ''));
|
||||
$user['active_changed_by_label'] = $activeChangerName !== '' ? $activeChangerName : ($activeChanger['email'] ?? '');
|
||||
$user['active_changed_by_uuid'] = $activeChanger['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = $user;
|
||||
// Users with tenants.update permission can see ALL tenants (to assign new tenants to users)
|
||||
$canManageTenants = permissionGateway()->userHas($currentUserId, PermissionService::TENANTS_UPDATE);
|
||||
$allowedTenantIds = $canManageTenants ? null : directoryServicesFactory()->createDirectoryScopeGateway()->getUserTenantIds($currentUserId);
|
||||
$scopeGateway = directoryServicesFactory()->createDirectoryScopeGateway();
|
||||
$strictTenantScope = $scopeGateway->isStrict();
|
||||
|
||||
$tenants = directoryServicesFactory()->createTenantService()->list();
|
||||
if ($allowedTenantIds) {
|
||||
if (is_array($allowedTenantIds)) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (!$canManageTenants && directoryServicesFactory()->createDirectoryScopeGateway()->isStrict()) {
|
||||
} elseif (!$canManageTenants && $strictTenantScope) {
|
||||
$tenants = [];
|
||||
}
|
||||
|
||||
$selectedTenantIds = $userTenantRepository->listTenantIdsByUserId($userId);
|
||||
if ($allowedTenantIds) {
|
||||
if (is_array($allowedTenantIds)) {
|
||||
$selectedTenantIds = array_values(array_intersect($selectedTenantIds, $allowedTenantIds));
|
||||
}
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
@@ -111,14 +134,15 @@ if (!$primaryTenantId && count($selectedTenantIds) === 1) {
|
||||
$primaryTenantId = (int) $selectedTenantIds[0];
|
||||
$user['primary_tenant_id'] = $primaryTenantId;
|
||||
}
|
||||
|
||||
$roles = directoryServicesFactory()->createRoleService()->listActive();
|
||||
$selectedRoleIds = $userRoleRepository->listRoleIdsByUserId($userId);
|
||||
$permissionRows = $rolePermissionRepository->listPermissionsWithRolesByRoleIds($selectedRoleIds);
|
||||
$permissionRows = $canViewPermissionsTable
|
||||
? $rolePermissionRepository->listPermissionsWithRolesByRoleIds($selectedRoleIds)
|
||||
: [];
|
||||
$selectedDepartmentIds = $userDepartmentRepository->listDepartmentIdsByUserId($userId);
|
||||
$departmentOptionsByTenant = directoryServicesFactory()->createDepartmentService()->groupActiveByTenantIds($selectedTenantIds);
|
||||
$canEditCustomFieldValues = $canEditUser
|
||||
&& (permissionGateway()->userHas($currentUserId, PermissionService::CUSTOM_FIELDS_EDIT_VALUES)
|
||||
|| ($isOwnAccount && $canUpdateSelf));
|
||||
|
||||
$customFieldDefinitionsByTenant = UserCustomFieldValueService::buildDefinitionsByTenant($selectedTenantIds);
|
||||
$customFieldDefinitionIds = [];
|
||||
foreach ($customFieldDefinitionsByTenant as $tenantDefinitions) {
|
||||
@@ -134,52 +158,65 @@ $customFieldPostedValues = [
|
||||
'custom_field_values' => [],
|
||||
'custom_field_values_multi' => [],
|
||||
];
|
||||
|
||||
$passwordResets = [];
|
||||
if ($canViewUsers || $canEditUser) {
|
||||
$passwordResets = $passwordResetRepository->listByUserId($userId, 25);
|
||||
}
|
||||
$rememberTokens = [];
|
||||
if ($canViewUsers || $canEditUser) {
|
||||
$rememberTokens = $rememberTokenRepository->listByUserId($userId, 25);
|
||||
}
|
||||
$apiTokens = [];
|
||||
$canManageApiTokens = $canEditUser
|
||||
&& permissionGateway()->userHas($currentUserId, PermissionService::API_TOKENS_MANAGE);
|
||||
if ($canViewUsers || $canEditUser) {
|
||||
if ($canViewSecurityArtifacts) {
|
||||
$passwordResets = $passwordResetRepository->listByUserId($userId, 25);
|
||||
$rememberTokens = $rememberTokenRepository->listByUserId($userId, 25);
|
||||
$apiTokens = $apiTokenService->listForUser($userId);
|
||||
}
|
||||
$showApiTokens = !empty($apiTokens) || $canManageApiTokens;
|
||||
// Keep initial $isOwnAccount/$canEditUser values for view permissions.
|
||||
$canManageApiTokens = $canManageApiTokens && $canViewSecurityArtifacts;
|
||||
$showApiTokens = $canViewSecurityArtifacts && (!empty($apiTokens) || $canManageApiTokens);
|
||||
|
||||
if (isset($_POST['email'])) {
|
||||
if (!$canEditUser) {
|
||||
$submitDecision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$submitDecision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$submitCapabilities = $submitDecision->attribute('capabilities', []);
|
||||
if (!is_array($submitCapabilities) || !isset($submitCapabilities['can_edit_user']) || !$submitCapabilities['can_edit_user']) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$canEditUser = (bool) $submitCapabilities['can_edit_user'];
|
||||
$canEditAssignments = (bool) ($submitCapabilities['can_edit_assignments'] ?? $canEditAssignments);
|
||||
$canEditCustomFieldValues = (bool) ($submitCapabilities['can_edit_custom_field_values'] ?? $canEditCustomFieldValues);
|
||||
|
||||
$result = $userAccountService->updateFromAdmin($userId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$tenantIds = $userAssignmentService->normalizeIdInput($_POST['tenant_ids'] ?? []);
|
||||
$existingTenantIds = $userTenantRepository->listTenantIdsByUserId($userId);
|
||||
$tenantIdsForSync = $tenantIds;
|
||||
if ($canEditAssignments && $allowedTenantIds) {
|
||||
|
||||
if ($canEditAssignments && is_array($allowedTenantIds)) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
$tenantIdsForSync = directoryServicesFactory()->createDirectoryScopeGateway()->mergeTenantIdsPreservingOutOfScope(
|
||||
$tenantIdsForSync = $scopeGateway->mergeTenantIdsPreservingOutOfScope(
|
||||
$tenantIds,
|
||||
$existingTenantIds,
|
||||
$allowedTenantIds
|
||||
);
|
||||
} elseif ($canEditAssignments && !$canManageTenants && directoryServicesFactory()->createDirectoryScopeGateway()->isStrict()) {
|
||||
} elseif ($canEditAssignments && !$canManageTenants && $strictTenantScope) {
|
||||
$tenantIds = [];
|
||||
$tenantIdsForSync = $existingTenantIds;
|
||||
}
|
||||
|
||||
if (!$canEditAssignments) {
|
||||
$tenantIds = $userTenantRepository->listTenantIdsByUserId($userId);
|
||||
if ($allowedTenantIds) {
|
||||
if (is_array($allowedTenantIds)) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
}
|
||||
$tenantIdsForSync = $tenantIds;
|
||||
}
|
||||
|
||||
$selectedTenantIds = $tenantIds;
|
||||
$primaryTenantId = (int) ($_POST['primary_tenant_id'] ?? 0);
|
||||
$selectedRoleIds = $userAssignmentService->normalizeIdInput($_POST['role_ids'] ?? []);
|
||||
@@ -211,16 +248,16 @@ if (isset($_POST['email'])) {
|
||||
$userAssignmentService->syncDepartments($userId, $selectedDepartmentIds, false);
|
||||
$userAssignmentService->bumpAuthzVersion($userId);
|
||||
$db->commit();
|
||||
} catch (\Throwable $exception) {
|
||||
} catch (\Throwable) {
|
||||
$db->rollback();
|
||||
$errors[] = t('User assignments can not be updated');
|
||||
}
|
||||
|
||||
// Update session if editing own account (tenant assignments changed)
|
||||
if ($currentUserId === $userId && !$errors) {
|
||||
$authService->loadTenantDataIntoSession($userId);
|
||||
}
|
||||
}
|
||||
|
||||
$customFieldSyncResult = UserCustomFieldValueService::syncForUser(
|
||||
$userId,
|
||||
$selectedTenantIds,
|
||||
|
||||
@@ -9,20 +9,22 @@
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Session;
|
||||
|
||||
$values = $form ?? $user ?? [];
|
||||
$values = $form;
|
||||
$avatarUuid = (string) ($values['uuid'] ?? '');
|
||||
$userAvatarService = (new UserServicesFactory())->createUserAvatarService();
|
||||
$hasAvatar = $avatarUuid !== '' && $userAvatarService->hasAvatar($avatarUuid);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === (int) ($values['id'] ?? 0);
|
||||
$titleText = $isOwnAccount ? t('My account') : t('Edit user');
|
||||
$canViewUsers = can('users.view');
|
||||
$canViewAddressBook = can('address_book.view');
|
||||
$canViewUserMeta = can('users.view_meta');
|
||||
$canViewUserAudit = can('users.view_audit');
|
||||
$canAccessPdf = can('users.access_pdf');
|
||||
$canViewUsers = !empty($canViewUsers);
|
||||
$canViewAddressBook = !empty($canViewAddressBook);
|
||||
$canViewUserMeta = !empty($canViewUserMeta);
|
||||
$canViewUserAudit = !empty($canViewUserAudit);
|
||||
$canAccessPdf = !empty($canAccessPdf);
|
||||
$canViewSecurityArtifacts = !empty($canViewSecurityArtifacts);
|
||||
$canViewPermissionsTable = !empty($canViewPermissionsTable);
|
||||
$canEditUser = !empty($canEditUser);
|
||||
$canDeleteUser = can('users.delete') && !$isOwnAccount;
|
||||
$canDeleteUser = !empty($canDeleteUser);
|
||||
$hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
$lastLoginProvider = strtolower((string) ($values['last_login_provider'] ?? ($user['last_login_provider'] ?? '')));
|
||||
$lastLoginAt = (string) ($values['last_login_at'] ?? ($user['last_login_at'] ?? ''));
|
||||
@@ -142,11 +144,11 @@ if ($lastLoginAt !== '') {
|
||||
$isReadOnly = !$canEditUser;
|
||||
$canEditAssignments = $canEditAssignments ?? false;
|
||||
$permissionRows = $permissionRows ?? [];
|
||||
$showPermissions = !empty($permissionRows) && can('permissions.view');
|
||||
$showPermissions = !empty($permissionRows) && $canViewPermissionsTable;
|
||||
$passwordResets = $passwordResets ?? [];
|
||||
$showPasswordResets = !empty($passwordResets);
|
||||
$showPasswordResets = $canViewSecurityArtifacts && !empty($passwordResets);
|
||||
$rememberTokens = $rememberTokens ?? [];
|
||||
$showRememberTokens = !empty($rememberTokens);
|
||||
$showRememberTokens = $canViewSecurityArtifacts && !empty($rememberTokens);
|
||||
$showDangerZone = $canDeleteUser;
|
||||
$dangerZoneDeleteFormId = $showDangerZone ? 'user-delete-form' : '';
|
||||
$dangerZoneWarning = t('This will permanently delete this user.');
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW, [
|
||||
'actor_user_id' => (int) ($_SESSION['user']['id'] ?? 0),
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code($decision->status());
|
||||
Router::json(['error' => $decision->error()]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
$rememberMeService = (new AuthServicesFactory())->createRememberMeService();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$canUpdateUser = permissionGateway()->userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = permissionGateway()->userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
@@ -23,14 +21,12 @@ if (!$user) {
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$isOwnAccount && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_FORGET_TOKENS, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_VIEW);
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW, [
|
||||
'actor_user_id' => (int) ($_SESSION['user']['id'] ?? 0),
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code($decision->status());
|
||||
Router::json(['error' => $decision->error()]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use MintyPHP\Service\Mail\MailServicesFactory;
|
||||
use MintyPHP\Service\User\UserAccessTemplateService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
@@ -9,11 +10,8 @@ use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
|
||||
$userAccountService = (new UserServicesFactory())->createUserAccountService();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$canUpdateUser = permissionGateway()->userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = permissionGateway()->userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
$mailService = (new MailServicesFactory())->createMailService();
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
@@ -24,14 +22,12 @@ if (!$user) {
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$isOwnAccount && !directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) {
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_SEND_ACCESS, [
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_user_id' => $userId,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\Security\SecurityServicesFactory;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
|
||||
ApiBootstrap::init();
|
||||
ApiBootstrap::init(false);
|
||||
ApiResponse::requireMethod('POST');
|
||||
|
||||
$input = ApiResponse::readJsonBody();
|
||||
|
||||
@@ -50,6 +50,20 @@ if ($method === 'POST') {
|
||||
ApiResponse::requirePermission(PermissionService::DEPARTMENTS_CREATE);
|
||||
|
||||
$input = ApiResponse::readJsonBody();
|
||||
if (array_key_exists('tenant_id', $input)) {
|
||||
ApiResponse::validationError(['tenant_id' => ['use_tenant_uuid']]);
|
||||
}
|
||||
if (array_key_exists('tenant_uuid', $input)) {
|
||||
$tenantUuid = trim((string) ($input['tenant_uuid'] ?? ''));
|
||||
if ($tenantUuid !== '') {
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($tenantUuid);
|
||||
if (!$tenant) {
|
||||
ApiResponse::validationError(['tenant_uuid' => ['not_found']]);
|
||||
}
|
||||
$input['tenant_id'] = (int) ($tenant['id'] ?? 0);
|
||||
}
|
||||
unset($input['tenant_uuid']);
|
||||
}
|
||||
$scopedTenantId = ApiAuth::scopedTenantId();
|
||||
if ($scopedTenantId) {
|
||||
$postedTenantId = (int) ($input['tenant_id'] ?? 0);
|
||||
|
||||
@@ -24,14 +24,21 @@ if ($method === 'GET') {
|
||||
|
||||
$departmentId = (int) ($department['id'] ?? 0);
|
||||
ApiAuth::requireResourceAccess('departments', $departmentId);
|
||||
$tenantUuid = '';
|
||||
$tenantId = (int) ($department['tenant_id'] ?? 0);
|
||||
if ($tenantId > 0) {
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findById($tenantId);
|
||||
$tenantUuid = (string) ($tenant['uuid'] ?? '');
|
||||
}
|
||||
|
||||
ApiResponse::success([
|
||||
'data' => [
|
||||
'uuid' => $department['uuid'] ?? '',
|
||||
'description' => $department['description'] ?? '',
|
||||
'code' => $department['code'] ?? '',
|
||||
'cost_center' => $department['cost_center'] ?? '',
|
||||
'active' => (bool) ($department['active'] ?? 1),
|
||||
'tenant_id' => $department['tenant_id'] ?? null,
|
||||
'tenant_uuid' => $tenantUuid,
|
||||
'created' => $department['created'] ?? '',
|
||||
'modified' => $department['modified'] ?? '',
|
||||
],
|
||||
@@ -50,6 +57,30 @@ if ($method === 'PUT' || $method === 'PATCH') {
|
||||
ApiAuth::requireResourceAccess('departments', $departmentId);
|
||||
|
||||
$input = ApiResponse::readJsonBody();
|
||||
if (array_key_exists('tenant_id', $input)) {
|
||||
ApiResponse::validationError(['tenant_id' => ['use_tenant_uuid']]);
|
||||
}
|
||||
if (array_key_exists('tenant_uuid', $input)) {
|
||||
$tenantUuid = trim((string) ($input['tenant_uuid'] ?? ''));
|
||||
if ($tenantUuid !== '') {
|
||||
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($tenantUuid);
|
||||
if (!$tenant) {
|
||||
ApiResponse::validationError(['tenant_uuid' => ['not_found']]);
|
||||
}
|
||||
$input['tenant_id'] = (int) ($tenant['id'] ?? 0);
|
||||
} else {
|
||||
$input['tenant_id'] = 0;
|
||||
}
|
||||
unset($input['tenant_uuid']);
|
||||
}
|
||||
$scopedTenantId = ApiAuth::scopedTenantId();
|
||||
if ($scopedTenantId) {
|
||||
$postedTenantId = (int) ($input['tenant_id'] ?? 0);
|
||||
if ($postedTenantId > 0 && $postedTenantId !== $scopedTenantId) {
|
||||
ApiResponse::forbidden('tenant_scoped_token_forbidden');
|
||||
}
|
||||
$input['tenant_id'] = $scopedTenantId;
|
||||
}
|
||||
$result = directoryServicesFactory()->createDepartmentService()->updateFromAdmin($departmentId, $input, ApiAuth::userId());
|
||||
ApiResponse::fromServiceResult($result);
|
||||
}
|
||||
|
||||
63
pages/api/v1/me/avatar().php
Normal file
63
pages/api/v1/me/avatar().php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\ApiAuth;
|
||||
use MintyPHP\Http\ApiBootstrap;
|
||||
use MintyPHP\Http\ApiResponse;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
|
||||
define('MINTY_ALLOW_OUTPUT', true);
|
||||
|
||||
ApiBootstrap::init();
|
||||
|
||||
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAvatarService = $userServicesFactory->createUserAvatarService();
|
||||
$user = ApiAuth::user();
|
||||
$uuid = (string) ($user['uuid'] ?? '');
|
||||
|
||||
if ($method === 'GET') {
|
||||
$size = isset($_GET['size']) ? (int) $_GET['size'] : null;
|
||||
|
||||
$path = $userAvatarService->findAvatarPath($uuid, $size);
|
||||
if (!$path || !is_file($path)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$mime = $userAvatarService->detectMime($path);
|
||||
header('Content-Type: ' . $mime);
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Security-Policy: sandbox');
|
||||
header('Cache-Control: private, max-age=300');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
readfile($path);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
|
||||
ApiResponse::forbidden();
|
||||
}
|
||||
|
||||
$result = $userAvatarService->saveUpload($uuid, $_FILES['avatar'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? 'upload_failed';
|
||||
ApiResponse::error($error, 422);
|
||||
}
|
||||
|
||||
ApiResponse::created(['data' => ['message' => 'avatar_updated']]);
|
||||
}
|
||||
|
||||
if ($method === 'DELETE') {
|
||||
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
|
||||
ApiResponse::forbidden();
|
||||
}
|
||||
|
||||
$userAvatarService->delete($uuid);
|
||||
ApiResponse::noContent();
|
||||
}
|
||||
|
||||
ApiResponse::methodNotAllowed();
|
||||
@@ -8,30 +8,77 @@ use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
|
||||
ApiBootstrap::init();
|
||||
ApiResponse::requireMethod('GET');
|
||||
$userAssignmentService = (new UserServicesFactory())->createUserAssignmentService();
|
||||
|
||||
$user = ApiAuth::user();
|
||||
$userId = ApiAuth::userId();
|
||||
$assignments = $userAssignmentService->buildAssignmentsForUser($userId);
|
||||
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAssignmentService = $userServicesFactory->createUserAssignmentService();
|
||||
|
||||
$currentTenantId = (int) (ApiAuth::tenantId() ?? 0);
|
||||
$currentTenant = $userAssignmentService->findTenantSummaryInAssignments($assignments, $currentTenantId);
|
||||
if ($method === 'GET') {
|
||||
$user = ApiAuth::user();
|
||||
$userId = ApiAuth::userId();
|
||||
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
$primaryTenant = $userAssignmentService->findTenantSummaryInAssignments($assignments, $primaryTenantId);
|
||||
ApiResponse::success([
|
||||
'data' => buildMeResponse($user, $userId, $userAssignmentService),
|
||||
]);
|
||||
}
|
||||
|
||||
$publicAssignments = $userAssignmentService->mapAssignmentsToPublic($assignments);
|
||||
$canViewPermissions = ApiAuth::hasPermission(PermissionService::PERMISSIONS_VIEW);
|
||||
$publicPermissions = $canViewPermissions ? ApiAuth::permissions() : [];
|
||||
$publicCustomFields = UserCustomFieldValueService::buildPublicValuesByTenant(
|
||||
$userId,
|
||||
$assignments['tenants'] ?? [],
|
||||
ApiAuth::scopedTenantId()
|
||||
);
|
||||
if ($method === 'PUT' || $method === 'PATCH') {
|
||||
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
|
||||
ApiResponse::forbidden();
|
||||
}
|
||||
|
||||
ApiResponse::success([
|
||||
'data' => [
|
||||
$userId = ApiAuth::userId();
|
||||
$input = ApiResponse::readJsonBody();
|
||||
|
||||
$userAccountService = $userServicesFactory->createUserAccountService();
|
||||
$result = $userAccountService->updateSelfProfile($userId, $input);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$errors = $result['errors'] ?? [];
|
||||
if (is_array($errors) && $errors !== []) {
|
||||
ApiResponse::validationError(['profile' => array_values($errors)]);
|
||||
}
|
||||
ApiResponse::fromServiceResult($result);
|
||||
}
|
||||
|
||||
// Reload user after update
|
||||
$user = $userAccountService->findById($userId);
|
||||
if (!$user) {
|
||||
ApiResponse::error('update_failed', 500);
|
||||
}
|
||||
|
||||
ApiResponse::success([
|
||||
'data' => buildMeResponse($user, $userId, $userAssignmentService),
|
||||
]);
|
||||
}
|
||||
|
||||
ApiResponse::methodNotAllowed();
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $user
|
||||
* @param int $userId
|
||||
* @param \MintyPHP\Service\User\UserAssignmentService $userAssignmentService
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function buildMeResponse(array $user, int $userId, $userAssignmentService): array
|
||||
{
|
||||
$assignments = $userAssignmentService->buildAssignmentsForUser($userId);
|
||||
|
||||
$currentTenantId = (int) (ApiAuth::tenantId() ?? 0);
|
||||
$currentTenant = $userAssignmentService->findTenantSummaryInAssignments($assignments, $currentTenantId);
|
||||
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
$primaryTenant = $userAssignmentService->findTenantSummaryInAssignments($assignments, $primaryTenantId);
|
||||
|
||||
$publicAssignments = $userAssignmentService->mapAssignmentsToPublic($assignments);
|
||||
$canViewPermissions = ApiAuth::hasPermission(PermissionService::PERMISSIONS_VIEW);
|
||||
$publicPermissions = $canViewPermissions ? ApiAuth::permissions() : [];
|
||||
$publicCustomFields = UserCustomFieldValueService::buildPublicValuesByTenant(
|
||||
$userId,
|
||||
$assignments['tenants'] ?? [],
|
||||
ApiAuth::scopedTenantId()
|
||||
);
|
||||
|
||||
return [
|
||||
'uuid' => $user['uuid'] ?? '',
|
||||
'first_name' => $user['first_name'] ?? '',
|
||||
'last_name' => $user['last_name'] ?? '',
|
||||
@@ -41,11 +88,19 @@ ApiResponse::success([
|
||||
'phone' => $user['phone'] ?? '',
|
||||
'mobile' => $user['mobile'] ?? '',
|
||||
'locale' => $user['locale'] ?? '',
|
||||
'theme' => $user['theme'] ?? '',
|
||||
'profile_description' => $user['profile_description'] ?? '',
|
||||
'short_dial' => $user['short_dial'] ?? '',
|
||||
'address' => $user['address'] ?? '',
|
||||
'postal_code' => $user['postal_code'] ?? '',
|
||||
'city' => $user['city'] ?? '',
|
||||
'country' => $user['country'] ?? '',
|
||||
'region' => $user['region'] ?? '',
|
||||
'current_tenant' => $currentTenant,
|
||||
'primary_tenant' => $primaryTenant,
|
||||
'can_view_permissions' => $canViewPermissions,
|
||||
'permissions' => $publicPermissions,
|
||||
'assignments' => $publicAssignments,
|
||||
'custom_fields' => $publicCustomFields,
|
||||
],
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
58
pages/api/v1/me/password().php
Normal file
58
pages/api/v1/me/password().php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\ApiAuth;
|
||||
use MintyPHP\Http\ApiBootstrap;
|
||||
use MintyPHP\Http\ApiResponse;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
|
||||
ApiBootstrap::init();
|
||||
ApiResponse::requireMethod('POST');
|
||||
|
||||
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
|
||||
ApiResponse::forbidden();
|
||||
}
|
||||
|
||||
$input = ApiResponse::readJsonBody();
|
||||
$currentPassword = (string) ($input['current_password'] ?? '');
|
||||
$password = (string) ($input['password'] ?? '');
|
||||
$password2 = (string) ($input['password2'] ?? '');
|
||||
|
||||
$validationErrors = [];
|
||||
if ($currentPassword === '') {
|
||||
$validationErrors['current_password'] = ['required'];
|
||||
}
|
||||
if ($password === '') {
|
||||
$validationErrors['password'] = ['required'];
|
||||
}
|
||||
if ($password2 === '') {
|
||||
$validationErrors['password2'] = ['required'];
|
||||
}
|
||||
if ($validationErrors) {
|
||||
ApiResponse::validationError($validationErrors);
|
||||
}
|
||||
|
||||
$userServicesFactory = new UserServicesFactory();
|
||||
$userAccountService = $userServicesFactory->createUserAccountService();
|
||||
$userPasswordService = $userServicesFactory->createUserPasswordService();
|
||||
|
||||
$userId = ApiAuth::userId();
|
||||
$user = $userAccountService->findById($userId);
|
||||
if (!$user) {
|
||||
ApiResponse::error('user_not_found', 404);
|
||||
}
|
||||
|
||||
if (!password_verify($currentPassword, (string) ($user['password'] ?? ''))) {
|
||||
ApiResponse::validationError(['current_password' => ['invalid']]);
|
||||
}
|
||||
|
||||
$result = $userPasswordService->resetPassword($userId, $password, $password2);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$errors = $result['errors'] ?? [];
|
||||
if ($errors) {
|
||||
ApiResponse::validationError(['password' => $errors]);
|
||||
}
|
||||
ApiResponse::error('password_change_failed', 422);
|
||||
}
|
||||
|
||||
ApiResponse::noContent();
|
||||
46
pages/api/v1/permissions/index().php
Normal file
46
pages/api/v1/permissions/index().php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\ApiBootstrap;
|
||||
use MintyPHP\Http\ApiResponse;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
|
||||
ApiBootstrap::init();
|
||||
ApiResponse::requireMethod('GET');
|
||||
ApiResponse::requirePermission(PermissionService::PERMISSIONS_VIEW);
|
||||
|
||||
$permissionService = (new AccessServicesFactory())->createPermissionService();
|
||||
|
||||
$allowedOrders = ['key', 'description', 'active', 'created'];
|
||||
$order = in_array($_GET['order'] ?? '', $allowedOrders, true) ? $_GET['order'] : 'key';
|
||||
$dir = in_array(strtolower($_GET['dir'] ?? ''), ['asc', 'desc'], true) ? strtolower($_GET['dir']) : 'asc';
|
||||
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 25)));
|
||||
$offset = max(0, (int) ($_GET['offset'] ?? 0));
|
||||
|
||||
$options = [
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => trim((string) ($_GET['search'] ?? '')),
|
||||
'active' => $_GET['active'] ?? null,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
];
|
||||
|
||||
$result = $permissionService->listPaged($options);
|
||||
|
||||
$rows = [];
|
||||
foreach (($result['data'] ?? []) as $row) {
|
||||
$rows[] = [
|
||||
'key' => $row['key'] ?? '',
|
||||
'description' => $row['description'] ?? '',
|
||||
'active' => (bool) ($row['active'] ?? 1),
|
||||
'is_system' => (bool) ($row['is_system'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
ApiResponse::success([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
]);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user