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
|
# @see https://en.wikipedia.org/wiki/OpenID_Connect
|
||||||
|
|
||||||
## Helper Variables:
|
# Helper Variables:
|
||||||
_oidc_client_realm: "{{ OIDC.CLIENT.REALM if OIDC.CLIENT is defined and OIDC.CLIENT.REALM is defined else SOFTWARE_NAME | lower }}"
|
_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: "{{
|
||||||
( OIDC.URL
|
( OIDC.URL
|
||||||
if (OIDC is defined and OIDC.URL is defined)
|
if (OIDC is defined and OIDC.URL is defined)
|
||||||
else WEB_PROTOCOL ~ '://' ~ (domains | get_domain('web-app-keycloak'))
|
else domains | get_url('web-app-keycloak', WEB_PROTOCOL)
|
||||||
).rstrip('/')
|
).rstrip('/')
|
||||||
}}"
|
}}"
|
||||||
_oidc_client_issuer_url: "{{ _oidc_url ~ '/realms/' ~ _oidc_client_realm }}"
|
_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_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:
|
defaults_oidc:
|
||||||
URL: "{{ _oidc_url }}"
|
URL: "{{ _oidc_url }}"
|
||||||
CLIENT:
|
CLIENT:
|
||||||
ID: "{{ _oidc_client_id }}" # Client identifier, typically matching your primary domain
|
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
|
# 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
|
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)
|
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
|
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
|
AUTHORIZE_URL: "{{ _oidc_protocol_oidc ~ '/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')
|
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_client_issuer_url ~ '/protocol/openid-connect/userinfo' }}" # Endpoint to retrieve user information
|
USER_INFO_URL: "{{ _oidc_protocol_oidc ~ '/userinfo' }}" # Endpoint to retrieve user information
|
||||||
LOGOUT_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/logout' }}" # Endpoint to log out the user
|
LOGOUT_URL: "{{ _oidc_protocol_oidc ~ '/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_protocol_oidc ~ '/certs' }}" # JSON Web Key Set (JWKS)
|
||||||
CERTS: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/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
|
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:
|
ATTRIBUTES:
|
||||||
# Attribut to identify the user
|
# Attribut to identify the user
|
||||||
USERNAME: "preferred_username"
|
USERNAME: "preferred_username"
|
||||||
|
@ -9,4 +9,4 @@
|
|||||||
community.general.pacman:
|
community.general.pacman:
|
||||||
name: "libreoffice-{{ applications['desk-libreoffice'].flavor }}-{{ item }}"
|
name: "libreoffice-{{ applications['desk-libreoffice'].flavor }}-{{ item }}"
|
||||||
state: present
|
state: present
|
||||||
loop: "{{libreoffice_languages}}"
|
loop: "{{ libreoffice_languages }}"
|
||||||
|
@ -59,5 +59,5 @@ http
|
|||||||
|
|
||||||
# For port proxies
|
# For port proxies
|
||||||
stream{
|
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
|
- name: Create {{ domains | get_domain(application_id) }}.conf if LDAP is exposed to internet
|
||||||
template:
|
template:
|
||||||
src: "nginx.stream.conf.j2"
|
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
|
notify: restart openresty
|
||||||
when: applications | get_app_conf(application_id, 'network.public', True) | bool
|
when: applications | get_app_conf(application_id, 'network.public', True) | bool
|
||||||
|
|
||||||
|
@ -10,11 +10,12 @@
|
|||||||
name: python-requests
|
name: python-requests
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
- meta: flush_handlers
|
- name: "Flush webserver handlers"
|
||||||
|
meta: flush_handlers
|
||||||
|
|
||||||
- include_role:
|
- include_role:
|
||||||
name: sys-service
|
name: sys-service
|
||||||
vars:
|
vars:
|
||||||
system_service_on_calendar: "{{ SYS_SCHEDULE_HEALTH_NGINX }}"
|
system_service_on_calendar: "{{ SYS_SCHEDULE_HEALTH_NGINX }}"
|
||||||
system_service_timer_enabled: true
|
system_service_timer_enabled: true
|
||||||
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
|
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:
|
Returns:
|
||||||
A list of expected HTTP status codes.
|
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]
|
return [404]
|
||||||
if (parts and parts[0] == 'www') or (domain in redirected_domains):
|
if (parts and parts[0] == 'www') or (domain in redirected_domains):
|
||||||
return [301]
|
return [301]
|
||||||
if domain == '{{domains | get_domain('web-app-yourls')}}':
|
if domain == '{{ domains | get_domain('web-app-yourls') }}':
|
||||||
return [{{ applications | get_app_conf('web-app-yourls', 'server.status_codes.landingpage', True) }}]
|
return [{{ applications | get_app_conf('web-app-yourls', 'server.status_codes.landingpage') }}]
|
||||||
return [200, 302, 301]
|
return [200, 302, 301]
|
||||||
|
|
||||||
# file in which fqdn server configs are deposit
|
# file in which fqdn server configs are deposit
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
- name: Include dependency 'sys-daemon'
|
- name: Include dependency 'sys-daemon'
|
||||||
include_role:
|
include_role:
|
||||||
name: sys-daemon
|
name: sys-daemon
|
||||||
public: true
|
|
||||||
when: run_once_sys_daemon is not defined
|
when: run_once_sys_daemon is not defined
|
||||||
|
|
||||||
- name: "reset (if enabled)"
|
- 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
|
Jinja filter: `inj_features(kind)` filters a list of features to only those
|
||||||
that actually provide the corresponding snippet template file.
|
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) -%}
|
{% macro push_snippets(list_name, features) -%}
|
||||||
{% set kind = list_name | regex_replace('_snippets$','') %}
|
{% set kind = list_name | regex_replace('_snippets$','') %}
|
||||||
{% for f in features if inj_enabled.get(f) -%}
|
{% 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 ""
|
local ct = ngx.header.content_type or ""
|
||||||
if ct:lower():find("^text/html") then
|
if ct:lower():find("^text/html") then
|
||||||
ngx.ctx.is_html = true
|
ngx.ctx.is_html = true
|
||||||
|
-- IMPORTANT: body will be modified → drop Content-Length to avoid mismatches
|
||||||
|
ngx.header.content_length = nil
|
||||||
else
|
else
|
||||||
ngx.ctx.is_html = false
|
ngx.ctx.is_html = false
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
body_filter_by_lua_block {
|
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
|
if not ngx.ctx.is_html then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- initialize or reuse the buffer
|
-- Buffer all chunks until EOF
|
||||||
ngx.ctx.buf = ngx.ctx.buf or {}
|
ngx.ctx.buf = ngx.ctx.buf or {}
|
||||||
local chunk, eof = ngx.arg[1], ngx.arg[2]
|
local chunk, eof = ngx.arg[1], ngx.arg[2]
|
||||||
|
|
||||||
@ -34,39 +36,56 @@ body_filter_by_lua_block {
|
|||||||
end
|
end
|
||||||
|
|
||||||
if not eof then
|
if not eof then
|
||||||
-- drop intermediate chunks; we’ll emit only on eof
|
-- Swallow intermediate chunks; emit once at EOF
|
||||||
ngx.arg[1] = nil
|
ngx.arg[1] = nil
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- on eof: concatenate all buffered chunks
|
-- Concatenate the full HTML
|
||||||
local whole = table.concat(ngx.ctx.buf)
|
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
|
-- Remove inline CSP <meta http-equiv="Content-Security-Policy"> (case-insensitive)
|
||||||
whole = whole:gsub(
|
local meta_re = [[<meta[^>]+http-equiv=["']Content-Security-Policy["'][^>]*>\s*]]
|
||||||
'<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 = {}
|
local head_snippets = {}
|
||||||
|
|
||||||
{{ push_snippets('head_snippets', inj_head_features) }}
|
{{ push_snippets('head_snippets', inj_head_features) }}
|
||||||
|
|
||||||
-- inject all collected snippets right before </head>
|
|
||||||
local head_payload = table.concat(head_snippets, "\n") .. "</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 = {}
|
local body_snippets = {}
|
||||||
|
|
||||||
{{ push_snippets('body_snippets', inj_body_features) }}
|
{{ push_snippets('body_snippets', inj_body_features) }}
|
||||||
|
|
||||||
-- inject all collected snippets right before </body>
|
|
||||||
local body_payload = table.concat(body_snippets, "\n") .. "</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
|
-- Inject before </body> (first occurrence), or append if missing
|
||||||
ngx.arg[1] = whole
|
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 ""
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
global_css_destination: "{{ NGINX.DIRECTORIES.DATA.CDN }}global.css"
|
global_css_destination: "{{ NGINX.DIRECTORIES.DATA.CDN }}global.css"
|
||||||
global_css_base_color: "{{ design.css.colors.base }}"
|
global_css_base_color: "{{ design.css.colors.base }}"
|
||||||
global_css_count: 7
|
global_css_count: 7
|
||||||
global_css_shades: 100
|
global_css_shades: 100
|
@ -2,9 +2,10 @@ features:
|
|||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
desktop: false
|
desktop: false
|
||||||
simpleicons: true # Activate Brand Icons for your groups
|
oidc: true # Needs to be activated so that the login url is working
|
||||||
javascript: true # Necessary for URL sync
|
simpleicons: true # Activate Brand Icons for your groups
|
||||||
logout: false # Doesn't have own user data. Just a frame.
|
javascript: true # Necessary for URL sync
|
||||||
|
logout: true
|
||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
@ -19,6 +20,7 @@ server:
|
|||||||
- https://cdn.jsdelivr.net
|
- https://cdn.jsdelivr.net
|
||||||
connect-src:
|
connect-src:
|
||||||
- https://ka-f.fontawesome.com
|
- https://ka-f.fontawesome.com
|
||||||
|
- "{{ WEB_PROTOCOL }}://auth.{{ PRIMARY_DOMAIN }}"
|
||||||
frame-src:
|
frame-src:
|
||||||
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
||||||
flags:
|
flags:
|
||||||
@ -31,4 +33,8 @@ server:
|
|||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "{{ PRIMARY_DOMAIN }}"
|
- "{{ PRIMARY_DOMAIN }}"
|
||||||
|
docker:
|
||||||
|
services:
|
||||||
|
desktop:
|
||||||
|
name: "desktop"
|
||||||
|
image: "application-portfolio"
|
@ -2,13 +2,16 @@
|
|||||||
include_tasks: "02_validate.yml"
|
include_tasks: "02_validate.yml"
|
||||||
when: MODE_ASSERT | bool
|
when: MODE_ASSERT | bool
|
||||||
|
|
||||||
|
- name: "Include JS routines"
|
||||||
|
include_tasks: "03_javascript.yml"
|
||||||
|
|
||||||
- name: "load docker, proxy for '{{ application_id }}'"
|
- name: "load docker, proxy for '{{ application_id }}'"
|
||||||
include_role:
|
include_role:
|
||||||
name: cmp-docker-proxy
|
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:
|
stat:
|
||||||
path: "{{ config_inventory_path }}"
|
path: "{{ DESKTOP_INVENTORY_CONFIG_PATH }}"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
become: false
|
become: false
|
||||||
register: config_file
|
register: config_file
|
||||||
@ -42,20 +45,20 @@
|
|||||||
|
|
||||||
- name: Copy host-specific config.yaml if it exists
|
- name: Copy host-specific config.yaml if it exists
|
||||||
template:
|
template:
|
||||||
src: "{{ config_inventory_path }}"
|
src: "{{ DESKTOP_INVENTORY_CONFIG_PATH }}"
|
||||||
dest: "{{docker_repository_path}}/app/config.yaml"
|
dest: "{{ docker_repository_path }}/app/config.yaml"
|
||||||
notify: docker compose up
|
notify: docker compose up
|
||||||
when: config_file.stat.exists
|
when: config_file.stat.exists
|
||||||
|
|
||||||
- name: Copy default config.yaml from the role template if host-specific file does not exist
|
- name: Copy default config.yaml from the role template if host-specific file does not exist
|
||||||
template:
|
template:
|
||||||
src: "config.yaml.j2"
|
src: "config.yaml.j2"
|
||||||
dest: "{{docker_repository_path}}/app/config.yaml"
|
dest: "{{ docker_repository_path }}/app/config.yaml"
|
||||||
notify: docker compose up
|
notify: docker compose up
|
||||||
when: not config_file.stat.exists
|
when: not config_file.stat.exists
|
||||||
|
|
||||||
- name: add docker-compose.yml
|
- name: add docker-compose.yml
|
||||||
template:
|
template:
|
||||||
src: docker-compose.yml.j2
|
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
|
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:
|
portfolio:
|
||||||
{% set container_port = 5000 %}
|
{% set container_port = 5000 %}
|
||||||
build:
|
build:
|
||||||
context: {{docker_repository_path}}
|
context: {{ docker_repository_path }}
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: application-portfolio
|
image: {{ DESKTOP_IMAGE }}
|
||||||
container_name: portfolio
|
container_name: {{ DESKTOP_CONTAINER }}
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
|
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
|
||||||
volumes:
|
volumes:
|
||||||
- {{docker_repository_path}}app:/app
|
- {{ docker_repository_path }}app:/app
|
||||||
restart: unless-stopped
|
restart: {{ DOCKER_RESTART_POLICY }}
|
||||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||||
{% include 'roles/docker-container/templates/healthcheck/tcp.yml.j2' %}
|
{% include 'roles/docker-container/templates/healthcheck/tcp.yml.j2' %}
|
||||||
|
|
||||||
|
@ -1,30 +1,46 @@
|
|||||||
window.addEventListener("message", function(event) {
|
// ===== Runtime loader for external JS files (no Jinja includes) =====
|
||||||
const allowedSuffix = ".{{ PRIMARY_DOMAIN }}";
|
(function () {
|
||||||
const origin = event.origin;
|
// 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 }}
|
// 2) Helper to load a <script> with proper query param
|
||||||
if (!origin.endsWith(allowedSuffix)) return;
|
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;
|
// 3) Load all files in order
|
||||||
|
async function loadAll() {
|
||||||
// 2. Only process valid iframeLocationChange messages
|
for (const name of FILES) {
|
||||||
if (data && data.type === "iframeLocationChange" && typeof data.href === "string") {
|
const fullUrl = BASE_URL + "/" + name.replace(/^\/+/, "");
|
||||||
try {
|
await loadScriptSequential(fullUrl);
|
||||||
const hrefUrl = new URL(data.href);
|
}
|
||||||
|
// Optional: hook after everything is ready
|
||||||
// 3. Only allow redirects to *.{{ PRIMARY_DOMAIN }}
|
if (typeof window.onDesktopJsLoaded === "function") {
|
||||||
if (!hrefUrl.hostname.endsWith(allowedSuffix)) return;
|
try { window.onDesktopJsLoaded(); } catch {}
|
||||||
|
|
||||||
// 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 %}
|
// 4) Start after DOM is ready (safe point to inject <script> tags)
|
||||||
console.log("[iframe-sync] Listener for iframe messages is active.");
|
if (document.readyState === "loading") {
|
||||||
{% endif %}
|
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 }}
|
icon: {{ app.icon }}
|
||||||
url: {{ app.url }}
|
url: {{ app.url }}
|
||||||
iframe: {{ app.iframe }}
|
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 %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
followus:
|
followus:
|
||||||
name: Follow Us
|
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:
|
icon:
|
||||||
class: fas fa-newspaper
|
class: fas fa-newspaper
|
||||||
{% if ["web-app-mastodon", "web-app-bluesky"] | any_in(group_names) %}
|
{% 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) }}
|
iframe: {{ applications | get_app_conf('web-app-peertube','features.desktop',True) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %}
|
{% 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.
|
description: Read {{ 'our' if service_provider.type == 'legal' else 'my' }} articles and stories.
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-blog
|
class: fa-solid fa-blog
|
||||||
@ -55,7 +55,7 @@ followus:
|
|||||||
- name: Friendica
|
- name: Friendica
|
||||||
description: Visit {{ 'our' if service_provider.type == 'legal' else 'my' }} friendica profile
|
description: Visit {{ 'our' if service_provider.type == 'legal' else 'my' }} friendica profile
|
||||||
icon:
|
icon:
|
||||||
class: fas fa-net-wired
|
class: fa-solid fa-network-wired
|
||||||
identifier: "{{service_provider.contact.friendica}}"
|
identifier: "{{service_provider.contact.friendica}}"
|
||||||
url: "{{ WEB_PROTOCOL }}://{{ service_provider.contact.friendica.split('@')[2] }}/@{{ service_provider.contact.friendica.split('@')[1] }}"
|
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) }}
|
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.
|
description: Access our comprehensive documentation and support resources to help you get the most out of the software.
|
||||||
icon:
|
icon:
|
||||||
class: fas fa-book
|
class: fas fa-book
|
||||||
url: https://{{domains | get_domain('web-app-sphinx')}}
|
url: {{ domains | get_url('web-app-sphinx', WEB_PROTOCOL) }}
|
||||||
iframe: {{ applications | get_app_conf('web-app-sphinx','features.desktop',True) }}
|
iframe: {{ applications | get_app_conf('web-app-sphinx','features.desktop') }}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -20,8 +20,8 @@
|
|||||||
description: Checkout the presentation
|
description: Checkout the presentation
|
||||||
icon:
|
icon:
|
||||||
class: "fas fa-chalkboard-teacher"
|
class: "fas fa-chalkboard-teacher"
|
||||||
url: https://{{domains | get_domain('web-app-navigator')}}
|
url: {{ domains | get_url('web-app-navigator', WEB_PROTOCOL) }}
|
||||||
iframe: {{ applications | get_app_conf('web-app-navigator','features.desktop',True) }}
|
iframe: {{ applications | get_app_conf('web-app-navigator','features.desktop') }}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- name: Solutions
|
- name: Solutions
|
||||||
|
@ -17,4 +17,39 @@
|
|||||||
description: Reload the application
|
description: Reload the application
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-rotate-right
|
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"
|
# General
|
||||||
docker_repository_address: "https://github.com/kevinveenbirkenbach/port-ui"
|
application_id: "web-app-desktop"
|
||||||
config_inventory_path: "{{ inventory_dir }}/files/{{ inventory_hostname }}/docker/web-app-desktop/config.yaml.j2"
|
|
||||||
docker_pull_git_repository: true
|
## 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
|
include_tasks: 01_meta.yml
|
||||||
when: not KEYCLOAK_LOAD_DEPENDENCIES | bool
|
when: not KEYCLOAK_LOAD_DEPENDENCIES | bool
|
||||||
|
|
||||||
|
|
||||||
- name: "Load cleanup routine for '{{ application_id }}'"
|
- name: "Load cleanup routine for '{{ application_id }}'"
|
||||||
include_tasks: 02_cleanup.yml
|
include_tasks: 02_cleanup.yml
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ server:
|
|||||||
rbac:
|
rbac:
|
||||||
roles:
|
roles:
|
||||||
mail-bot:
|
mail-bot:
|
||||||
description: "Has an token to send and recieve emails"
|
description: "Has an token to send and receive emails"
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
|
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
|
||||||
build:
|
build:
|
||||||
context: "{{docker_repository_path}}"
|
context: "{{ docker_repository_path }}"
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ mig_roles_meta_volume }}:/usr/share/nginx/html/roles:ro"
|
- "{{ mig_roles_meta_volume }}:/usr/share/nginx/html/roles:ro"
|
||||||
|
@ -114,7 +114,7 @@
|
|||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:80"
|
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:80"
|
||||||
volumes:
|
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
|
- static-data:/taiga/static
|
||||||
- media-data:/taiga/media
|
- media-data:/taiga/media
|
||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% 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
|
max_upload_size: "15M" # Low default upload size, because you should use Peertube for Videos and Funkwhale for Audio files
|
||||||
plugins:
|
plugins:
|
||||||
wp-discourse:
|
wp-discourse:
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
register: wp_is_multisite
|
register: wp_is_multisite
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: "Update Single Side Wordpress domain"
|
- name: "Update Single Side WordPress domain"
|
||||||
include_tasks: 04_update_domain.yml
|
include_tasks: 04_update_domain.yml
|
||||||
when: (wp_is_multisite.stdout | trim) == '0'
|
when: (wp_is_multisite.stdout | trim) == '0'
|
||||||
vars:
|
vars:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
users: # Credentials
|
users: # Credentials
|
||||||
administrator: # Wordpress administrator
|
administrator: # WordPress administrator
|
||||||
username: "administrator"
|
username: "administrator"
|
||||||
email: "administrator@{{ PRIMARY_DOMAIN }}"
|
email: "administrator@{{ PRIMARY_DOMAIN }}"
|
@ -3,7 +3,7 @@ application_id: "web-app-wordpress"
|
|||||||
database_type: "mariadb"
|
database_type: "mariadb"
|
||||||
host_msmtp_conf: "{{docker_compose.directories.config}}msmtprc.conf"
|
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_max_upload_size: "{{ applications | get_app_conf(application_id, 'max_upload_size') }}"
|
||||||
wordpress_custom_image: "wordpress_custom"
|
wordpress_custom_image: "wordpress_custom"
|
||||||
wordpress_docker_html_path: "/var/www/html"
|
wordpress_docker_html_path: "/var/www/html"
|
||||||
|
@ -24,5 +24,5 @@ server:
|
|||||||
rbac:
|
rbac:
|
||||||
roles:
|
roles:
|
||||||
mail-bot:
|
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