mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 18:29:21 +00:00 
			
		
		
		
	feat(frontend): rename inj roles to sys-front-*, add sys-svc-cdn, cache-busting lookup
Introduce sys-svc-cdn (cdn_paths/cdn_urls/cdn_dirs) and ensure CDN directories + latest symlink. Rename sys-srv-web-inj-* → sys-front-inj-*; update includes/templates; serve shared/per-app CSS & JS via CDN. Add lookup_plugins/local_mtime_qs.py for mtime-based cache busting; split CSS into default.css/bootstrap.css + optional per-app style.css. CSP: use style-src-elem; drop unsafe-inline for styles. Services: fix SYS_SERVICE_ALL_ENABLED bool and controlled flush. BREAKING CHANGE: role names changed; replace includes and references accordingly. Conversation: https://chatgpt.com/share/68b55494-9ec4-800f-b559-44707029141d
This commit is contained in:
		
							
								
								
									
										29
									
								
								roles/sys-front-inj-all/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								roles/sys-front-inj-all/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| # Nginx Global Matomo & Theming Modifier Role 🚀 | ||||
|  | ||||
| This role enhances your Nginx configuration by conditionally injecting global Matomo tracking and theming elements into your HTML responses. It uses Nginx sub-filters to seamlessly add tracking scripts and CSS links to your web pages. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - **Global Matomo Tracking**   | ||||
|   The role includes Matomo tracking configuration and injects the corresponding tracking script into your HTML. | ||||
|  | ||||
| - **Global Theming**   | ||||
|   The role injects a global CSS link for consistent theming across your site. | ||||
|  | ||||
| - **Smart Injection**   | ||||
|   Uses Nginx's `sub_filter` to insert the tracking and theming snippets right before the closing `</head>` tag of your HTML documents. | ||||
|  | ||||
|  | ||||
| This will automatically activate Matomo tracking and/or global theming based on your configuration. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Author | ||||
|  | ||||
| Developed by [Kevin Veen-Birkenbach](https://www.veen.world) 😎 | ||||
|  | ||||
| --- | ||||
|  | ||||
| Happy automating! 🎉 | ||||
							
								
								
									
										0
									
								
								roles/sys-front-inj-all/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								roles/sys-front-inj-all/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								roles/sys-front-inj-all/filter_plugins/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								roles/sys-front-inj-all/filter_plugins/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										34
									
								
								roles/sys-front-inj-all/filter_plugins/inj_enabled.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								roles/sys-front-inj-all/filter_plugins/inj_enabled.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| # | ||||
| # Usage in tasks: | ||||
| #   - set_fact: | ||||
| #       inj_enabled: "{{ applications | inj_enabled(application_id, ['javascript','logout','css','matomo','desktop']) }}" | ||||
|  | ||||
| import sys | ||||
| import os | ||||
|  | ||||
| # allow imports from module_utils (same trick as your get_app_conf filter) | ||||
| base = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) | ||||
| mu = os.path.join(base, 'module_utils') | ||||
| for p in (base, mu): | ||||
|     if p not in sys.path: | ||||
|         sys.path.insert(0, p) | ||||
|  | ||||
| from module_utils.config_utils import get_app_conf | ||||
|  | ||||
| def inj_enabled_filter(applications, application_id, features, prefix="features", default=False): | ||||
|     """ | ||||
|     Build a dict {feature: value} by reading the feature flags under the given prefix for the selected application. | ||||
|     Uses get_app_conf with strict=False so missing keys just return the default. | ||||
|     """ | ||||
|     result = {} | ||||
|     for f in features: | ||||
|         path = f"{prefix}.{f}" if prefix else f | ||||
|         result[f] = get_app_conf(applications, application_id, path, strict=False, default=default) | ||||
|     return result | ||||
|  | ||||
|  | ||||
| class FilterModule(object): | ||||
|     def filters(self): | ||||
|         return { | ||||
|             "inj_enabled": inj_enabled_filter, | ||||
|         } | ||||
							
								
								
									
										55
									
								
								roles/sys-front-inj-all/filter_plugins/inj_snippets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								roles/sys-front-inj-all/filter_plugins/inj_snippets.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| """ | ||||
| Jinja filter: `inj_features(kind)` filters a list of features to only those | ||||
| that actually provide the corresponding snippet template file. | ||||
|  | ||||
| - kind='head' -> roles/sys-front-inj-<feature>/templates/head_sub.j2 | ||||
| - kind='body' -> roles/sys-front-inj-<feature>/templates/body_sub.j2 | ||||
|  | ||||
| If the feature's role directory (roles/sys-front-inj-<feature>) does not | ||||
| exist, this filter raises FileNotFoundError. | ||||
|  | ||||
| Usage in a template: | ||||
|     {% set head_features = SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('head') %} | ||||
|     {% set body_features = SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('body') %} | ||||
| """ | ||||
|  | ||||
| import os | ||||
|  | ||||
| # This file lives at: roles/sys-front-inj-all/filter_plugins/inj_snippets.py | ||||
| _THIS_DIR = os.path.dirname(__file__) | ||||
| _ROLE_DIR = os.path.abspath(os.path.join(_THIS_DIR, ".."))   # roles/sys-front-inj-all | ||||
| _ROLES_DIR = os.path.abspath(os.path.join(_ROLE_DIR, ".."))  # roles | ||||
|  | ||||
| def _feature_role_dir(feature: str) -> str: | ||||
|     return os.path.join(_ROLES_DIR, f"sys-front-inj-{feature}") | ||||
|  | ||||
| def _has_snippet(feature: str, kind: str) -> bool: | ||||
|     if kind not in ("head", "body"): | ||||
|         raise ValueError("kind must be 'head' or 'body'") | ||||
|  | ||||
|     role_dir = _feature_role_dir(feature) | ||||
|     if not os.path.isdir(role_dir): | ||||
|         raise FileNotFoundError( | ||||
|             f"[inj_snippets] Expected role directory not found for feature " | ||||
|             f"'{feature}': {role_dir}" | ||||
|         ) | ||||
|  | ||||
|     path = os.path.join(role_dir, "templates", f"{kind}_sub.j2") | ||||
|     return os.path.exists(path) | ||||
|  | ||||
| def inj_features_filter(features, kind: str = "head"): | ||||
|     if not isinstance(features, (list, tuple)): | ||||
|         return [] | ||||
|     # Validation + filtering in one pass; will raise if a role dir is missing. | ||||
|     valid = [] | ||||
|     for f in features: | ||||
|         name = str(f) | ||||
|         if _has_snippet(name, kind): | ||||
|             valid.append(name) | ||||
|     return valid | ||||
|  | ||||
| class FilterModule(object): | ||||
|     def filters(self): | ||||
|         return { | ||||
|             "inj_features": inj_features_filter, | ||||
|         } | ||||
							
								
								
									
										22
									
								
								roles/sys-front-inj-all/meta/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								roles/sys-front-inj-all/meta/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| galaxy_info: | ||||
