page update
This commit is contained in:
@@ -10,6 +10,7 @@ return [
|
|||||||
['path' => 'password/reset', 'target' => 'auth/reset', 'public' => true],
|
['path' => 'password/reset', 'target' => 'auth/reset', 'public' => true],
|
||||||
['path' => 'verify-email', 'target' => 'auth/verify-email', 'public' => true],
|
['path' => 'verify-email', 'target' => 'auth/verify-email', 'public' => true],
|
||||||
['path' => 'lang', 'target' => 'lang', 'public' => true],
|
['path' => 'lang', 'target' => 'lang', 'public' => true],
|
||||||
['path' => 'imprint', 'target' => 'page/imprint', 'public' => true],
|
['path' => 'imprint', 'target' => 'public-page/imprint', 'public' => true],
|
||||||
['path' => 'privacy', 'target' => 'page/privacy', 'public' => true],
|
['path' => 'privacy', 'target' => 'public-page/privacy', 'public' => true],
|
||||||
|
['path' => 'terms', 'target' => 'public-page/terms', 'public' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -875,8 +875,61 @@ WHERE u.email = 'demo@user.com'
|
|||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO `pages` (`uuid`, `slug`, `created`)
|
INSERT INTO `pages` (`uuid`, `slug`, `created`)
|
||||||
SELECT UUID(), 'impressum', NOW()
|
SELECT UUID(), v.slug, NOW()
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM pages WHERE slug = 'impressum');
|
FROM (
|
||||||
|
SELECT 'imprint' AS slug
|
||||||
|
UNION ALL SELECT 'privacy'
|
||||||
|
UNION ALL SELECT 'terms'
|
||||||
|
) AS v
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM pages p WHERE p.slug = v.slug);
|
||||||
|
|
||||||
|
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
||||||
|
SELECT p.id, 'de', '{"blocks":[{"type":"header","data":{"text":"Impressum","level":2}},{"type":"paragraph","data":{"text":"Diese Seite ist eine Vorlage. Bitte tragen Sie hier die Pflichtangaben Ihres Unternehmens ein."}},{"type":"paragraph","data":{"text":"Angaben gemaess Paragraph 5 TMG: Firmenname, Rechtsform, vertretungsberechtigte Person, Anschrift, Kontakt und Registerangaben."}},{"type":"paragraph","data":{"text":"Inhaltlich verantwortlich: Name und Anschrift."}}]}', p.created
|
||||||
|
FROM pages p
|
||||||
|
WHERE p.slug = 'imprint'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM page_contents pc WHERE pc.page_id = p.id AND pc.locale = 'de'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
||||||
|
SELECT p.id, 'en', '{"blocks":[{"type":"header","data":{"text":"Imprint","level":2}},{"type":"paragraph","data":{"text":"This page is a template. Please add your mandatory company details here."}},{"type":"paragraph","data":{"text":"Provide legal publisher information, address, contact channels, company register data and authorized representatives."}},{"type":"paragraph","data":{"text":"Responsible for content: name and address."}}]}', p.created
|
||||||
|
FROM pages p
|
||||||
|
WHERE p.slug = 'imprint'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM page_contents pc WHERE pc.page_id = p.id AND pc.locale = 'en'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
||||||
|
SELECT p.id, 'de', '{"blocks":[{"type":"header","data":{"text":"Datenschutz","level":2}},{"type":"paragraph","data":{"text":"Diese Seite ist eine Vorlage. Beschreiben Sie hier transparent, welche personenbezogenen Daten verarbeitet werden."}},{"type":"paragraph","data":{"text":"Nennen Sie Rechtsgrundlagen, Speicherdauer, Empfaenger, Betroffenenrechte und Kontakt der Datenschutzstelle."}},{"type":"paragraph","data":{"text":"Pruefen Sie die Inhalte regelmaessig mit Ihrem Datenschutzbeauftragten."}}]}', p.created
|
||||||
|
FROM pages p
|
||||||
|
WHERE p.slug = 'privacy'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM page_contents pc WHERE pc.page_id = p.id AND pc.locale = 'de'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
||||||
|
SELECT p.id, 'en', '{"blocks":[{"type":"header","data":{"text":"Privacy","level":2}},{"type":"paragraph","data":{"text":"This page is a template. Explain which personal data is processed and for what purpose."}},{"type":"paragraph","data":{"text":"Document legal bases, retention periods, recipients, data subject rights and your privacy contact point."}},{"type":"paragraph","data":{"text":"Review this content regularly with your legal or privacy team."}}]}', p.created
|
||||||
|
FROM pages p
|
||||||
|
WHERE p.slug = 'privacy'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM page_contents pc WHERE pc.page_id = p.id AND pc.locale = 'en'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
||||||
|
SELECT p.id, 'de', '{"blocks":[{"type":"header","data":{"text":"Nutzungsbedingungen","level":2}},{"type":"paragraph","data":{"text":"Diese Seite ist eine Vorlage. Beschreiben Sie hier Zweck, Leistungsumfang und zulaessige Nutzung Ihres Angebots."}},{"type":"paragraph","data":{"text":"Ergaenzen Sie Regelungen zu Haftung, Laufzeit, Kuendigung, Gerichtsstand und anwendbarem Recht."}},{"type":"paragraph","data":{"text":"Lassen Sie die Inhalte vor Veroeffentlichung rechtlich pruefen."}}]}', p.created
|
||||||
|
FROM pages p
|
||||||
|
WHERE p.slug = 'terms'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM page_contents pc WHERE pc.page_id = p.id AND pc.locale = 'de'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
||||||
|
SELECT p.id, 'en', '{"blocks":[{"type":"header","data":{"text":"Terms of Service","level":2}},{"type":"paragraph","data":{"text":"This page is a template. Describe the service scope, intended use and user obligations."}},{"type":"paragraph","data":{"text":"Add clauses for liability, term, termination, governing law and jurisdiction."}},{"type":"paragraph","data":{"text":"Have this content reviewed by legal counsel before publishing."}}]}', p.created
|
||||||
|
FROM pages p
|
||||||
|
WHERE p.slug = 'terms'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM page_contents pc WHERE pc.page_id = p.id AND pc.locale = 'en'
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
INSERT INTO `page_contents` (`page_id`, `locale`, `content`, `created`)
|
||||||
SELECT p.id, 'de', NULL, p.created
|
SELECT p.id, 'de', NULL, p.created
|
||||||
|
|||||||
@@ -752,6 +752,7 @@
|
|||||||
"Unverified": "Nicht verifiziert",
|
"Unverified": "Nicht verifiziert",
|
||||||
"Imprint": "Impressum",
|
"Imprint": "Impressum",
|
||||||
"Privacy": "Datenschutz",
|
"Privacy": "Datenschutz",
|
||||||
|
"Terms of Service": "Nutzungsbedingungen",
|
||||||
"Custom fields": "Zusatzfelder",
|
"Custom fields": "Zusatzfelder",
|
||||||
"User custom fields": "Benutzerzusatzfelder",
|
"User custom fields": "Benutzerzusatzfelder",
|
||||||
"Affected tenants": "Betroffene Mandanten",
|
"Affected tenants": "Betroffene Mandanten",
|
||||||
@@ -1271,5 +1272,17 @@
|
|||||||
"Force on": "Erzwingen (an)",
|
"Force on": "Erzwingen (an)",
|
||||||
"Force off": "Erzwingen (aus)",
|
"Force off": "Erzwingen (aus)",
|
||||||
"Controls whether Microsoft logins automatically persist a remember-me token.": "Steuert, ob Microsoft-Anmeldungen automatisch ein Angemeldet-bleiben-Token setzen.",
|
"Controls whether Microsoft logins automatically persist a remember-me token.": "Steuert, ob Microsoft-Anmeldungen automatisch ein Angemeldet-bleiben-Token setzen.",
|
||||||
|
"Complete": "Vollständig",
|
||||||
|
"Incomplete": "Unvollständig",
|
||||||
|
"No domain restrictions": "Keine Domain-Einschränkungen",
|
||||||
|
"Configuration status": "Konfigurationsstatus",
|
||||||
|
"SSO setup is incomplete": "SSO-Konfiguration ist unvollständig",
|
||||||
|
"Complete the required Microsoft configuration before enforcing Microsoft-only login.": "Vervollständigen Sie die erforderliche Microsoft-Konfiguration, bevor Sie Microsoft-only-Anmeldung erzwingen.",
|
||||||
|
"Login policy": "Anmelderichtlinie",
|
||||||
|
"Allow Microsoft-only login (disable local password login)": "Nur Microsoft-Anmeldung erlauben (lokale Passwort-Anmeldung deaktivieren)",
|
||||||
|
"Remember login after Microsoft sign-in": "Angemeldet bleiben nach Microsoft-Login",
|
||||||
|
"Access and profile sync": "Zugriff und Profilabgleich",
|
||||||
|
"Profile sync": "Profilabgleich",
|
||||||
|
"App credentials (advanced)": "App-Anmeldedaten (Erweitert)",
|
||||||
"Remember token lifetime is invalid": "Token-Lebensdauer ist ungültig"
|
"Remember token lifetime is invalid": "Token-Lebensdauer ist ungültig"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -752,6 +752,7 @@
|
|||||||
"Unverified": "Unverified",
|
"Unverified": "Unverified",
|
||||||
"Imprint": "Imprint",
|
"Imprint": "Imprint",
|
||||||
"Privacy": "Privacy",
|
"Privacy": "Privacy",
|
||||||
|
"Terms of Service": "Terms of Service",
|
||||||
"Custom fields": "Custom fields",
|
"Custom fields": "Custom fields",
|
||||||
"User custom fields": "User custom fields",
|
"User custom fields": "User custom fields",
|
||||||
"Affected tenants": "Affected tenants",
|
"Affected tenants": "Affected tenants",
|
||||||
@@ -1271,5 +1272,17 @@
|
|||||||
"Force on": "Force on",
|
"Force on": "Force on",
|
||||||
"Force off": "Force off",
|
"Force off": "Force off",
|
||||||
"Controls whether Microsoft logins automatically persist a remember-me token.": "Controls whether Microsoft logins automatically persist a remember-me token.",
|
"Controls whether Microsoft logins automatically persist a remember-me token.": "Controls whether Microsoft logins automatically persist a remember-me token.",
|
||||||
|
"Complete": "Complete",
|
||||||
|
"Incomplete": "Incomplete",
|
||||||
|
"No domain restrictions": "No domain restrictions",
|
||||||
|
"Configuration status": "Configuration status",
|
||||||
|
"SSO setup is incomplete": "SSO setup is incomplete",
|
||||||
|
"Complete the required Microsoft configuration before enforcing Microsoft-only login.": "Complete the required Microsoft configuration before enforcing Microsoft-only login.",
|
||||||
|
"Login policy": "Login policy",
|
||||||
|
"Allow Microsoft-only login (disable local password login)": "Allow Microsoft-only login (disable local password login)",
|
||||||
|
"Remember login after Microsoft sign-in": "Remember login after Microsoft sign-in",
|
||||||
|
"Access and profile sync": "Access and profile sync",
|
||||||
|
"Profile sync": "Profile sync",
|
||||||
|
"App credentials (advanced)": "App credentials (advanced)",
|
||||||
"Remember token lifetime is invalid": "Remember token lifetime is invalid"
|
"Remember token lifetime is invalid": "Remember token lifetime is invalid"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,6 @@ class AccessControl
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check page/ prefix with slug lookup
|
|
||||||
if (str_starts_with($normalizedPath, 'page/')) {
|
|
||||||
$pageSlug = substr($normalizedPath, strlen('page/'));
|
|
||||||
if (in_array($pageSlug, $this->configuredPublicPaths, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check always-public prefixes
|
// Check always-public prefixes
|
||||||
foreach (self::ALWAYS_PUBLIC_PREFIXES as $prefix) {
|
foreach (self::ALWAYS_PUBLIC_PREFIXES as $prefix) {
|
||||||
if (str_starts_with($normalizedPath, $prefix)) {
|
if (str_starts_with($normalizedPath, $prefix)) {
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ function navActivePublicPages(array $extraSlugs = []): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "page/<slug>" routes and known standalone slugs should highlight the same nav entry.
|
// "page/<slug>" routes and known standalone slugs should highlight the same nav entry.
|
||||||
$publicSlugs = array_merge(['imprint', 'privacy'], $extraSlugs);
|
$publicSlugs = array_merge(['imprint', 'privacy', 'terms'], $extraSlugs);
|
||||||
$isActive = strpos($current, 'page/') === 0 || in_array($current, $publicSlugs, true);
|
$isActive = strpos($current, 'page/') === 0 || in_array($current, $publicSlugs, true);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -104,6 +104,43 @@ $ssoStatusTiles = [
|
|||||||
'tone' => $ssoCredentialSource === 'shared' ? 'info' : 'neutral',
|
'tone' => $ssoCredentialSource === 'shared' ? 'info' : 'neutral',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
$allowedDomainsList = preg_split('/[\s,;]+/', strtolower($allowedDomains), -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$allowedDomainsList = is_array($allowedDomainsList) ? array_values(array_unique($allowedDomainsList)) : [];
|
||||||
|
$allowedDomainsCount = count($allowedDomainsList);
|
||||||
|
$syncFieldCount = count($syncProfileFields);
|
||||||
|
|
||||||
|
$ssoSetupBadgeVariant = $microsoftEnabled ? 'success' : 'neutral';
|
||||||
|
$ssoSetupBadgeLabel = sprintf(
|
||||||
|
'%s: %s',
|
||||||
|
t('Microsoft login'),
|
||||||
|
$microsoftEnabled ? t('Active') : t('Inactive')
|
||||||
|
);
|
||||||
|
$ssoShowConfigBadge = $microsoftEnabled;
|
||||||
|
$ssoConfigBadgeVariant = $microsoftEnabled ? ($ssoConfigComplete ? 'success' : 'warning') : 'neutral';
|
||||||
|
$ssoConfigBadgeLabel = sprintf(
|
||||||
|
'%s: %s',
|
||||||
|
t('Configuration status'),
|
||||||
|
$ssoConfigComplete ? t('Complete') : t('Incomplete')
|
||||||
|
);
|
||||||
|
|
||||||
|
$ssoPolicyBadgeVariant = match ($ssoPasswordMode) {
|
||||||
|
'microsoft_only' => 'warning',
|
||||||
|
'local_and_microsoft' => 'info',
|
||||||
|
default => 'neutral',
|
||||||
|
};
|
||||||
|
$ssoSyncBadgeVariant = $syncProfileOnLogin ? 'info' : 'neutral';
|
||||||
|
$ssoSyncBadgeLabel = $syncProfileOnLogin ? t('Active') : t('Inactive');
|
||||||
|
$ssoDomainsBadgeVariant = $allowedDomainsCount > 0 ? 'info' : 'neutral';
|
||||||
|
$ssoDomainsBadgeLabel = $allowedDomainsCount > 0
|
||||||
|
? sprintf('%d %s', $allowedDomainsCount, t('Allowed email domains'))
|
||||||
|
: t('No domain restrictions');
|
||||||
|
$ssoCredentialsBadgeVariant = $ssoCredentialSource === 'shared' ? 'info' : 'warning';
|
||||||
|
$ssoCredentialsBadgeLabel = $ssoCredentialSourceLabel;
|
||||||
|
|
||||||
|
$openSetupCard = $detailsOpenAll || !$microsoftEnabled || ($microsoftEnabled && !$ssoConfigComplete);
|
||||||
|
$openPolicyCard = $detailsOpenAll || ($microsoftEnabled && $microsoftEnforce);
|
||||||
|
$openAccessCard = $detailsOpenAll || ($microsoftEnabled && ($allowedDomainsCount > 0 || $syncProfileOnLogin));
|
||||||
|
$openOverrideCard = $detailsOpenAll;
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<form id="<?php e($formId); ?>" method="post" data-standard-detail-form="1">
|
<form id="<?php e($formId); ?>" method="post" data-standard-detail-form="1">
|
||||||
@@ -446,137 +483,188 @@ $ssoStatusTiles = [
|
|||||||
?>
|
?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($microsoftEnabled && !$ssoConfigComplete && $ssoConfigErrorLabel !== ''): ?>
|
<?php if ($microsoftEnabled && !$ssoConfigComplete): ?>
|
||||||
<small class="muted"><?php e($ssoConfigErrorLabel); ?></small>
|
<blockquote data-variant="warning">
|
||||||
|
<strong><?php e(t('SSO setup is incomplete')); ?></strong><br>
|
||||||
|
<?php if ($ssoConfigErrorLabel !== ''): ?>
|
||||||
|
<?php e($ssoConfigErrorLabel); ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php e(t('Complete the required Microsoft configuration before enforcing Microsoft-only login.')); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</blockquote>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<fieldset>
|
<details class="app-details-card" name="tenant-sso-setup" <?php e($openSetupCard ? 'open' : ''); ?>>
|
||||||
<legend><small><?php e(t('Required Microsoft configuration')); ?></small></legend>
|
<summary>
|
||||||
<label class="app-field">
|
<span class="app-details-card-summary-title"><?php e(t('Required Microsoft configuration')); ?></span>
|
||||||
<input
|
<span class="app-details-card-summary-meta">
|
||||||
type="checkbox"
|
<span class="badge" data-variant="<?php e($ssoSetupBadgeVariant); ?>"><?php e($ssoSetupBadgeLabel); ?></span>
|
||||||
role="switch"
|
<?php if ($ssoShowConfigBadge): ?>
|
||||||
name="microsoft_enabled"
|
<span class="badge" data-variant="<?php e($ssoConfigBadgeVariant); ?>"><?php e($ssoConfigBadgeLabel); ?></span>
|
||||||
value="1"
|
<?php endif; ?>
|
||||||
data-tenant-sso-enabled
|
</span>
|
||||||
<?php e($microsoftEnabled ? 'checked' : ''); ?>
|
</summary>
|
||||||
<?php e($disabledAttr); ?>>
|
<div class="app-details-card-container">
|
||||||
<span><?php e(t('Enable Microsoft login for this tenant')); ?></span>
|
<label class="app-field">
|
||||||
</label>
|
<input
|
||||||
<small class="muted"><?php e(t('Required for Microsoft login.')); ?></small>
|
type="checkbox"
|
||||||
<hr>
|
role="switch"
|
||||||
<label>
|
name="microsoft_enabled"
|
||||||
<span><?php e(t('Entra tenant ID (tid)')); ?></span>
|
value="1"
|
||||||
<input
|
data-tenant-sso-enabled
|
||||||
type="text"
|
<?php e($microsoftEnabled ? 'checked' : ''); ?>
|
||||||
name="entra_tenant_id"
|
<?php e($disabledAttr); ?>>
|
||||||
value="<?php e($entraTenantId); ?>"
|
<span><?php e(t('Enable Microsoft login for this tenant')); ?></span>
|
||||||
placeholder="00000000-0000-0000-0000-000000000000"
|
</label>
|
||||||
<?php e($readonlyAttr); ?>>
|
<small class="muted"><?php e(t('Required for Microsoft login.')); ?></small>
|
||||||
</label>
|
<div data-tenant-sso-when-enabled>
|
||||||
<label class="app-field">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
name="enforce_microsoft_login"
|
|
||||||
value="1"
|
|
||||||
<?php e($microsoftEnforce ? 'checked' : ''); ?>
|
|
||||||
<?php e($disabledAttr); ?>>
|
|
||||||
<span><?php e(t('Disable password login for this tenant login')); ?></span>
|
|
||||||
</label>
|
|
||||||
<small class="muted"><?php e(t('Microsoft-only login requires a complete configuration')); ?></small>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset data-tenant-sso-when-enabled>
|
|
||||||
<legend><small><?php e(t('Optional restrictions')); ?></small></legend>
|
|
||||||
<label>
|
|
||||||
<span><?php e(t('Allowed email domains')); ?></span>
|
|
||||||
<textarea name="allowed_domains" rows="3" placeholder="example.com partner.com" <?php e($readonlyAttr); ?>><?php e($allowedDomains); ?></textarea>
|
|
||||||
<small><?php e(t('Optional allow-list, comma or newline separated (example.com).')); ?></small>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset data-tenant-sso-when-enabled>
|
|
||||||
<legend><small><?php e(t('Login persistence')); ?></small></legend>
|
|
||||||
<label class="app-field">
|
|
||||||
<span><?php e(t('Microsoft Auto-Remember')); ?></span>
|
|
||||||
<select name="microsoft_auto_remember_mode" <?php e($readonlyAttr); ?>>
|
|
||||||
<option value="inherit" <?php if (($form['auto_remember_mode'] ?? null) === null) echo 'selected'; ?>><?php e(t('Inherit global default')); ?></option>
|
|
||||||
<option value="1" <?php if (($form['auto_remember_mode'] ?? null) === 1 || ($form['auto_remember_mode'] ?? null) === '1') echo 'selected'; ?>><?php e(t('Force on')); ?></option>
|
|
||||||
<option value="0" <?php if (($form['auto_remember_mode'] ?? null) === 0 || ($form['auto_remember_mode'] ?? null) === '0') echo 'selected'; ?>><?php e(t('Force off')); ?></option>
|
|
||||||
</select>
|
|
||||||
<small class="muted"><?php e(t('Controls whether Microsoft logins automatically persist a remember-me token.')); ?></small>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset data-tenant-sso-when-enabled>
|
|
||||||
<legend><small><?php e(t('Profile sync (optional)')); ?></small></legend>
|
|
||||||
<label class="app-field">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
name="sync_profile_on_login"
|
|
||||||
value="1"
|
|
||||||
data-tenant-sso-sync-toggle
|
|
||||||
<?php e($syncProfileOnLogin ? 'checked' : ''); ?>
|
|
||||||
<?php e($disabledAttr); ?>>
|
|
||||||
<span><?php e(t('Profile sync on Microsoft login')); ?></span>
|
|
||||||
</label>
|
|
||||||
<small class="muted"><?php e(t('User profile fields are global and affect all tenants of this user')); ?></small>
|
|
||||||
<div data-tenant-sso-sync-fields>
|
|
||||||
<hr>
|
|
||||||
<label><span><?php e(t('Sync fields')); ?></span></label>
|
|
||||||
<div class="grid grid-1-1">
|
|
||||||
<?php foreach ($syncFieldOptions as $syncFieldKey => $syncFieldLabel): ?>
|
|
||||||
<label class="app-field">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="sync_profile_fields[]"
|
|
||||||
value="<?php e($syncFieldKey); ?>"
|
|
||||||
<?php e(in_array($syncFieldKey, $syncProfileFields, true) ? 'checked' : ''); ?>
|
|
||||||
<?php e($disabledAttr); ?>>
|
|
||||||
<span><?php e(t($syncFieldLabel)); ?></span>
|
|
||||||
</label>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php if ($ssoSyncNeedsGraph): ?>
|
|
||||||
<hr>
|
<hr>
|
||||||
<small class="muted"><?php e(t('Phone/mobile/avatar sync requires Microsoft Graph (User.Read)')); ?></small>
|
<label>
|
||||||
<?php endif; ?>
|
<span><?php e(t('Entra tenant ID (tid)')); ?></span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="entra_tenant_id"
|
||||||
|
value="<?php e($entraTenantId); ?>"
|
||||||
|
placeholder="00000000-0000-0000-0000-000000000000"
|
||||||
|
<?php e($readonlyAttr); ?>>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</details>
|
||||||
|
|
||||||
<details data-tenant-sso-override-details data-remember-state="admin-tenant-sso-override">
|
<details class="app-details-card" name="tenant-sso-policy" data-tenant-sso-when-enabled <?php e($openPolicyCard ? 'open' : ''); ?>>
|
||||||
<summary><?php e(t('Advanced app override')); ?></summary>
|
<summary>
|
||||||
<hr>
|
<span class="app-details-card-summary-title"><?php e(t('Login policy')); ?></span>
|
||||||
<label class="app-field" data-tenant-sso-when-enabled>
|
<span class="app-details-card-summary-meta">
|
||||||
<input type="hidden" name="use_shared_app" value="0">
|
<span class="badge" data-variant="<?php e($ssoPolicyBadgeVariant); ?>"><?php e($ssoPasswordModeLabel); ?></span>
|
||||||
<input
|
</span>
|
||||||
type="checkbox"
|
</summary>
|
||||||
role="switch"
|
<div class="app-details-card-container">
|
||||||
name="use_shared_app"
|
<label class="app-field">
|
||||||
value="1"
|
<input
|
||||||
data-tenant-sso-shared-toggle
|
type="checkbox"
|
||||||
<?php e($useSharedApp ? 'checked' : ''); ?>
|
role="switch"
|
||||||
<?php e($disabledAttr); ?>>
|
name="enforce_microsoft_login"
|
||||||
<span><?php e(t('Use shared app credentials from settings')); ?></span>
|
value="1"
|
||||||
</label>
|
<?php e($microsoftEnforce ? 'checked' : ''); ?>
|
||||||
<small class="muted"><?php e(t('Override only if this tenant needs a separate Entra app.')); ?></small>
|
<?php e($disabledAttr); ?>>
|
||||||
<div class="grid" data-tenant-sso-override-fields>
|
<span><?php e(t('Allow Microsoft-only login (disable local password login)')); ?></span>
|
||||||
<label>
|
|
||||||
<span><?php e(t('Client ID override')); ?></span>
|
|
||||||
<input type="text" name="client_id_override" value="<?php e($clientIdOverride); ?>" <?php e($readonlyAttr); ?>>
|
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<small class="muted"><?php e(t('Microsoft-only login requires a complete configuration')); ?></small>
|
||||||
<span><?php e(t('Client secret override')); ?></span>
|
<hr>
|
||||||
<input type="password" name="client_secret_override" value="" autocomplete="new-password" <?php e($readonlyAttr); ?>>
|
<label class="app-field">
|
||||||
<small><?php e(t('Write-only. Leave empty to keep current secret.')); ?></small>
|
<span><?php e(t('Remember login after Microsoft sign-in')); ?></span>
|
||||||
|
<select name="microsoft_auto_remember_mode" <?php e($readonlyAttr); ?>>
|
||||||
|
<option value="inherit" <?php if (($form['auto_remember_mode'] ?? null) === null) echo 'selected'; ?>><?php e(t('Inherit global default')); ?></option>
|
||||||
|
<option value="1" <?php if (($form['auto_remember_mode'] ?? null) === 1 || ($form['auto_remember_mode'] ?? null) === '1') echo 'selected'; ?>><?php e(t('Force on')); ?></option>
|
||||||
|
<option value="0" <?php if (($form['auto_remember_mode'] ?? null) === 0 || ($form['auto_remember_mode'] ?? null) === '0') echo 'selected'; ?>><?php e(t('Force off')); ?></option>
|
||||||
|
</select>
|
||||||
|
<small class="muted"><?php e(t('Controls whether Microsoft logins automatically persist a remember-me token.')); ?></small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="app-details-card" name="tenant-sso-access-sync" data-tenant-sso-when-enabled <?php e($openAccessCard ? 'open' : ''); ?>>
|
||||||
|
<summary>
|
||||||
|
<span class="app-details-card-summary-title"><?php e(t('Access and profile sync')); ?></span>
|
||||||
|
<span class="app-details-card-summary-meta">
|
||||||
|
<span class="badge" data-variant="<?php e($ssoDomainsBadgeVariant); ?>"><?php e($ssoDomainsBadgeLabel); ?></span>
|
||||||
|
<span class="badge" data-variant="<?php e($ssoSyncBadgeVariant); ?>">
|
||||||
|
<?php e(t('Profile sync')); ?>: <?php e($ssoSyncBadgeLabel); ?>
|
||||||
|
</span>
|
||||||
|
<?php if ($syncProfileOnLogin): ?>
|
||||||
|
<span class="badge" data-variant="info">
|
||||||
|
<?php e(t('Sync fields')); ?>: <?php e((string) $syncFieldCount); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="app-details-card-container">
|
||||||
|
<label>
|
||||||
|
<span><?php e(t('Allowed email domains')); ?></span>
|
||||||
|
<textarea name="allowed_domains" rows="3" placeholder="example.com partner.com" <?php e($readonlyAttr); ?>><?php e($allowedDomains); ?></textarea>
|
||||||
|
<small><?php e(t('Optional allow-list, comma or newline separated (example.com).')); ?></small>
|
||||||
|
</label>
|
||||||
|
<hr>
|
||||||
|
<label class="app-field">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
name="sync_profile_on_login"
|
||||||
|
value="1"
|
||||||
|
data-tenant-sso-sync-toggle
|
||||||
|
<?php e($syncProfileOnLogin ? 'checked' : ''); ?>
|
||||||
|
<?php e($disabledAttr); ?>>
|
||||||
|
<span><?php e(t('Profile sync on Microsoft login')); ?></span>
|
||||||
|
</label>
|
||||||
|
<small class="muted"><?php e(t('User profile fields are global and affect all tenants of this user')); ?></small>
|
||||||
|
<div data-tenant-sso-sync-fields>
|
||||||
|
<hr>
|
||||||
|
<label><span><?php e(t('Sync fields')); ?></span></label>
|
||||||
|
<div class="grid grid-1-1">
|
||||||
|
<?php foreach ($syncFieldOptions as $syncFieldKey => $syncFieldLabel): ?>
|
||||||
|
<label class="app-field">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="sync_profile_fields[]"
|
||||||
|
value="<?php e($syncFieldKey); ?>"
|
||||||
|
<?php e(in_array($syncFieldKey, $syncProfileFields, true) ? 'checked' : ''); ?>
|
||||||
|
<?php e($disabledAttr); ?>>
|
||||||
|
<span><?php e(t($syncFieldLabel)); ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php if ($ssoSyncNeedsGraph): ?>
|
||||||
|
<hr>
|
||||||
|
<small class="muted"><?php e(t('Phone/mobile/avatar sync requires Microsoft Graph (User.Read)')); ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details
|
||||||
|
class="app-details-card"
|
||||||
|
name="tenant-sso-advanced"
|
||||||
|
data-tenant-sso-override-details
|
||||||
|
data-tenant-sso-when-enabled
|
||||||
|
data-remember-state="admin-tenant-sso-override"
|
||||||
|
<?php e($openOverrideCard ? 'open' : ''); ?>>
|
||||||
|
<summary>
|
||||||
|
<span class="app-details-card-summary-title"><?php e(t('App credentials (advanced)')); ?></span>
|
||||||
|
<span class="app-details-card-summary-meta">
|
||||||
|
<span class="badge" data-variant="<?php e($ssoCredentialsBadgeVariant); ?>"><?php e($ssoCredentialsBadgeLabel); ?></span>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="app-details-card-container">
|
||||||
|
<label class="app-field">
|
||||||
|
<input type="hidden" name="use_shared_app" value="0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
name="use_shared_app"
|
||||||
|
value="1"
|
||||||
|
data-tenant-sso-shared-toggle
|
||||||
|
<?php e($useSharedApp ? 'checked' : ''); ?>
|
||||||
|
<?php e($disabledAttr); ?>>
|
||||||
|
<span><?php e(t('Use shared app credentials from settings')); ?></span>
|
||||||
|
</label>
|
||||||
|
<small class="muted"><?php e(t('Override only if this tenant needs a separate Entra app.')); ?></small>
|
||||||
|
<div class="grid" data-tenant-sso-override-fields>
|
||||||
|
<label>
|
||||||
|
<span><?php e(t('Client ID override')); ?></span>
|
||||||
|
<input type="text" name="client_id_override" value="<?php e($clientIdOverride); ?>" <?php e($readonlyAttr); ?>>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span><?php e(t('Client secret override')); ?></span>
|
||||||
|
<input type="password" name="client_secret_override" value="" autocomplete="new-password" <?php e($readonlyAttr); ?>>
|
||||||
|
<small><?php e(t('Write-only. Leave empty to keep current secret.')); ?></small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="app-field" data-tenant-sso-override-fields>
|
||||||
|
<input type="checkbox" name="clear_client_secret_override" value="1" <?php e($disabledAttr); ?>>
|
||||||
|
<span><?php e(t('Clear override secret')); ?></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="app-field" data-tenant-sso-override-fields>
|
|
||||||
<input type="checkbox" name="clear_client_secret_override" value="1" <?php e($disabledAttr); ?>>
|
|
||||||
<span><?php e(t('Clear override secret')); ?></span>
|
|
||||||
</label>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -698,4 +786,4 @@ $ssoStatusTiles = [
|
|||||||
<?php Session::getCsrfInput(); ?>
|
<?php Session::getCsrfInput(); ?>
|
||||||
</form>
|
</form>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use MintyPHP\Http\Request;
|
|||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\I18n;
|
use MintyPHP\I18n;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
|
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||||
|
use MintyPHP\Service\Access\UiAccessService;
|
||||||
use MintyPHP\Service\Content\PageService;
|
use MintyPHP\Service\Content\PageService;
|
||||||
use MintyPHP\Session;
|
use MintyPHP\Session;
|
||||||
use MintyPHP\Support\Flash;
|
use MintyPHP\Support\Flash;
|
||||||
@@ -18,8 +20,18 @@ if ($currentUserId <= 0) {
|
|||||||
Router::redirect('login');
|
Router::redirect('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$return = Request::safeReturnTarget((string) (requestInput()->bodyAll()['return'] ?? ''));
|
||||||
|
$canEdit = app(UiAccessService::class)->allow(
|
||||||
|
$currentUserId,
|
||||||
|
SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE
|
||||||
|
);
|
||||||
|
if (!$canEdit) {
|
||||||
|
http_response_code(403);
|
||||||
|
Flash::error(t('You are not allowed to edit this page'), $return, 'page_forbidden');
|
||||||
|
Router::redirect($return !== '' ? $return : '');
|
||||||
|
}
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!Session::checkCsrfToken()) {
|
||||||
$return = Request::safeReturnTarget((string) (requestInput()->bodyAll()['return'] ?? ''));
|
|
||||||
Flash::error(t('Form expired, please try again'), $return, 'page_csrf');
|
Flash::error(t('Form expired, please try again'), $return, 'page_csrf');
|
||||||
Router::redirect($return !== '' ? $return : '');
|
Router::redirect($return !== '' ? $return : '');
|
||||||
}
|
}
|
||||||
@@ -28,7 +40,6 @@ $slug = trim((string) (requestInput()->bodyAll()['slug'] ?? ''));
|
|||||||
$fromLocale = trim((string) (requestInput()->bodyAll()['from_locale'] ?? (I18n::$locale ?? '')));
|
$fromLocale = trim((string) (requestInput()->bodyAll()['from_locale'] ?? (I18n::$locale ?? '')));
|
||||||
$toLocale = trim((string) (requestInput()->bodyAll()['to_locale'] ?? ''));
|
$toLocale = trim((string) (requestInput()->bodyAll()['to_locale'] ?? ''));
|
||||||
|
|
||||||
$return = Request::safeReturnTarget((string) (requestInput()->bodyAll()['return'] ?? ''));
|
|
||||||
if ($return === '' && $slug !== '') {
|
if ($return === '' && $slug !== '') {
|
||||||
$return = 'page/' . $slug;
|
$return = 'page/' . $slug;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use MintyPHP\Http\Request;
|
|||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\I18n;
|
use MintyPHP\I18n;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
|
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||||
|
use MintyPHP\Service\Access\UiAccessService;
|
||||||
use MintyPHP\Service\Content\PageService;
|
use MintyPHP\Service\Content\PageService;
|
||||||
use MintyPHP\Session;
|
use MintyPHP\Session;
|
||||||
use MintyPHP\Support\Flash;
|
use MintyPHP\Support\Flash;
|
||||||
@@ -28,7 +30,10 @@ if ($notFound) {
|
|||||||
|
|
||||||
$user = $session['user'] ?? [];
|
$user = $session['user'] ?? [];
|
||||||
$currentUserId = (int) ($user['id'] ?? 0);
|
$currentUserId = (int) ($user['id'] ?? 0);
|
||||||
$canEdit = $currentUserId > 0;
|
$canEdit = app(UiAccessService::class)->allow(
|
||||||
|
$currentUserId,
|
||||||
|
SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE
|
||||||
|
);
|
||||||
$returnPath = Request::path();
|
$returnPath = Request::path();
|
||||||
if ($returnPath === '') {
|
if ($returnPath === '') {
|
||||||
$returnPath = 'page/' . $slug;
|
$returnPath = 'page/' . $slug;
|
||||||
|
|||||||
113
pages/page/index(default).phtml
Normal file
113
pages/page/index(default).phtml
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array|null $page
|
||||||
|
* @var bool $canEdit
|
||||||
|
* @var bool $editMode
|
||||||
|
* @var string $contentJson
|
||||||
|
* @var string $slug
|
||||||
|
*/
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="app-details-container app-page">
|
||||||
|
<section>
|
||||||
|
<?php if (!$page): ?>
|
||||||
|
<p><?php e(t('Page not found')); ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="post" action="<?php e($formAction ?? ''); ?>" class="page-editor" id="page-editor" data-page-editor
|
||||||
|
data-can-edit="<?php e($canEdit ? '1' : '0'); ?>" data-edit-mode="<?php e($editMode ? 'edit' : 'view'); ?>"
|
||||||
|
data-placeholder="<?php e(t('Start writing...')); ?>" data-header-placeholder="<?php e(t('Heading')); ?>">
|
||||||
|
<?php MintyPHP\Session::getCsrfInput(); ?>
|
||||||
|
<input type="hidden" name="return" value="<?php e($returnPath ?? ''); ?>">
|
||||||
|
<textarea name="content" id="page-content" hidden><?php e($contentJson); ?></textarea>
|
||||||
|
<div data-editor-holder></div>
|
||||||
|
</form>
|
||||||
|
<?php if (file_exists('web/vendor/editorjs/tools/header.umd.js')): ?>
|
||||||
|
<script src="<?php e(assetVersion('vendor/editorjs/tools/header.umd.js')); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (file_exists('web/vendor/editorjs/tools/list.umd.js')): ?>
|
||||||
|
<script src="<?php e(assetVersion('vendor/editorjs/tools/list.umd.js')); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (file_exists('web/vendor/editorjs/tools/checklist.umd.js')): ?>
|
||||||
|
<script src="<?php e(assetVersion('vendor/editorjs/tools/checklist.umd.js')); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (file_exists('web/vendor/editorjs/tools/table.umd.js')): ?>
|
||||||
|
<script src="<?php e(assetVersion('vendor/editorjs/tools/table.umd.js')); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (file_exists('web/vendor/editorjs/tools/columns.js')): ?>
|
||||||
|
<script src="<?php e(assetVersion('vendor/editorjs/tools/columns.js')); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (file_exists('web/vendor/editorjs/tools/marker.umd.js')): ?>
|
||||||
|
<script src="<?php e(assetVersion('vendor/editorjs/tools/marker.umd.js')); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<script type="module" src="<?php e(assetVersion('js/components/app-page-editor.js')); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
<?php if ($canEdit): ?>
|
||||||
|
<aside id="app-details-aside-section">
|
||||||
|
<div class="app-details-aside-section app-page-action-card">
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<button type="button" class="secondary outline" data-tooltip-pos="top"
|
||||||
|
data-tooltip="<?php e($editMode ? t('View') : t('Edit')); ?>" data-editor-toggle
|
||||||
|
data-edit-label="<?php e(t('Edit')); ?>" data-view-label="<?php e(t('View')); ?>">
|
||||||
|
<?php if ($editMode): ?>
|
||||||
|
<i class="bi bi-eye-fill" data-editor-icon aria-hidden="true"></i>
|
||||||
|
<?php else: ?>
|
||||||
|
<i class="bi bi-pencil-square" data-editor-icon aria-hidden="true"></i>
|
||||||
|
<?php endif; ?>
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="page-editor" class="primary" data-editor-save>
|
||||||
|
<?php e(t('Save')); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($locales, $currentLocale) && is_array($locales) && count($locales) > 1): ?>
|
||||||
|
<hr>
|
||||||
|
<details>
|
||||||
|
<summary><?php e(t('Copy')); ?></summary>
|
||||||
|
<hr>
|
||||||
|
<form method="post" action="page/copy-language" class="app-page-copy">
|
||||||
|
<?php MintyPHP\Session::getCsrfInput(); ?>
|
||||||
|
<input type="hidden" name="slug" value="<?php e($slug ?? ''); ?>">
|
||||||
|
<input type="hidden" name="from_locale" value="<?php e($currentLocale); ?>">
|
||||||
|
<input type="hidden" name="return" value="<?php e($returnPath ?? ''); ?>">
|
||||||
|
<label class="app-field">
|
||||||
|
<span>
|
||||||
|
<?php e(t('Copy content to')); ?>
|
||||||
|
</span>
|
||||||
|
<div role="group">
|
||||||
|
<select name="to_locale" required>
|
||||||
|
<?php foreach ($locales as $locale): ?>
|
||||||
|
<?php if ($locale === $currentLocale) {
|
||||||
|
continue;
|
||||||
|
} ?>
|
||||||
|
<option value="<?php e($locale); ?>">
|
||||||
|
<?php
|
||||||
|
$label = strtoupper($locale);
|
||||||
|
if ($locale === 'de') {
|
||||||
|
$label = t('German');
|
||||||
|
} elseif ($locale === 'en') {
|
||||||
|
$label = t('English');
|
||||||
|
}
|
||||||
|
e($label);
|
||||||
|
?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="secondary outline">
|
||||||
|
<?php e(t('Copy')); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
<small>
|
||||||
|
<?php e(t('Existing content in the target language will be overwritten')); ?>
|
||||||
|
</small>
|
||||||
|
</details>
|
||||||
|
<hr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
47
pages/public-page/index($slug).php
Normal file
47
pages/public-page/index($slug).php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use MintyPHP\Buffer;
|
||||||
|
use MintyPHP\Http\Request;
|
||||||
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
|
use MintyPHP\I18n;
|
||||||
|
use MintyPHP\Router;
|
||||||
|
use MintyPHP\Service\Content\PageService;
|
||||||
|
|
||||||
|
$sessionStore = app(SessionStoreInterface::class);
|
||||||
|
$session = $sessionStore->all();
|
||||||
|
|
||||||
|
$slug = trim((string) ($slug ?? ''));
|
||||||
|
if ($slug === '') {
|
||||||
|
Router::redirect('');
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUserId = (int) (($session['user']['id'] ?? 0));
|
||||||
|
if ($currentUserId > 0) {
|
||||||
|
Router::redirect(Request::withLocale('page/' . $slug, I18n::$locale ?? I18n::$defaultLocale));
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageBundle = PageService::findBySlugWithLocale($slug, I18n::$locale ?? null);
|
||||||
|
$page = $pageBundle['page'] ?? null;
|
||||||
|
$pageContent = $pageBundle['content'] ?? null;
|
||||||
|
$notFound = $page === null;
|
||||||
|
if ($notFound) {
|
||||||
|
http_response_code(404);
|
||||||
|
Buffer::set('title', t('Page not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$returnPath = Request::path();
|
||||||
|
if ($returnPath === '') {
|
||||||
|
$returnPath = $slug;
|
||||||
|
}
|
||||||
|
$formAction = '/' . ltrim($returnPath, '/');
|
||||||
|
|
||||||
|
$contentJson = '';
|
||||||
|
if (is_array($pageContent) && isset($pageContent['content'])) {
|
||||||
|
$contentJson = (string) $pageContent['content'];
|
||||||
|
}
|
||||||
|
if ($contentJson === '') {
|
||||||
|
$contentJson = json_encode(['blocks' => []], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
$canEdit = false;
|
||||||
|
$editMode = false;
|
||||||
@@ -41,6 +41,7 @@ if ($bufferTitle !== '') {
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="<?php e(lurl('imprint')); ?>"><?php e(t('Imprint')); ?></a></li>
|
<li><a href="<?php e(lurl('imprint')); ?>"><?php e(t('Imprint')); ?></a></li>
|
||||||
<li><a href="<?php e(lurl('privacy')); ?>"><?php e(t('Privacy')); ?></a></li>
|
<li><a href="<?php e(lurl('privacy')); ?>"><?php e(t('Privacy')); ?></a></li>
|
||||||
|
<li><a href="<?php e(lurl('terms')); ?>"><?php e(t('Terms of Service')); ?></a></li>
|
||||||
<li><a href="<?php e(lurl('login')); ?>"><?php e(t('Login')); ?></a></li>
|
<li><a href="<?php e(lurl('login')); ?>"><?php e(t('Login')); ?></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
$isAuthenticated = !empty($_SESSION['user']['id']);
|
||||||
|
$pagePrefix = $isAuthenticated ? 'page/' : '';
|
||||||
|
?>
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<nav aria-label="<?php e(t('Footer navigation')); ?>">
|
<nav aria-label="<?php e(t('Footer navigation')); ?>">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -7,10 +11,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="<?php e(lurl('imprint')); ?>" class="muted"><small><?php e(t('Imprint')); ?></small></a>
|
<a href="<?php e(lurl($pagePrefix . 'imprint')); ?>" class="muted"><small><?php e(t('Imprint')); ?></small></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="<?php e(lurl('privacy')); ?>" class="muted"><small><?php e(t('Privacy')); ?></small></a>
|
<a href="<?php e(lurl($pagePrefix . 'privacy')); ?>" class="muted"><small><?php e(t('Privacy')); ?></small></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="<?php e(lurl($pagePrefix . 'terms')); ?>" class="muted"><small><?php e(t('Terms of Service')); ?></small></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
56
tests/Architecture/PublicPageContractTest.php
Normal file
56
tests/Architecture/PublicPageContractTest.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Tests\Architecture;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class PublicPageContractTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProjectFileAssertionSupport;
|
||||||
|
|
||||||
|
public function testPublicStaticPageRoutesPointToPublicPageTargets(): void
|
||||||
|
{
|
||||||
|
$routes = include $this->projectRootPath() . '/config/routes.php';
|
||||||
|
$this->assertIsArray($routes);
|
||||||
|
|
||||||
|
$byPath = [];
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
if (!is_array($route)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$path = trim((string) ($route['path'] ?? ''));
|
||||||
|
if ($path === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$byPath[$path] = $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['imprint', 'privacy', 'terms'] as $slug) {
|
||||||
|
$this->assertArrayHasKey($slug, $byPath);
|
||||||
|
$this->assertSame('public-page/' . $slug, (string) ($byPath[$slug]['target'] ?? ''));
|
||||||
|
$this->assertTrue((bool) ($byPath[$slug]['public'] ?? false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPageTemplatesAreSeparatedByContext(): void
|
||||||
|
{
|
||||||
|
$root = $this->projectRootPath();
|
||||||
|
|
||||||
|
$this->assertFileExists($root . '/pages/page/index(default).phtml');
|
||||||
|
$this->assertFileDoesNotExist($root . '/pages/page/index(page).phtml');
|
||||||
|
$this->assertFileExists($root . '/pages/public-page/index(page).phtml');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPageMutationsUseSettingsUpdateCapabilityGuard(): void
|
||||||
|
{
|
||||||
|
$indexAction = $this->readProjectFile('pages/page/index($slug).php');
|
||||||
|
$copyAction = $this->readProjectFile('pages/page/copy-language().php');
|
||||||
|
|
||||||
|
$this->assertStringContainsString('UiAccessService::class', $indexAction);
|
||||||
|
$this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE', $indexAction);
|
||||||
|
$this->assertStringNotContainsString('$canEdit = $currentUserId > 0;', $indexAction);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('UiAccessService::class', $copyAction);
|
||||||
|
$this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE', $copyAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
tests/Http/AccessControlTest.php
Normal file
33
tests/Http/AccessControlTest.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Tests\Http;
|
||||||
|
|
||||||
|
use MintyPHP\Http\AccessControl;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class AccessControlTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testConfiguredPublicPathIsAccessible(): void
|
||||||
|
{
|
||||||
|
$accessControl = new AccessControl(['imprint', 'privacy']);
|
||||||
|
|
||||||
|
$this->assertTrue($accessControl->isPublicPath('imprint'));
|
||||||
|
$this->assertTrue($accessControl->isPublicPath('/privacy'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPageSlugPathIsNotAutoPublic(): void
|
||||||
|
{
|
||||||
|
$accessControl = new AccessControl(['imprint']);
|
||||||
|
|
||||||
|
$this->assertTrue($accessControl->isPublicPath('imprint'));
|
||||||
|
$this->assertFalse($accessControl->isPublicPath('page/imprint'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAlwaysPublicPrefixesRemainAccessible(): void
|
||||||
|
{
|
||||||
|
$accessControl = new AccessControl([]);
|
||||||
|
|
||||||
|
$this->assertTrue($accessControl->isPublicPath('api/v1/health'));
|
||||||
|
$this->assertTrue($accessControl->isPublicPath('auth/microsoft/callback'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,76 +1,81 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
.app-page-header {
|
.app-page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--app-spacing);
|
gap: var(--app-spacing);
|
||||||
margin-bottom: var(--app-spacing);
|
margin-bottom: var(--app-spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-page-header > div {
|
.app-page-header > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--app-spacing);
|
gap: var(--app-spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-page-header > div > * {
|
.app-page-header > div > * {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-page-header button,
|
.app-page-header button,
|
||||||
.app-page-header input {
|
.app-page-header input {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-page-header h1 {
|
.app-page-header h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.app-field > span {
|
||||||
|
|
||||||
label.app-field > span {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
label.app-field > small {
|
label.app-field > small {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-editor-actions {
|
||||||
|
margin-top: var(--app-spacing);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--app-spacing);
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.page-editor-actions {
|
.page-editor[data-edit-mode="view"] .page-editor-actions {
|
||||||
margin-top: var(--app-spacing);
|
display: none;
|
||||||
display: flex;
|
}
|
||||||
gap: var(--app-spacing);
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-editor[data-edit-mode="view"] .page-editor-actions {
|
.page-editor[data-edit-mode="view"] .ce-toolbar,
|
||||||
display: none;
|
.page-editor[data-edit-mode="view"] .ce-inline-toolbar,
|
||||||
}
|
.page-editor[data-edit-mode="view"] .ce-popover {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.page-editor[data-edit-mode="view"] .ce-toolbar,
|
.ce-block__content,
|
||||||
.page-editor[data-edit-mode="view"] .ce-inline-toolbar,
|
.ce-toolbar__content {
|
||||||
.page-editor[data-edit-mode="view"] .ce-popover {
|
max-width: 90% !important;
|
||||||
display: none !important;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.ce-block__content, .ce-toolbar__content {
|
.ce-editorjsColumns_wrapper .ce-toolbar__content,
|
||||||
max-width: 90%!important;
|
.ce-editorjsColumns_wrapper .ce-block__content {
|
||||||
}
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ce-editorjsColumns_wrapper .ce-toolbar__content,
|
.ce-popover-item[data-item-name="columns"] svg {
|
||||||
.ce-editorjsColumns_wrapper .ce-block__content {
|
|
||||||
max-width: 100% ! Important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ce-popover-item[data-item-name="columns"] svg {
|
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-details-aside-section button {
|
.app-details-aside-section button {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 968px) {
|
||||||
|
.app-page-action-card {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--app-spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user