mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-27 05:55:15 +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:
parent
ce033c370a
commit
c182ecf516
@ -7,33 +7,40 @@
|
||||
#############################################
|
||||
# @see https://en.wikipedia.org/wiki/OpenID_Connect
|
||||
|
||||
## Helper Variables:
|
||||
_oidc_client_realm: "{{ OIDC.CLIENT.REALM if OIDC.CLIENT is defined and OIDC.CLIENT.REALM is defined else SOFTWARE_NAME | lower }}"
|
||||
_oidc_url: "{{
|
||||
( OIDC.URL
|
||||
if (OIDC is defined and OIDC.URL is defined)
|
||||
else WEB_PROTOCOL ~ '://' ~ (domains | get_domain('web-app-keycloak'))
|
||||
).rstrip('/')
|
||||
}}"
|
||||
_oidc_client_issuer_url: "{{ _oidc_url ~ '/realms/' ~ _oidc_client_realm }}"
|
||||
_oidc_client_id: "{{ OIDC.CLIENT.ID if OIDC.CLIENT is defined and OIDC.CLIENT.ID is defined else SOFTWARE_NAME | lower }}"
|
||||
# Helper Variables:
|
||||
_oidc_client_realm: "{{ OIDC.CLIENT.REALM if OIDC.CLIENT is defined and OIDC.CLIENT.REALM is defined else SOFTWARE_NAME | lower }}"
|
||||
_oidc_url: "{{
|
||||
( OIDC.URL
|
||||
if (OIDC is defined and OIDC.URL is defined)
|
||||
else domains | get_url('web-app-keycloak', WEB_PROTOCOL)
|
||||
).rstrip('/')
|
||||
}}"
|
||||
_oidc_client_issuer_url: "{{ _oidc_url ~ '/realms/' ~ _oidc_client_realm }}"
|
||||
_oidc_client_id: "{{ OIDC.CLIENT.ID if OIDC.CLIENT is defined and OIDC.CLIENT.ID is defined else SOFTWARE_NAME | lower }}"
|
||||
_oidc_account_url: "{{ _oidc_client_issuer_url ~ '/account' }}"
|
||||
_oidc_protocol_oidc: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect' }}"
|
||||
|
||||
# Definition
|
||||
defaults_oidc:
|
||||
URL: "{{ _oidc_url }}"
|
||||
CLIENT:
|
||||
ID: "{{ _oidc_client_id }}" # Client identifier, typically matching your primary domain
|
||||
# secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters
|
||||
REALM: "{{ _oidc_client_realm }}" # The realm to which the client belongs in the OIDC provider
|
||||
ISSUER_URL: "{{ _oidc_client_issuer_url }}" # Base URL of the OIDC provider (issuer)
|
||||
DISCOVERY_DOCUMENT: "{{ _oidc_client_issuer_url ~ '/.well-known/openid-configuration' }}" # URL for fetching the provider's configuration details
|
||||
AUTHORIZE_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/auth' }}" # Endpoint to start the authorization process
|
||||
TOKEN_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/token' }}" # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url')
|
||||
USER_INFO_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/userinfo' }}" # Endpoint to retrieve user information
|
||||
LOGOUT_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/logout' }}" # Endpoint to log out the user
|
||||
CHANGE_CREDENTIALS: "{{ _oidc_client_issuer_url ~ '/account/account-security/signing-in' }}" # URL for managing or changing user credentials
|
||||
CERTS: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/certs' }}" # JSON Web Key Set (JWKS)
|
||||
ID: "{{ _oidc_client_id }}" # Client identifier, typically matching your primary domain
|
||||
# secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters
|
||||
REALM: "{{ _oidc_client_realm }}" # The realm to which the client belongs in the OIDC provider
|
||||
ISSUER_URL: "{{ _oidc_client_issuer_url }}" # Base URL of the OIDC provider (issuer)
|
||||
DISCOVERY_DOCUMENT: "{{ _oidc_client_issuer_url ~ '/.well-known/openid-configuration' }}" # URL for fetching the provider's configuration details
|
||||
AUTHORIZE_URL: "{{ _oidc_protocol_oidc ~ '/auth' }}" # Endpoint to start the authorization process
|
||||
TOKEN_URL: "{{ _oidc_protocol_oidc ~ '/token' }}" # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url')
|
||||
USER_INFO_URL: "{{ _oidc_protocol_oidc ~ '/userinfo' }}" # Endpoint to retrieve user information
|
||||
LOGOUT_URL: "{{ _oidc_protocol_oidc ~ '/logout' }}" # Endpoint to log out the user
|
||||
CERTS: "{{ _oidc_protocol_oidc ~ '/certs' }}" # JSON Web Key Set (JWKS)
|
||||
ACCOUNT:
|
||||
URL: "{{ _oidc_account_url }}" # Entry point for the user settings console
|
||||
PROFILE_URL: "{{ _oidc_account_url ~ '/#/personal-info' }}" # Section for managing personal information
|
||||
SECURITY_URL: "{{ _oidc_account_url ~ '/#/security/signingin' }}" # Section for managing login and security settings
|
||||
CHANGE_CREDENTIALS: "{{ _oidc_account_url ~ '/account-security/signing-in' }}" # URL for managing or changing user credentials
|
||||
RESET_CREDENTIALS: "{{ _oidc_client_issuer_url ~ '/login-actions/reset-credentials?client_id=' ~ _oidc_client_id }}" # Password reset url
|
||||
BUTTON_TEXT: "SSO Login ({{ PRIMARY_DOMAIN | upper }})" # Default button text
|
||||
BUTTON_TEXT: "SSO Login ({{ PRIMARY_DOMAIN | upper }})" # Default button text
|
||||
ATTRIBUTES:
|
||||
# Attribut to identify the user
|
||||
USERNAME: "preferred_username"
|
||||
|
@ -9,4 +9,4 @@
|
||||
community.general.pacman:
|
||||
name: "libreoffice-{{ applications['desk-libreoffice'].flavor }}-{{ item }}"
|
||||
state: present
|
||||
loop: "{{libreoffice_languages}}"
|
||||
loop: "{{ libreoffice_languages }}"
|
||||
|
@ -59,5 +59,5 @@ http
|
||||
|
||||
# For port proxies
|
||||
stream{
|
||||
include {{NGINX.DIRECTORIES.STREAMS}}*.conf;
|
||||
include {{ NGINX.DIRECTORIES.STREAMS }}*.conf;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
- name: Create {{ domains | get_domain(application_id) }}.conf if LDAP is exposed to internet
|
||||
template:
|
||||
src: "nginx.stream.conf.j2"
|
||||
dest: "{{NGINX.DIRECTORIES.STREAMS}}{{ domains | get_domain(application_id) }}.conf"
|
||||
dest: "{{ NGINX.DIRECTORIES.STREAMS }}{{ domains | get_domain(application_id) }}.conf"
|
||||
notify: restart openresty
|
||||
when: applications | get_app_conf(application_id, 'network.public', True) | bool
|
||||
|
||||
|
@ -10,11 +10,12 @@
|
||||
name: python-requests
|
||||
state: present
|
||||
|
||||
- meta: flush_handlers
|
||||
- name: "Flush webserver handlers"
|
||||
meta: flush_handlers
|
||||
|
||||
- include_role:
|
||||
name: sys-service
|
||||
vars:
|
||||
system_service_on_calendar: "{{ SYS_SCHEDULE_HEALTH_NGINX }}"
|
||||
system_service_timer_enabled: true
|
||||
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
|
||||
system_service_on_calendar: "{{ SYS_SCHEDULE_HEALTH_NGINX }}"
|
||||
system_service_timer_enabled: true
|
||||
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
|
||||
|
@ -15,12 +15,12 @@ def get_expected_statuses(domain: str, parts: list[str], redirected_domains: set
|
||||
Returns:
|
||||
A list of expected HTTP status codes.
|
||||
"""
|
||||
if domain == '{{domains | get_domain('web-app-listmonk')}}':
|
||||
if domain == '{{ domains | get_domain('web-app-listmonk') }}':
|
||||
return [404]
|
||||
if (parts and parts[0] == 'www') or (domain in redirected_domains):
|
||||
return [301]
|
||||
if domain == '{{domains | get_domain('web-app-yourls')}}':
|
||||
return [{{ applications | get_app_conf('web-app-yourls', 'server.status_codes.landingpage', True) }}]
|
||||
if domain == '{{ domains | get_domain('web-app-yourls') }}':
|
||||
return [{{ applications | get_app_conf('web-app-yourls', 'server.status_codes.landingpage') }}]
|
||||
return [200, 302, 301]
|
||||
|
||||
# file in which fqdn server configs are deposit
|
||||
|
@ -1,7 +1,6 @@
|
||||
- name: Include dependency 'sys-daemon'
|
||||
include_role:
|
||||
name: sys-daemon
|
||||
public: true
|
||||
when: run_once_sys_daemon is not defined
|
||||
|
||||
- name: "reset (if enabled)"
|
||||
|
@ -1,4 +1,3 @@
|
||||
# roles/sys-srv-web-inj-compose/filter_plugins/inj_snippets.py
|
||||
"""
|
||||
Jinja filter: `inj_features(kind)` filters a list of features to only those
|
||||
that actually provide the corresponding snippet template file.
|
||||
|
@ -1,4 +1,4 @@
|
||||
{# roles/sys-srv-web-inj-compose/templates/location.lua.j2 #}
|
||||
{# Jinja macro: expands feature snippets into Lua array pushes at render time #}
|
||||
{% macro push_snippets(list_name, features) -%}
|
||||
{% set kind = list_name | regex_replace('_snippets$','') %}
|
||||
{% for f in features if inj_enabled.get(f) -%}
|
||||
@ -14,18 +14,20 @@ header_filter_by_lua_block {
|
||||
local ct = ngx.header.content_type or ""
|
||||
if ct:lower():find("^text/html") then
|
||||
ngx.ctx.is_html = true
|
||||
-- IMPORTANT: body will be modified → drop Content-Length to avoid mismatches
|
||||
ngx.header.content_length = nil
|
||||
else
|
||||
ngx.ctx.is_html = false
|
||||
end
|
||||
}
|
||||
|
||||
body_filter_by_lua_block {
|
||||
-- only apply further processing if this is an HTML response
|
||||
-- Only process HTML responses
|
||||
if not ngx.ctx.is_html then
|
||||
return
|
||||
end
|
||||
|
||||
-- initialize or reuse the buffer
|
||||
-- Buffer all chunks until EOF
|
||||
ngx.ctx.buf = ngx.ctx.buf or {}
|
||||
local chunk, eof = ngx.arg[1], ngx.arg[2]
|
||||
|
||||
@ -34,39 +36,56 @@ body_filter_by_lua_block {
|
||||
end
|
||||
|
||||
if not eof then
|
||||
-- drop intermediate chunks; we’ll emit only on eof
|
||||
-- Swallow intermediate chunks; emit once at EOF
|
||||
ngx.arg[1] = nil
|
||||
return
|
||||
end
|
||||
|
||||
-- on eof: concatenate all buffered chunks
|
||||
-- Concatenate the full HTML
|
||||
local whole = table.concat(ngx.ctx.buf)
|
||||
ngx.ctx.buf = nil -- clear buffer
|
||||
ngx.ctx.buf = nil
|
||||
|
||||
-- remove html CSP, due to management via Infinito.Nexus policies
|
||||
whole = whole:gsub(
|
||||
'<meta[^>]-http%-equiv=["\']Content%-Security%-Policy["\'][^>]->%s*',
|
||||
''
|
||||
)
|
||||
-- Remove inline CSP <meta http-equiv="Content-Security-Policy"> (case-insensitive)
|
||||
local meta_re = [[<meta[^>]+http-equiv=["']Content-Security-Policy["'][^>]*>\s*]]
|
||||
whole = ngx.re.gsub(whole, meta_re, "", "ijo")
|
||||
|
||||
-- build a list of head-injection snippets
|
||||
-- Build head snippets (rendered by Jinja at template time)
|
||||
local head_snippets = {}
|
||||
|
||||
{{ push_snippets('head_snippets', inj_head_features) }}
|
||||
|
||||
-- inject all collected snippets right before </head>
|
||||
local head_payload = table.concat(head_snippets, "\n") .. "</head>"
|
||||
whole = ngx.re.gsub(whole, "</head>", head_payload, "ijo", nil, 1)
|
||||
|
||||
-- build a list of body-injection snippets
|
||||
-- Inject before </head> (first occurrence)
|
||||
local function repl_head(_) return head_payload end
|
||||
local new, n, err = ngx.re.sub(whole, [[</head\s*>]], repl_head, "ijo")
|
||||
if new then
|
||||
whole = new
|
||||
else
|
||||
ngx.log(ngx.WARN, "No </head> found; trying <body> fallback: ", err or "nil")
|
||||
-- Fallback: inject right AFTER the opening <body ...> tag
|
||||
local body_open_re = [[<body\b[^>]*>]]
|
||||
new, n, err = ngx.re.sub(whole, body_open_re, "$0\n" .. table.concat(head_snippets, "\n"), "ijo")
|
||||
if new then
|
||||
whole = new
|
||||
else
|
||||
ngx.log(ngx.ERR, "Head-fallback failed: ", err or "nil")
|
||||
end
|
||||
end
|
||||
|
||||
-- Build body snippets (rendered by Jinja at template time)
|
||||
local body_snippets = {}
|
||||
|
||||
{{ push_snippets('body_snippets', inj_body_features) }}
|
||||
|
||||
-- inject all collected snippets right before </body>
|
||||
local body_payload = table.concat(body_snippets, "\n") .. "</body>"
|
||||
whole = ngx.re.gsub(whole, "</body>", body_payload, "ijo", nil, 1)
|
||||
|
||||
-- finally send the modified HTML out
|
||||
ngx.arg[1] = whole
|
||||
-- Inject before </body> (first occurrence), or append if missing
|
||||
local function repl_body(_) return body_payload end
|
||||
new, n, err = ngx.re.sub(whole, [[</body\s*>]], repl_body, "ijo")
|
||||
if new then
|
||||
whole = new
|
||||
else
|
||||
ngx.log(ngx.WARN, "No </body> found; appending body snippets at end: ", err or "nil")
|
||||
whole = whole .. table.concat(body_snippets, "\n")
|
||||
end
|
||||
|
||||
-- Emit the modified HTML
|
||||
ngx.arg[1] = whole or ""
|
||||
}
|
||||
|
@ -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"
|
||||
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,30 +24,6 @@ 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 %}
|
||||
|
||||
|
@ -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') }}"
|
@ -3,7 +3,6 @@
|
||||
include_tasks: 01_meta.yml
|
||||
when: not KEYCLOAK_LOAD_DEPENDENCIES | bool
|
||||
|
||||
|
||||
- name: "Load cleanup routine for '{{ application_id }}'"
|
||||
include_tasks: 02_cleanup.yml
|
||||
|
||||
|
@ -27,7 +27,7 @@ server:
|
||||
rbac:
|
||||
roles:
|
||||
mail-bot:
|
||||
description: "Has an token to send and recieve emails"
|
||||
description: "Has an token to send and receive emails"
|
||||
docker:
|
||||
services:
|
||||
redis:
|
||||
|
@ -8,7 +8,7 @@
|
||||
ports:
|
||||
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
|
||||
build:
|
||||
context: "{{docker_repository_path}}"
|
||||
context: "{{ docker_repository_path }}"
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- "{{ mig_roles_meta_volume }}:/usr/share/nginx/html/roles:ro"
|
||||
|
@ -114,7 +114,7 @@
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:80"
|
||||
volumes:
|
||||
- {{docker_repository_path}}taiga-gateway/taiga.conf:/etc/nginx/conf.d/default.conf
|
||||
- {{ docker_repository_path }}taiga-gateway/taiga.conf:/etc/nginx/conf.d/default.conf
|
||||
- static-data:/taiga/static
|
||||
- media-data:/taiga/media
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
title: "Blog" # Wordpress titel
|
||||
title: "Blog" # WordPress titel
|
||||
max_upload_size: "15M" # Low default upload size, because you should use Peertube for Videos and Funkwhale for Audio files
|
||||
plugins:
|
||||
wp-discourse:
|
||||
|
@ -63,7 +63,7 @@
|
||||
register: wp_is_multisite
|
||||
changed_when: false
|
||||
|
||||
- name: "Update Single Side Wordpress domain"
|
||||
- name: "Update Single Side WordPress domain"
|
||||
include_tasks: 04_update_domain.yml
|
||||
when: (wp_is_multisite.stdout | trim) == '0'
|
||||
vars:
|
||||
|
@ -1,4 +1,4 @@
|
||||
users: # Credentials
|
||||
administrator: # Wordpress administrator
|
||||
administrator: # WordPress administrator
|
||||
username: "administrator"
|
||||
email: "administrator@{{ PRIMARY_DOMAIN }}"
|
@ -3,7 +3,7 @@ application_id: "web-app-wordpress"
|
||||
database_type: "mariadb"
|
||||
host_msmtp_conf: "{{docker_compose.directories.config}}msmtprc.conf"
|
||||
|
||||
# Wordpress Specific
|
||||
# WordPress Specific
|
||||
wordpress_max_upload_size: "{{ applications | get_app_conf(application_id, 'max_upload_size') }}"
|
||||
wordpress_custom_image: "wordpress_custom"
|
||||
wordpress_docker_html_path: "/var/www/html"
|
||||
|
@ -24,5 +24,5 @@ server:
|
||||
rbac:
|
||||
roles:
|
||||
mail-bot:
|
||||
description: "Has an token to send and recieve emails"
|
||||
description: "Has an token to send and receive emails"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user