|   author: "Kevin Veen-Birkenbach" | ||||
|   description: "Core role for Nginx HTML injection of Matomo, theming, iFrame and JS snippets based on application feature flags." | ||||
|   license: "Infinito.Nexus NonCommercial License" | ||||
|   license_url: "https://s.infinito.nexus/license" | ||||
|   company: | | ||||
|     Kevin Veen-Birkenbach | ||||
|     Consulting & Coaching Solutions | ||||
|     https://www.veen.world | ||||
|   galaxy_tags: | ||||
|   - nginx | ||||
|   - injector | ||||
|   - matomo | ||||
|   - theming | ||||
|   repository: "https://s.infinito.nexus/code" | ||||
|   issue_tracker_url: "https://s.infinito.nexus/issues" | ||||
|   documentation: "https://s.infinito.nexus/code/tree/main/roles/sys-front-inj-all" | ||||
|   min_ansible_version: "2.9" | ||||
|   platforms: | ||||
|   - name: Any | ||||
|     versions: | ||||
|     - all | ||||
							
								
								
									
										49
									
								
								roles/sys-front-inj-all/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								roles/sys-front-inj-all/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| - name: Build inj_enabled | ||||
|   set_fact: | ||||
|     inj_enabled: "{{ applications | inj_enabled(application_id, SRV_WEB_INJ_COMP_FEATURES_ALL) }}" | ||||
|  | ||||
| - name: "Load CDN Service for '{{ domain }}'" | ||||
|   include_role: | ||||
|     name: sys-svc-cdn | ||||
|     public: true # Expose variables so that they can be used in all injection roles | ||||
|  | ||||
| - name: Reinitialize 'inj_enabled' for '{{ domain }}', after modification by CDN | ||||
|   set_fact: | ||||
|     inj_enabled: "{{ applications | inj_enabled(application_id, SRV_WEB_INJ_COMP_FEATURES_ALL) }}" | ||||
|     inj_head_features: "{{ SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('head') }}" | ||||
|     inj_body_features: "{{ SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('body') }}" | ||||
|  | ||||
| - name: "Activate Desktop iFrame notifier for '{{ domain }}'" | ||||
|   include_role: | ||||
|     name:   sys-front-inj-desktop | ||||
|     public: true # Vars used in templates | ||||
|   when: inj_enabled.desktop | ||||
|  | ||||
| - name: "Activate Corporate CSS for '{{ domain }}'" | ||||
|   include_role: | ||||
|     name: sys-front-inj-css | ||||
|   when: inj_enabled.css | ||||
|  | ||||
| - name: "Activate Matomo Tracking for '{{ domain }}'" | ||||
|   include_role: | ||||
|     name: sys-front-inj-matomo | ||||
|   when: inj_enabled.matomo | ||||
|  | ||||
| - name: "Activate Javascript for '{{ domain }}'" | ||||
|   include_role: | ||||
|     name: sys-front-inj-javascript | ||||
|   when: inj_enabled.javascript | ||||
|  | ||||
| - name: "Activate logout proxy for '{{ domain }}'" | ||||
|   include_role: | ||||
|     name: sys-front-inj-logout | ||||
|     public: true # Vars used in templates | ||||
|   when: inj_enabled.logout | ||||
|  | ||||
| - block: | ||||
|   - name: Include dependency 'srv-core' | ||||
|     include_role: | ||||
|       name: srv-core | ||||
|     when: run_once_srv_core is not defined | ||||
|   - include_tasks: utils/run_once.yml | ||||
|   when: run_once_sys_front_inj_all is not defined | ||||
							
								
								
									
										91
									
								
								roles/sys-front-inj-all/templates/location.lua.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								roles/sys-front-inj-all/templates/location.lua.j2
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| {# 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) -%} | ||||
| {{ list_name }}[#{{ list_name }} + 1] = [=[ | ||||
|   {%- include 'roles/sys-front-inj-' ~ f ~ '/templates/' ~ kind ~ '_sub.j2' -%} | ||||
| ]=] | ||||
| {% endfor -%} | ||||
| {%- endmacro %} | ||||
|  | ||||
| lua_need_request_body on; | ||||
|  | ||||
| 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 process HTML responses | ||||
|     if not ngx.ctx.is_html then | ||||
|         return | ||||
|     end | ||||
|  | ||||
|     -- Buffer all chunks until EOF | ||||
|     ngx.ctx.buf = ngx.ctx.buf or {} | ||||
|     local chunk, eof = ngx.arg[1], ngx.arg[2] | ||||
|  | ||||
|     if chunk ~= "" then | ||||
|         table.insert(ngx.ctx.buf, chunk) | ||||
|     end | ||||
|  | ||||
|     if not eof then | ||||
|         -- Swallow intermediate chunks; emit once at EOF | ||||
|         ngx.arg[1] = nil | ||||
|         return | ||||
|     end | ||||
|  | ||||
|     -- Concatenate the full HTML | ||||
|     local whole = table.concat(ngx.ctx.buf) | ||||
|     ngx.ctx.buf = nil | ||||
|  | ||||
|     -- 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 head snippets (rendered by Jinja at template time) | ||||
|     local head_snippets = {} | ||||
|     {{ push_snippets('head_snippets', inj_head_features) }} | ||||
|     local head_payload = table.concat(head_snippets, "\n") .. "</head>" | ||||
|  | ||||
|     -- 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) }} | ||||
|     local body_payload = table.concat(body_snippets, "\n") .. "</body>" | ||||
|  | ||||
|     -- 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 "" | ||||
| } | ||||
							
								
								
									
										3
									
								
								roles/sys-front-inj-all/templates/server.conf.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								roles/sys-front-inj-all/templates/server.conf.j2
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| {% if inj_enabled.logout %} | ||||
| {% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %} | ||||
| {% endif %} | ||||
							
								
								
									
										9
									
								
								roles/sys-front-inj-all/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								roles/sys-front-inj-all/vars/main.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # Docker | ||||
| docker_pull_git_repository: false # Deactivated here to don't inhire this | ||||
|  | ||||
| SRV_WEB_INJ_COMP_FEATURES_ALL: | ||||
|   - 'javascript' | ||||
|   - 'logout' | ||||
|   - 'css' | ||||
|   - 'matomo' | ||||
|   - 'desktop' | ||||
		Reference in New Issue
	
	Block a user