Add custom error and not found pages, and implement SAML authentication configuration
- Created a new error page (error.twig) with a user-friendly design for displaying error messages. - Created a new not found page (not-found.twig) to handle 404 errors with appropriate messaging and actions. - Added a SAML authentication configuration file (settings-custom.php) to support group-based admin assignment and user provisioning.
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
defined('XIBO') or die("Sorry, you are not allowed to directly access this page.<br /> Please press the back button in your browser.");
|
defined('XIBO') or die("Sorry, you are not allowed to directly access this page.<br /> Please press the back button in your browser.");
|
||||||
|
|
||||||
$config = array(
|
$config = array(
|
||||||
'theme_name' => 'otssignange',
|
'theme_name' => 'otssigns',
|
||||||
'theme_title' => 'OTS Signs',
|
'theme_title' => 'OTS Signs',
|
||||||
'app_name' => 'OTS Signage',
|
'app_name' => 'OTS Signage',
|
||||||
'theme_url' => 'CMS Homepage',
|
'theme_url' => 'CMS Homepage',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
9217
ots-signs/css/override.css.bak
Normal file
9217
ots-signs/css/override.css.bak
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,17 @@
|
|||||||
{% extends "base.twig" %}
|
{% extends "base.twig" %}
|
||||||
|
|
||||||
{% block headContent %}
|
{% block headContent %}
|
||||||
|
{% if not currentUser.isSuperAdmin() and not currentUser.isGroupAdmin() %}
|
||||||
|
<script nonce="{{ cspNonce }}">
|
||||||
|
(function() {
|
||||||
|
var path = window.location.pathname;
|
||||||
|
if (path.indexOf('/layout/designer/') !== -1) return;
|
||||||
|
var cmsIdx = path.toLowerCase().indexOf('/cms');
|
||||||
|
var portalUrl = window.location.origin + (cmsIdx > 0 ? path.substring(0, cmsIdx) : '') + '/';
|
||||||
|
window.location.replace(portalUrl);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
<script nonce="{{ cspNonce }}">
|
<script nonce="{{ cspNonce }}">
|
||||||
(function(){
|
(function(){
|
||||||
try{
|
try{
|
||||||
|
|||||||
192
ots-signs/views/error.twig
Normal file
192
ots-signs/views/error.twig
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{% trans "Error" %} | {{ theme.getThemeConfig("theme_title") }}</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="public-path" content="{{ theme.rootUri() }}"/>
|
||||||
|
<link rel="shortcut icon" href="{{ theme.uri("img/favicon.ico") }}" />
|
||||||
|
<script src="{{ theme.rootUri() }}dist/style.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||||
|
<link href="{{ theme.uri("css/override.css") }}?{{ version }}" rel="stylesheet" media="screen">
|
||||||
|
<link href="{{ theme.uri("css/override-dark.css") }}?{{ version }}" rel="stylesheet" media="screen">
|
||||||
|
<style type="text/css" nonce="{{ cspNonce }}">
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.error-page {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
max-width: 560px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.error-logo {
|
||||||
|
height: 48px;
|
||||||
|
width: auto;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.error-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(239, 68, 68, 0.15);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.error-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
.error-detail {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #fca5a5;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.error-fallback {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0 0 28px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.error-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn-home {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
background-color: #e87800;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.btn-home:hover {
|
||||||
|
background-color: #c46500;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.btn-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #e2e8f0;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.btn-back:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.redirect-notice {
|
||||||
|
margin-top: 28px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.redirect-notice span {
|
||||||
|
color: #e87800;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.error-divider {
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
background: #ef4444;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 16px auto 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="error-page" role="main">
|
||||||
|
<a href="{{ homeUrl }}">
|
||||||
|
<img class="error-logo" src="{{ theme.uri("img/xibologo.png") }}" alt="OTS Signs">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="error-icon" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="error-title">{% trans "Something Went Wrong" %}</h1>
|
||||||
|
<div class="error-divider" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
{% if message is defined and message != "" %}
|
||||||
|
<div class="error-detail" role="alert">{{ message }}</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="error-fallback">
|
||||||
|
{% trans "An unexpected error occurred. Please try again or contact support if the problem persists." %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="error-actions">
|
||||||
|
<a class="btn-home" href="{{ homeUrl }}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L8.354 1.146z"/></svg>
|
||||||
|
{% trans "Go to Dashboard" %}
|
||||||
|
</a>
|
||||||
|
<button class="btn-back" onclick="history.back()" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/></svg>
|
||||||
|
{% trans "Go Back" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="redirect-notice" id="redirect-notice" aria-live="polite">
|
||||||
|
{% trans "Redirecting to dashboard in" %} <span id="countdown">15</span> {% trans "seconds" %}…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ theme.rootUri() }}dist/vendor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||||
|
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||||
|
(function () {
|
||||||
|
var homeUrl = {{ homeUrl | json_encode | raw }};
|
||||||
|
var seconds = 15;
|
||||||
|
var el = document.getElementById('countdown');
|
||||||
|
var interval = setInterval(function () {
|
||||||
|
seconds -= 1;
|
||||||
|
if (el) el.textContent = seconds;
|
||||||
|
if (seconds <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
window.location.replace(homeUrl);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
{#
|
{#
|
||||||
/**
|
/**
|
||||||
* OTS Signs — Layout Designer Page (with embed mode support)
|
* OTS Signs — Layout Designer Page
|
||||||
*
|
*
|
||||||
* Overrides the Xibo core layout-designer-page.twig to add an embeddable
|
* Overrides the Xibo core layout-designer-page.twig to add two tailored
|
||||||
* layout editor mode for use inside an iframe in external applications.
|
* editor experiences alongside the standard full-page mode.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* Normal: /layout/designer/{layoutId}
|
* Normal: /layout/designer/{layoutId}
|
||||||
|
* Deep-link: /layout/designer/{layoutId}?deeplink=1[&returnUrl=/app/layouts/123]
|
||||||
* Embed: /layout/designer/{layoutId}?embed=1
|
* Embed: /layout/designer/{layoutId}?embed=1
|
||||||
*
|
*
|
||||||
* In embed mode:
|
* Deep-link mode (?deeplink=1):
|
||||||
* - All CMS navigation is hidden (sidebar, topbar, help pane)
|
* - Designed for direct navigation from an external React app.
|
||||||
* - Back/Exit buttons in the editor toolbar are hidden
|
* - All Xibo chrome is stripped (sidebar, topbar, help pane, back buttons).
|
||||||
|
* - A minimal OTS branded back bar is rendered at the top of the viewport.
|
||||||
|
* - Clicking "Back" navigates to ?returnUrl= (relative paths only) or
|
||||||
|
* falls back to window.history.back() if the param is absent/invalid.
|
||||||
|
* - The editor options dropdown (Publish, Checkout, Discard…) stays visible.
|
||||||
|
*
|
||||||
|
* Embed mode (?embed=1):
|
||||||
|
* - All CMS navigation is hidden (sidebar, topbar, help pane).
|
||||||
|
* - Back/Exit buttons in the editor toolbar are hidden.
|
||||||
* - postMessage events are sent to the parent window:
|
* - postMessage events are sent to the parent window:
|
||||||
* xibo:editor:ready, xibo:editor:save, xibo:editor:publish, xibo:editor:exit
|
* xibo:editor:ready, xibo:editor:save, xibo:editor:publish, xibo:editor:exit
|
||||||
* - Parent can send: xibo:editor:requestSave
|
* - Parent can send: xibo:editor:requestSave
|
||||||
@@ -33,7 +42,7 @@
|
|||||||
{% block headContent %}
|
{% block headContent %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
|
|
||||||
{# Embed mode: early body class to prevent FOUC #}
|
{# Early mode detection — prevents FOUC for both embed and deep-link modes #}
|
||||||
<script nonce="{{ cspNonce }}">
|
<script nonce="{{ cspNonce }}">
|
||||||
(function(){
|
(function(){
|
||||||
try {
|
try {
|
||||||
@@ -42,6 +51,9 @@
|
|||||||
if (isEmbed) {
|
if (isEmbed) {
|
||||||
document.documentElement.classList.add('ots-embed-mode');
|
document.documentElement.classList.add('ots-embed-mode');
|
||||||
}
|
}
|
||||||
|
if (params.get('deeplink') === '1') {
|
||||||
|
document.documentElement.classList.add('ots-deeplink-mode');
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// Cross-origin iframe detection may throw — treat as embed
|
// Cross-origin iframe detection may throw — treat as embed
|
||||||
document.documentElement.classList.add('ots-embed-mode');
|
document.documentElement.classList.add('ots-embed-mode');
|
||||||
@@ -101,11 +113,130 @@
|
|||||||
.ots-embed-mode .editor-top-bar #layoutJumpListContainer {
|
.ots-embed-mode .editor-top-bar #layoutJumpListContainer {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Deep-link mode styles ──────────────────────────────── */
|
||||||
|
|
||||||
|
/* Hide Back/Exit button area */
|
||||||
|
.ots-deeplink-mode .back-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the help pane */
|
||||||
|
.ots-deeplink-mode #help-pane {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide floating page actions (notification bell, user menu) */
|
||||||
|
.ots-deeplink-mode .ots-page-actions {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove content wrapper padding/margins for full-bleed editor */
|
||||||
|
.ots-deeplink-mode #content-wrapper {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ots-deeplink-mode .page-content {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ots-deeplink-mode .page-content > .row {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ots-deeplink-mode .page-content > .row > .col-sm-12 {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full viewport height for the editor */
|
||||||
|
.ots-deeplink-mode body,
|
||||||
|
.ots-deeplink-mode #layout-editor {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Push body content down to clear the fixed OTS back bar (44px) */
|
||||||
|
.ots-deeplink-mode body {
|
||||||
|
padding-top: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── OTS branded back bar ──────────────────────────────── */
|
||||||
|
|
||||||
|
/* Hidden by default; revealed only in deep-link mode */
|
||||||
|
#ots-deeplink-backbar {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 44px;
|
||||||
|
background: #0f172a;
|
||||||
|
border-bottom: 2px solid #e87800;
|
||||||
|
z-index: 1300;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ots-deeplink-mode #ots-deeplink-backbar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ots-deeplink-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #e87800;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ots-deeplink-back:hover {
|
||||||
|
background: rgba(232, 120, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ots-deeplink-back:focus-visible {
|
||||||
|
outline: 2px solid #e87800;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ots-deeplink-back svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ots-deeplink-wordmark {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pageContent %}
|
{% block pageContent %}
|
||||||
|
|
||||||
|
{# Deep-link mode: branded back bar (position:fixed — shown only when ots-deeplink-mode is active) #}
|
||||||
|
<div id="ots-deeplink-backbar" role="navigation" aria-label="{{ "Back navigation"|trans }}">
|
||||||
|
<button id="ots-deeplink-back" type="button">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
{{ "Back"|trans }}
|
||||||
|
</button>
|
||||||
|
<span class="ots-deeplink-wordmark">OTS Signs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Editor structure -->
|
<!-- Editor structure -->
|
||||||
<div id="layout-editor" data-published-layout-id="{{ publishedLayoutId }}" data-layout-id="{{ layout.layoutId }}" data-layout-help={{ help }}></div>
|
<div id="layout-editor" data-published-layout-id="{{ publishedLayoutId }}" data-layout-id="{{ layout.layoutId }}" data-layout-help={{ help }}></div>
|
||||||
|
|
||||||
@@ -697,4 +828,45 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{# ── Deep-link mode: returnUrl back navigation ────────────── #}
|
||||||
|
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.get('deeplink') !== '1') return;
|
||||||
|
|
||||||
|
// Mirror the html-level class onto body (needed for body-targeted CSS rules).
|
||||||
|
document.body.classList.add('ots-deeplink-mode');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow only safe same-origin relative paths.
|
||||||
|
* Accepts: /app/layouts, /layouts/123?foo=bar
|
||||||
|
* Rejects: //evil.com, https://evil.com, javascript:void(0), empty string
|
||||||
|
*/
|
||||||
|
function validateReturnUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return false;
|
||||||
|
// Must start with '/' but not '//' (protocol-relative URL)
|
||||||
|
if (url.charAt(0) !== '/' || url.charAt(1) === '/') return false;
|
||||||
|
// Reject anything containing a scheme (e.g. https://)
|
||||||
|
if (url.indexOf('://') !== -1) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawReturn = params.get('returnUrl');
|
||||||
|
var safeReturn = validateReturnUrl(rawReturn) ? rawReturn : null;
|
||||||
|
|
||||||
|
var backBtn = document.getElementById('ots-deeplink-back');
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.addEventListener('click', function() {
|
||||||
|
if (safeReturn) {
|
||||||
|
window.location.href = safeReturn;
|
||||||
|
} else {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<script nonce="{{ cspNonce }}">
|
||||||
|
(function() {
|
||||||
|
if (window.location.search.indexOf('/layout/designer/') !== -1) return;
|
||||||
|
var path = window.location.pathname;
|
||||||
|
var cmsIdx = path.toLowerCase().indexOf('/cms');
|
||||||
|
var portalUrl = window.location.origin + (cmsIdx > 0 ? path.substring(0, cmsIdx) : '') + '/';
|
||||||
|
window.location.replace(portalUrl);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<title>{{ theme.getThemeConfig("theme_title") }}</title>
|
<title>{{ theme.getThemeConfig("theme_title") }}</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
|||||||
167
ots-signs/views/not-found.twig
Normal file
167
ots-signs/views/not-found.twig
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{% trans "Page Not Found" %} | {{ theme.getThemeConfig("theme_title") }}</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="public-path" content="{{ theme.rootUri() }}"/>
|
||||||
|
<link rel="shortcut icon" href="{{ theme.uri("img/favicon.ico") }}" />
|
||||||
|
<script src="{{ theme.rootUri() }}dist/style.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||||
|
<link href="{{ theme.uri("css/override.css") }}?{{ version }}" rel="stylesheet" media="screen">
|
||||||
|
<link href="{{ theme.uri("css/override-dark.css") }}?{{ version }}" rel="stylesheet" media="screen">
|
||||||
|
<style type="text/css" nonce="{{ cspNonce }}">
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.error-page {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
max-width: 520px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.error-logo {
|
||||||
|
height: 48px;
|
||||||
|
width: auto;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.error-code {
|
||||||
|
font-size: 6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
color: #e87800;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.error-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0 0 32px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.error-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn-home {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
background-color: #e87800;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.btn-home:hover {
|
||||||
|
background-color: #c46500;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.btn-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #e2e8f0;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.btn-back:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.redirect-notice {
|
||||||
|
margin-top: 28px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.redirect-notice span {
|
||||||
|
color: #e87800;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.error-divider {
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
background: #e87800;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 16px auto 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="error-page" role="main">
|
||||||
|
<a href="{{ homeUrl }}">
|
||||||
|
<img class="error-logo" src="{{ theme.uri("img/xibologo.png") }}" alt="OTS Signs">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="error-code" aria-label="Error 404">404</p>
|
||||||
|
<div class="error-divider" aria-hidden="true"></div>
|
||||||
|
<h1 class="error-title">{% trans "Page Not Found" %}</h1>
|
||||||
|
<p class="error-message">
|
||||||
|
{% trans "Sorry, we couldn't find the page you were looking for." %}
|
||||||
|
{% trans "It may have been moved, renamed, or it may not exist." %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="error-actions">
|
||||||
|
<a class="btn-home" href="{{ homeUrl }}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L8.354 1.146z"/></svg>
|
||||||
|
{% trans "Go to Dashboard" %}
|
||||||
|
</a>
|
||||||
|
<button class="btn-back" onclick="history.back()" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/></svg>
|
||||||
|
{% trans "Go Back" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="redirect-notice" id="redirect-notice" aria-live="polite">
|
||||||
|
{% trans "Redirecting to dashboard in" %} <span id="countdown">10</span> {% trans "seconds" %}…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ theme.rootUri() }}dist/vendor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||||
|
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||||
|
(function () {
|
||||||
|
var homeUrl = {{ homeUrl | json_encode | raw }};
|
||||||
|
var seconds = 10;
|
||||||
|
var el = document.getElementById('countdown');
|
||||||
|
var interval = setInterval(function () {
|
||||||
|
seconds -= 1;
|
||||||
|
if (el) el.textContent = seconds;
|
||||||
|
if (seconds <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
window.location.replace(homeUrl);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
93
settings-custom.php
Normal file
93
settings-custom.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SAML Authentication Configuration with Group-Based Admin Assignment
|
||||||
|
*
|
||||||
|
* Group-Based Admin Assignment:
|
||||||
|
* ────────────────────────────────────────────────────────────────────────
|
||||||
|
* To make members of specific Authentik groups admins in Xibo:
|
||||||
|
*
|
||||||
|
* 1. In Authentik, create a custom property mapping for your SAML provider:
|
||||||
|
* - Name: saml-usertypeid
|
||||||
|
* - Expression: Return "1" if user in admin group, else return empty string
|
||||||
|
* - Example: return "1" if user.groups.all() | selectattr("name", "equalto", "OTS IT") else ""
|
||||||
|
*
|
||||||
|
* 2. Attach this mapping to the SAML provider via the API or UI
|
||||||
|
*
|
||||||
|
* 3. The usertypeid mapping below will read this attribute from the SAML response
|
||||||
|
*
|
||||||
|
* 4. On JIT provisioning, Xibo will assign users with usertypeid=1 as super-admins
|
||||||
|
*
|
||||||
|
* Excluded Groups:
|
||||||
|
* ────────────────────────────────────────────────────────────────────────
|
||||||
|
* Groups listed in {{EXCLUDED_GROUPS}} are not synced to Xibo during provisioning.
|
||||||
|
* However, users in excluded groups can still log in via SSO (they'll use the
|
||||||
|
* default 'Users' group). Use this to prevent internal admin groups from appearing
|
||||||
|
* as Xibo user groups.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$authentication = new \Xibo\Middleware\SAMLAuthentication();
|
||||||
|
$samlSettings = [
|
||||||
|
'workflow' => [
|
||||||
|
'jit' => true,
|
||||||
|
'field_to_identify' => 'UserName',
|
||||||
|
'libraryQuota' => 1000,
|
||||||
|
'homePage' => 'icondashboard.view',
|
||||||
|
'slo' => true,
|
||||||
|
'mapping' => [
|
||||||
|
'UserID' => '',
|
||||||
|
// usertypeid: Set to 1 (super-admin) for members of admin groups.
|
||||||
|
// Requires a custom SAML property mapping in Authentik (see notes above).
|
||||||
|
'usertypeid' => 'usertypeid',
|
||||||
|
'UserName' => 'http://schemas.goauthentik.io/2021/02/saml/username',
|
||||||
|
'email' => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
||||||
|
],
|
||||||
|
'group' => 'Users',
|
||||||
|
'matchGroups' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'attribute' => 'http://schemas.xmlsoap.org/claims/Group',
|
||||||
|
'extractionRegEx' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'strict' => true,
|
||||||
|
'debug' => true,
|
||||||
|
'baseurl' => 'https://app.ots-signs.com/{CUSTOMER_SLUG}/cms/saml',
|
||||||
|
'idp' => [
|
||||||
|
'entityId' => 'signs-otsdemo-cms',
|
||||||
|
'singleSignOnService' => [
|
||||||
|
'url' => 'https://app.ots-signs.com/auth/application/saml/{AUTHENTIK_SLUG}/sso/binding/redirect/',
|
||||||
|
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||||
|
],
|
||||||
|
'singleLogoutService' => [
|
||||||
|
'url' => 'https://app.ots-signs.com/auth/application/saml/{AUTHENTIK_SLUG}/slo/binding/redirect/',
|
||||||
|
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||||
|
],
|
||||||
|
'x509cert' => '',
|
||||||
|
],
|
||||||
|
'sp' => [
|
||||||
|
'entityId' => 'https://app.ots-signs.com/{CUSTOMER_SLUG}/cms/saml/metadata',
|
||||||
|
'assertionConsumerService' => [
|
||||||
|
'url' => 'https://app.ots-signs.com/{CUSTOMER_SLUG}/cms/saml/acs',
|
||||||
|
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
||||||
|
],
|
||||||
|
'singleLogoutService' => [
|
||||||
|
'url' => 'https://app.ots-signs.com/{CUSTOMER_SLUG}/cms/saml/sls',
|
||||||
|
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||||
|
],
|
||||||
|
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||||
|
'x509cert' => '',
|
||||||
|
'privateKey' => '',
|
||||||
|
],
|
||||||
|
'security' => [
|
||||||
|
'nameIdEncrypted' => false,
|
||||||
|
'authnRequestsSigned' => false,
|
||||||
|
'logoutRequestSigned' => false,
|
||||||
|
'logoutResponseSigned' => false,
|
||||||
|
'signMetadata' => false,
|
||||||
|
'wantMessagesSigned' => false,
|
||||||
|
'wantAssertionsSigned' => false,
|
||||||
|
'wantAssertionsEncrypted' => false,
|
||||||
|
'wantNameIdEncrypted' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// {{ EXCLUDED_GROUPS_COMMENT: Groups to exclude from Xibo sync: OTS IT }}
|
||||||
Reference in New Issue
Block a user