mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-30 15:28:12 +02:00
Refactor and cleanup OIDC, desktop, and web-app roles
- Improved OIDC variable definitions (12_oidc.yml) - Added account/security/profile URLs - Restructured web-app-desktop tasks and JS handling - Introduced oidc.js and iframe.js with runtime loader - Fixed nginx.conf, LDAP, and healthcheck templates spacing - Improved Lua injection for CSP and snippets - Fixed typos (WordPress, receive, etc.) - Added silent-check-sso nginx location Conversation: https://chatgpt.com/share/68ae0060-4fac-800f-9f02-22592a4087d3
This commit is contained in:
@@ -2,9 +2,10 @@ features:
|
||||
matomo: true
|
||||
css: true
|
||||
desktop: false
|
||||
simpleicons: true # Activate Brand Icons for your groups
|
||||
javascript: true # Necessary for URL sync
|
||||
logout: false # Doesn't have own user data. Just a frame.
|
||||
oidc: true # Needs to be activated so that the login url is working
|
||||
simpleicons: true # Activate Brand Icons for your groups
|
||||
javascript: true # Necessary for URL sync
|
||||
logout: true
|
||||
server:
|
||||
csp:
|
||||
whitelist:
|
||||
@@ -19,6 +20,7 @@ server:
|
||||
- https://cdn.jsdelivr.net
|
||||
connect-src:
|
||||
- https://ka-f.fontawesome.com
|
||||
- "{{ WEB_PROTOCOL }}://auth.{{ PRIMARY_DOMAIN }}"
|
||||
frame-src:
|
||||
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
||||
flags:
|
||||
@@ -31,4 +33,8 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- "{{ PRIMARY_DOMAIN }}"
|
||||
|
||||
docker:
|
||||
services:
|
||||
desktop:
|
||||
name: "desktop"
|
||||
image: "application-portfolio"
|
@@ -2,13 +2,16 @@
|
||||
include_tasks: "02_validate.yml"
|
||||
when: MODE_ASSERT | bool
|
||||
|
||||
- name: "Include JS routines"
|
||||
include_tasks: "03_javascript.yml"
|
||||
|
||||
- name: "load docker, proxy for '{{ application_id }}'"
|
||||
include_role:
|
||||
name: cmp-docker-proxy
|
||||
|
||||
- name: "Check if host-specific config.yaml exists in {{ config_inventory_path }}"
|
||||
- name: "Check if host-specific config.yaml exists in {{ DESKTOP_INVENTORY_CONFIG_PATH }}"
|
||||
stat:
|
||||
path: "{{ config_inventory_path }}"
|
||||
path: "{{ DESKTOP_INVENTORY_CONFIG_PATH }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: config_file
|
||||
@@ -42,20 +45,20 @@
|
||||
|
||||
- name: Copy host-specific config.yaml if it exists
|
||||
template:
|
||||
src: "{{ config_inventory_path }}"
|
||||
dest: "{{docker_repository_path}}/app/config.yaml"
|
||||
src: "{{ DESKTOP_INVENTORY_CONFIG_PATH }}"
|
||||
dest: "{{ docker_repository_path }}/app/config.yaml"
|
||||
notify: docker compose up
|
||||
when: config_file.stat.exists
|
||||
|
||||
- name: Copy default config.yaml from the role template if host-specific file does not exist
|
||||
template:
|
||||
src: "config.yaml.j2"
|
||||
dest: "{{docker_repository_path}}/app/config.yaml"
|
||||
dest: "{{ docker_repository_path }}/app/config.yaml"
|
||||
notify: docker compose up
|
||||
when: not config_file.stat.exists
|
||||
|
||||
- name: add docker-compose.yml
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{docker_compose.directories.instance}}docker-compose.yml"
|
||||
notify: docker compose up
|
||||
dest: "{{ docker_compose.directories.instance }}docker-compose.yml"
|
||||
notify: docker compose up
|
||||
|
19
roles/web-app-desktop/tasks/03_javascript.yml
Normal file
19
roles/web-app-desktop/tasks/03_javascript.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
- name: "load required 'web-svc-cdn' for {{ application_id }}"
|
||||
include_role:
|
||||
name: web-svc-cdn
|
||||
public: false
|
||||
when: run_once_web_svc_cdn is not defined
|
||||
|
||||
- name: Ensure {{ DESKTOP_JS_SERVER_DIR }} exists
|
||||
file:
|
||||
path: "{{ DESKTOP_JS_SERVER_DIR }}"
|
||||
state: directory
|
||||
owner: "{{ NGINX.USER }}"
|
||||
group: "{{ NGINX.USER }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: "Include file specific JS Routines"
|
||||
include_tasks: "_javascript_file.yml"
|
||||
loop: "{{ DESKTOP_JS_FILES }}"
|
||||
loop_control:
|
||||
loop_var: js_file_name
|
17
roles/web-app-desktop/tasks/_javascript_file.yml
Normal file
17
roles/web-app-desktop/tasks/_javascript_file.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
- name: Deploy {{ js_file_name }}
|
||||
template:
|
||||
src: "javascript/{{ js_file_name }}.j2"
|
||||
dest: "{{ DESKTOP_JS_SERVER_DIR }}/{{ js_file_name }}"
|
||||
owner: "{{ NGINX.USER }}"
|
||||
group: "{{ NGINX.USER }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Get stat for {{ js_file_name }}
|
||||
stat:
|
||||
path: "{{ DESKTOP_JS_SERVER_DIR }}/{{ js_file_name }}"
|
||||
register: javascript_file_stat
|
||||
|
||||
- name: Update javascript_file_version with highest mtime
|
||||
set_fact:
|
||||
javascript_file_version: >-
|
||||
{{ [ (javascript_file_version | default(0) | int), (javascript_file_stat.stat.mtime | int) ] | max }}
|
@@ -2,15 +2,15 @@
|
||||
portfolio:
|
||||
{% set container_port = 5000 %}
|
||||
build:
|
||||
context: {{docker_repository_path}}
|
||||
context: {{ docker_repository_path }}
|
||||
dockerfile: Dockerfile
|
||||
image: application-portfolio
|
||||
container_name: portfolio
|
||||
image: {{ DESKTOP_IMAGE }}
|
||||
container_name: {{ DESKTOP_CONTAINER }}
|
||||
ports:
|
||||
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
|
||||
volumes:
|
||||
- {{docker_repository_path}}app:/app
|
||||
restart: unless-stopped
|
||||
- {{ docker_repository_path }}app:/app
|
||||
restart: {{ DOCKER_RESTART_POLICY }}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/healthcheck/tcp.yml.j2' %}
|
||||
|
||||
|
@@ -1,30 +1,46 @@
|
||||
window.addEventListener("message", function(event) {
|
||||
const allowedSuffix = ".{{ PRIMARY_DOMAIN }}";
|
||||
const origin = event.origin;
|
||||
// ===== Runtime loader for external JS files (no Jinja includes) =====
|
||||
(function () {
|
||||
// 1) Values injected by Ansible/Jinja
|
||||
// Base URL where your files were deployed (e.g. CDN), made safe w/o trailing slash
|
||||
const BASE_URL = ("{{ DESKTOP_JS_BASE_URL }}").replace(/\/+$/, "");
|
||||
// List of files to load, in order
|
||||
const FILES = [
|
||||
{% for f in DESKTOP_JS_FILES -%}
|
||||
"{{ f }}"{% if not loop.last %},{% endif %}
|
||||
{%- endfor %}
|
||||
];
|
||||
// Cache buster (highest mtime computed during deploy)
|
||||
const VERSION = "{{ javascript_file_version }}";
|
||||
|
||||
// 1. Only allow messages from *.{{ PRIMARY_DOMAIN }}
|
||||
if (!origin.endsWith(allowedSuffix)) return;
|
||||
// 2) Helper to load a <script> with proper query param
|
||||
function loadScriptSequential(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = document.createElement("script");
|
||||
// Append ?v=... (or &v=... if there are already params)
|
||||
s.src = url + (url.includes("?") ? "&" : "?") + "v=" + encodeURIComponent(VERSION);
|
||||
// Keep execution order: do not set async/defer
|
||||
s.onload = () => resolve();
|
||||
s.onerror = () => reject(new Error("Failed to load " + url));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
|
||||
// 2. Only process valid iframeLocationChange messages
|
||||
if (data && data.type === "iframeLocationChange" && typeof data.href === "string") {
|
||||
try {
|
||||
const hrefUrl = new URL(data.href);
|
||||
|
||||
// 3. Only allow redirects to *.{{ PRIMARY_DOMAIN }}
|
||||
if (!hrefUrl.hostname.endsWith(allowedSuffix)) return;
|
||||
|
||||
// 4. Update the ?iframe= parameter in the browser URL
|
||||
const newUrl = new URL(window.location);
|
||||
newUrl.searchParams.set("iframe", hrefUrl.href);
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
} catch (e) {
|
||||
// Invalid or malformed URL – ignore
|
||||
// 3) Load all files in order
|
||||
async function loadAll() {
|
||||
for (const name of FILES) {
|
||||
const fullUrl = BASE_URL + "/" + name.replace(/^\/+/, "");
|
||||
await loadScriptSequential(fullUrl);
|
||||
}
|
||||
// Optional: hook after everything is ready
|
||||
if (typeof window.onDesktopJsLoaded === "function") {
|
||||
try { window.onDesktopJsLoaded(); } catch {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
{% if MODE_DEBUG | bool %}
|
||||
console.log("[iframe-sync] Listener for iframe messages is active.");
|
||||
{% endif %}
|
||||
// 4) Start after DOM is ready (safe point to inject <script> tags)
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", loadAll);
|
||||
} else {
|
||||
loadAll();
|
||||
}
|
||||
})();
|
||||
|
30
roles/web-app-desktop/templates/javascript/iframe.js.j2
Normal file
30
roles/web-app-desktop/templates/javascript/iframe.js.j2
Normal file
@@ -0,0 +1,30 @@
|
||||
window.addEventListener("message", function(event) {
|
||||
const allowedSuffix = ".{{ PRIMARY_DOMAIN }}";
|
||||
const origin = event.origin;
|
||||
|
||||
// 1. Only allow messages from *.{{ PRIMARY_DOMAIN }}
|
||||
if (!origin.endsWith(allowedSuffix)) return;
|
||||
|
||||
const data = event.data;
|
||||
|
||||
// 2. Only process valid iframeLocationChange messages
|
||||
if (data && data.type === "iframeLocationChange" && typeof data.href === "string") {
|
||||
try {
|
||||
const hrefUrl = new URL(data.href);
|
||||
|
||||
// 3. Only allow redirects to *.{{ PRIMARY_DOMAIN }}
|
||||
if (!hrefUrl.hostname.endsWith(allowedSuffix)) return;
|
||||
|
||||
// 4. Update the ?iframe= parameter in the browser URL
|
||||
const newUrl = new URL(window.location);
|
||||
newUrl.searchParams.set("iframe", hrefUrl.href);
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
} catch (e) {
|
||||
// Invalid or malformed URL – ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
{% if MODE_DEBUG | bool %}
|
||||
console.log("[iframe-sync] Listener for iframe messages is active.");
|
||||
{% endif %}
|
220
roles/web-app-desktop/templates/javascript/oidc.js.j2
Normal file
220
roles/web-app-desktop/templates/javascript/oidc.js.j2
Normal file
@@ -0,0 +1,220 @@
|
||||
/* ==========================================================================================
|
||||
roles/web-app-desktop/templates/javascript/oidc.js.j2
|
||||
Purpose: Hide "Login" and show "Account" when a Keycloak SSO session exists,
|
||||
wire login/logout clicks to the Keycloak adapter, and keep state fresh.
|
||||
NOTE: Ensure CSP allows {{ OIDC.URL }} for script-src-elem, connect-src, and frame-src.
|
||||
========================================================================================== */
|
||||
|
||||
/* =======================
|
||||
1) Configuration (filled by Jinja/Ansible)
|
||||
======================= */
|
||||
const KC_CONFIG = {
|
||||
url: "{{ OIDC.URL }}", // e.g. https://auth.infinito.nexus
|
||||
realm: "{{ OIDC.CLIENT.REALM }}", // e.g. cymais.cloud
|
||||
clientId: "{{ OIDC.CLIENT.ID }}", // e.g. cymais.cloud
|
||||
redirectUri: window.location.origin, // where to return after login/logout
|
||||
silentCheckSsoRedirectUri: window.location.origin + "{{ DESKTOP_LOCATION_SILENT_CHECK }}"
|
||||
};
|
||||
|
||||
const DEBUG = {{ 'true' if MODE_DEBUG | default(false) else 'false' }};
|
||||
|
||||
/* ==============================================
|
||||
2) Helpers for menu manipulation
|
||||
============================================== */
|
||||
function normalizedLabel(el) {
|
||||
return (el?.getAttribute?.("data-label") || el?.ariaLabel || el?.title || el?.textContent || "")
|
||||
.replace(/\s+/g, " ").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function findMenuItemByText(label) {
|
||||
const wanted = (label || "").trim().toLowerCase();
|
||||
if (!wanted) return null;
|
||||
|
||||
const nodes = document.querySelectorAll(
|
||||
"nav a, nav button, nav .nav-link, nav .dropdown-toggle, nav .dropdown-item, .navbar a, .navbar button"
|
||||
);
|
||||
|
||||
for (const el of nodes) {
|
||||
const text = (el.getAttribute?.("data-label") || el.ariaLabel || el.title || el.textContent || "")
|
||||
.replace(/\s+/g, " ").trim().toLowerCase();
|
||||
if (text !== wanted) continue;
|
||||
|
||||
const container =
|
||||
el.closest("li, .nav-item, .dropdown, .btn-group, .menu-item") ||
|
||||
el.closest(".navbar-nav > *") ||
|
||||
el.parentElement ||
|
||||
el;
|
||||
return container;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function setItemVisible(el, visible) {
|
||||
if (!el) return;
|
||||
el.style.display = visible ? "" : "none";
|
||||
if (el.toggleAttribute) el.toggleAttribute("hidden", !visible);
|
||||
el.setAttribute?.("aria-hidden", visible ? "false" : "true");
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
3) Dynamically load the Keycloak.js adapter
|
||||
============================================== */
|
||||
function loadKeycloakAdapter(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = document.createElement("script");
|
||||
s.src = src;
|
||||
s.onload = resolve;
|
||||
s.onerror = () => reject(new Error("Failed to load keycloak.js from " + src));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
4) UI logic: toggle Account/Login visibility
|
||||
============================================== */
|
||||
let keycloak = null;
|
||||
|
||||
function applyAuthMenuVisibility(authenticated) {
|
||||
try {
|
||||
const accountItem = findMenuItemByText("Account");
|
||||
const loginItem = findMenuItemByText("Login");
|
||||
setItemVisible(accountItem, !!authenticated);
|
||||
setItemVisible(loginItem, !authenticated);
|
||||
if (DEBUG) console.log("[oidc] applyAuthMenuVisibility:", { authenticated, accountItem, loginItem });
|
||||
} catch (e) {
|
||||
console.warn("[oidc] applyAuthMenuVisibility failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function wireLoginLogoutClicks() {
|
||||
const loginItem = findMenuItemByText("Login");
|
||||
const logoutItem = findMenuItemByText("Logout"); // child under "Account"
|
||||
const loginA = loginItem?.querySelector("a,button");
|
||||
const logoutA = logoutItem?.querySelector("a,button");
|
||||
|
||||
// Intercept login click to use the adapter (gracefully falls back to href if adapter failed)
|
||||
loginA?.addEventListener("click", (ev) => {
|
||||
try {
|
||||
if (keycloak) {
|
||||
ev.preventDefault();
|
||||
keycloak.login({ redirectUri: KC_CONFIG.redirectUri });
|
||||
if (DEBUG) console.log("[oidc] login clicked → keycloak.login()");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[oidc] login handler error:", e);
|
||||
}
|
||||
}, { capture: true });
|
||||
|
||||
// Intercept logout click
|
||||
logoutA?.addEventListener("click", (ev) => {
|
||||
try {
|
||||
if (keycloak) {
|
||||
ev.preventDefault();
|
||||
keycloak.logout({ redirectUri: KC_CONFIG.redirectUri });
|
||||
if (DEBUG) console.log("[oidc] logout clicked → keycloak.logout()");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[oidc] logout handler error:", e);
|
||||
}
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
5) Initialize Keycloak with silent SSO check
|
||||
============================================== */
|
||||
async function initAuthUI() {
|
||||
// Default UI state until we know better
|
||||
applyAuthMenuVisibility(false);
|
||||
|
||||
// Load keycloak.js
|
||||
const kcJsUrl = "https://cdn.jsdelivr.net/npm/keycloak-js@latest/dist/keycloak.min.js";
|
||||
try {
|
||||
await loadKeycloakAdapter(kcJsUrl);
|
||||
} catch (e) {
|
||||
console.error("[oidc] Failed to load adapter:", e);
|
||||
return; // nothing else to do
|
||||
}
|
||||
|
||||
if (typeof window.Keycloak !== "function") {
|
||||
console.error("[oidc] window.Keycloak is not available after loading:", kcJsUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the Keycloak instance
|
||||
let authenticated = false;
|
||||
try {
|
||||
keycloak = new Keycloak({ url: KC_CONFIG.url, realm: KC_CONFIG.realm, clientId: KC_CONFIG.clientId });
|
||||
|
||||
const hasAuthCode = /\bcode=/.test(window.location.search);
|
||||
const onLoadMode = hasAuthCode ? "login-required" : "check-sso";
|
||||
|
||||
authenticated = await keycloak.init({
|
||||
onLoad: onLoadMode,
|
||||
pkceMethod: "S256",
|
||||
silentCheckSsoRedirectUri: KC_CONFIG.silentCheckSsoRedirectUri,
|
||||
checkLoginIframe: true,
|
||||
tokenMinValid: 30
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[oidc] Keycloak init failed:", e);
|
||||
}
|
||||
|
||||
applyAuthMenuVisibility(!!authenticated);
|
||||
wireLoginLogoutClicks();
|
||||
|
||||
// Schedule token refresh only if we are authenticated
|
||||
async function scheduleRefresh() {
|
||||
if (!keycloak?.tokenParsed?.exp) return;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const exp = keycloak.tokenParsed.exp;
|
||||
const refreshInMs = Math.max((exp - now - 30), 1) * 1000;
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const ok = await keycloak.updateToken(60); // refresh if <60s valid
|
||||
applyAuthMenuVisibility(!!ok || !!keycloak?.authenticated);
|
||||
if (DEBUG) console.log("[oidc] token refresh → ok:", ok);
|
||||
} catch (e) {
|
||||
console.warn("[oidc] token refresh failed:", e);
|
||||
applyAuthMenuVisibility(false);
|
||||
}
|
||||
scheduleRefresh();
|
||||
}, refreshInMs);
|
||||
}
|
||||
if (authenticated) scheduleRefresh();
|
||||
|
||||
// Re-apply if the navbar is re-rendered dynamically
|
||||
const navbar = document.querySelector("nav.navbar") || document.querySelector(".navbar") || document.querySelector("nav");
|
||||
if (navbar && "MutationObserver" in window) {
|
||||
new MutationObserver(() => applyAuthMenuVisibility(!!keycloak?.authenticated || !!authenticated))
|
||||
.observe(navbar, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("[oidc] init done", {
|
||||
authenticated,
|
||||
kcJsUrl,
|
||||
silentCheckSsoRedirectUri: KC_CONFIG.silentCheckSsoRedirectUri
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
6) Start when DOM is ready
|
||||
============================================== */
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initAuthUI, { once: true });
|
||||
} else {
|
||||
// If script is injected after DOMContentLoaded (e.g., via runtime loader), run immediately
|
||||
initAuthUI();
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
NOTE: You must provide a file {{ DESKTOP_LOCATION_SILENT_CHECK }} at your web root, e.g.:
|
||||
|
||||
<!DOCTYPE html><html><body>Silent SSO</body></html>
|
||||
|
||||
This file is loaded in an invisible iframe by Keycloak to check login state.
|
||||
Also ensure CSP allows {{ OIDC.URL }} in script-src-elem, connect-src, and frame-src.
|
||||
========================================================================================== */
|
@@ -24,31 +24,7 @@ applications:
|
||||
icon: {{ app.icon }}
|
||||
url: {{ app.url }}
|
||||
iframe: {{ app.iframe }}
|
||||
{% if app.title == 'Keycloak' %}
|
||||
{% set keycloak_url = domains | get_url('web-app-keycloak', WEB_PROTOCOL) %}
|
||||
{{ domains | get_url(application_id, WEB_PROTOCOL) }}
|
||||
children:
|
||||
- name: Administration
|
||||
description: Access the central admin console
|
||||
icon:
|
||||
class: fa-solid fa-shield-halved
|
||||
url: {{ keycloak_url }}/admin
|
||||
iframe: {{ applications | get_app_conf( 'web-app-keycloak', 'features.desktop', False) }}
|
||||
- name: Profile
|
||||
description: Update your personal admin settings
|
||||
icon:
|
||||
class: fa-solid fa-user-gear
|
||||
url: {{ keycloak_url }}/realms/{{ OIDC.CLIENT.ID }}/account
|
||||
iframe: {{ applications | get_app_conf( 'web-app-keycloak', 'features.desktop', False) }}
|
||||
- name: Logout
|
||||
description: End your admin session securely
|
||||
icon:
|
||||
class: fa-solid fa-right-from-bracket
|
||||
url: {{ keycloak_url }}/realms/{{ OIDC.CLIENT.ID }}/protocol/openid-connect/logout
|
||||
iframe: false
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
followus:
|
||||
name: Follow Us
|
||||
description: Follow us to stay up to recieve the newest {{ SOFTWARE_NAME }} updates
|
||||
description: Follow us to stay up to receive the newest {{ SOFTWARE_NAME }} updates
|
||||
icon:
|
||||
class: fas fa-newspaper
|
||||
{% if ["web-app-mastodon", "web-app-bluesky"] | any_in(group_names) %}
|
||||
@@ -43,7 +43,7 @@ followus:
|
||||
iframe: {{ applications | get_app_conf('web-app-peertube','features.desktop',True) }}
|
||||
{% endif %}
|
||||
{% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %}
|
||||
- name: Wordpress
|
||||
- name: WordPress
|
||||
description: Read {{ 'our' if service_provider.type == 'legal' else 'my' }} articles and stories.
|
||||
icon:
|
||||
class: fa-solid fa-blog
|
||||
@@ -55,7 +55,7 @@ followus:
|
||||
- name: Friendica
|
||||
description: Visit {{ 'our' if service_provider.type == 'legal' else 'my' }} friendica profile
|
||||
icon:
|
||||
class: fas fa-net-wired
|
||||
class: fa-solid fa-network-wired
|
||||
identifier: "{{service_provider.contact.friendica}}"
|
||||
url: "{{ WEB_PROTOCOL }}://{{ service_provider.contact.friendica.split('@')[2] }}/@{{ service_provider.contact.friendica.split('@')[1] }}"
|
||||
iframe: {{ applications | get_app_conf('web-app-friendica','features.desktop',True) }}
|
||||
|
@@ -9,8 +9,8 @@
|
||||
description: Access our comprehensive documentation and support resources to help you get the most out of the software.
|
||||
icon:
|
||||
class: fas fa-book
|
||||
url: https://{{domains | get_domain('web-app-sphinx')}}
|
||||
iframe: {{ applications | get_app_conf('web-app-sphinx','features.desktop',True) }}
|
||||
url: {{ domains | get_url('web-app-sphinx', WEB_PROTOCOL) }}
|
||||
iframe: {{ applications | get_app_conf('web-app-sphinx','features.desktop') }}
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
description: Checkout the presentation
|
||||
icon:
|
||||
class: "fas fa-chalkboard-teacher"
|
||||
url: https://{{domains | get_domain('web-app-navigator')}}
|
||||
iframe: {{ applications | get_app_conf('web-app-navigator','features.desktop',True) }}
|
||||
url: {{ domains | get_url('web-app-navigator', WEB_PROTOCOL) }}
|
||||
iframe: {{ applications | get_app_conf('web-app-navigator','features.desktop') }}
|
||||
|
||||
{% endif %}
|
||||
- name: Solutions
|
||||
|
@@ -17,4 +17,39 @@
|
||||
description: Reload the application
|
||||
icon:
|
||||
class: fa-solid fa-rotate-right
|
||||
url: "{{ WEB_PROTOCOL }}://{{ domains | get_domain('web-app-desktop') }}"
|
||||
url: "{{ domains | get_url('web-app-desktop', WEB_PROTOCOL) }}"
|
||||
|
||||
{% if DESKTOP_OIDC_ENABLED | bool %}
|
||||
|
||||
- name: Account
|
||||
description: Manage your Account
|
||||
icon:
|
||||
class: fa-solid fa-user
|
||||
children:
|
||||
- name: Profile
|
||||
description: Manage your profile
|
||||
icon:
|
||||
class: fa-solid fa-id-card
|
||||
url: {{ OIDC.CLIENT.ACCOUNT.PROFILE_URL }}
|
||||
iframe: {{ DESKTOP_KEYCLOAK_IFRAME_ENABLED }}
|
||||
- name: Security
|
||||
description: Manage your security settings
|
||||
icon:
|
||||
class: fa-solid fa-user-gear
|
||||
url: {{ OIDC.CLIENT.ACCOUNT.SECURITY_URL }}
|
||||
iframe: {{ DESKTOP_KEYCLOAK_IFRAME_ENABLED }}
|
||||
- name: Logout
|
||||
description: "Logout from {{ SOFTWARE_NAME }} on {{ PRIMARY_DOMAIN }}"
|
||||
target: "_top"
|
||||
icon:
|
||||
class: fa-solid fa-right-from-bracket
|
||||
url: {{ OIDC.CLIENT.LOGOUT_URL }}
|
||||
iframe: false # Neccesary to refresh desktop page after logout
|
||||
- name: Login
|
||||
description: "Login to {{ SOFTWARE_NAME }} on {{ PRIMARY_DOMAIN }}"
|
||||
target: "_top"
|
||||
icon:
|
||||
class: fa-solid fa-right-to-bracket
|
||||
url: {{ DESKTOP_KEYCLOAK_LOGIN_URL }}
|
||||
iframe: false # Neccesary to refresh desktop page after login
|
||||
{% endif %}
|
||||
|
16
roles/web-app-desktop/templates/nginx/sso.html.conf.j2
Normal file
16
roles/web-app-desktop/templates/nginx/sso.html.conf.j2
Normal file
@@ -0,0 +1,16 @@
|
||||
# Serve a static silent-check-sso.html file directly from memory
|
||||
location = {{ DESKTOP_LOCATION_SILENT_CHECK }} {
|
||||
default_type text/html;
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header Cache-Control "no-store";
|
||||
|
||||
return 200 '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Silent SSO</title>
|
||||
</head>
|
||||
<body>
|
||||
Checking SSO...
|
||||
</body>
|
||||
</html>';
|
||||
}
|
@@ -1,4 +1,39 @@
|
||||
application_id: "web-app-desktop"
|
||||
docker_repository_address: "https://github.com/kevinveenbirkenbach/port-ui"
|
||||
config_inventory_path: "{{ inventory_dir }}/files/{{ inventory_hostname }}/docker/web-app-desktop/config.yaml.j2"
|
||||
docker_pull_git_repository: true
|
||||
# General
|
||||
application_id: "web-app-desktop"
|
||||
|
||||
## Webserver
|
||||
proxy_extra_configuration: "{{ lookup('template', 'nginx/sso.html.conf.j2') }}"
|
||||
|
||||
## Docker
|
||||
docker_repository_address: "https://github.com/kevinveenbirkenbach/port-ui"
|
||||
docker_pull_git_repository: true
|
||||
|
||||
# Desktop
|
||||
|
||||
## Javascript
|
||||
DESKTOP_JS_CDN_URL: "{{ domains | get_url('web-svc-cdn', WEB_PROTOCOL) }}"
|
||||
DESKTOP_JS_FILES: ['iframe.js','oidc.js']
|
||||
DESKTOP_JS_BASE_PATH: "{{ application_id | get_entity_name }}/js"
|
||||
DESKTOP_JS_SERVER_DIR: "{{ [ NGINX.DIRECTORIES.DATA.CDN, DESKTOP_JS_BASE_PATH ] | path_join }}"
|
||||
DESKTOP_JS_BASE_URL: "{{ (DESKTOP_JS_CDN_URL | trim('/')) ~ '/' ~ (DESKTOP_JS_BASE_PATH | trim('/')) }}"
|
||||
|
||||
## Webserver
|
||||
DESKTOP_LOCATION_SILENT_CHECK: "/silent-check-sso.html"
|
||||
|
||||
|
||||
## Configuration
|
||||
DESKTOP_INVENTORY_CONFIG_PATH: "{{ inventory_dir }}/files/{{ inventory_hostname }}/docker/web-app-desktop/config.yaml.j2"
|
||||
|
||||
## OIDC
|
||||
DESKTOP_KEYCLOAK_LOGIN_URL: >-
|
||||
{{ OIDC.CLIENT.AUTHORIZE_URL
|
||||
~ '?client_id=' ~ OIDC.CLIENT.ID
|
||||
~ '&response_type=code'
|
||||
~ '&scope=openid%20profile%20email'
|
||||
~ '&redirect_uri=' ~ (domains | get_url(application_id, WEB_PROTOCOL)) | urlencode }}
|
||||
DESKTOP_KEYCLOAK_IFRAME_ENABLED: "{{ applications | get_app_conf( 'web-app-keycloak', 'features.desktop') }}"
|
||||
DESKTOP_OIDC_ENABLED: "{{ applications | get_app_conf( application_id, 'features.oidc') }}"
|
||||
|
||||
## Docker
|
||||
DESKTOP_CONTAINER: "{{ applications | get_app_conf( application_id, 'docker.services.desktop.name') }}"
|
||||
DESKTOP_IMAGE: "{{ applications | get_app_conf( application_id, 'docker.services.desktop.image') }}"
|
Reference in New Issue
Block a user