Browse Source

feat: Enhance Keycloak integration and refactor setup process

This commit introduces significant improvements to the Keycloak integration and refactors the setup process for better security and automation.

Key changes include:

*   **Security:**
    *   Implemented secure handling of the Nextcloud client secret by passing it as an extra variable to the Ansible playbook.
    *   Moved credential files to `.gitignore` and implemented a more robust encryption method in the setup script, prioritizing `openssl` with `gpg` as a fallback.
    *   Removed the previous credential backup file.
*   **Keycloak Configuration:**
    *   Added a dedicated location block in the Keycloak Nginx configuration for the token endpoint, including specific buffer and timeout settings.
    *   Ensured proper forwarding of SSL information to Keycloak.
    *   Improved the Nextcloud client configuration in Keycloak, including `postLogoutRedirectUris` and the creation of a dedicated `groups-nextcloud` client scope with a group membership mapper.
    *   Added more granular Nextcloud-specific groups in Keycloak (admins, users, youpi, service).
    *   Removed the `nextcloud-dedicated` client scope from the Nextcloud client.
*   **Nextcloud Configuration:**
    *   Added an explicit Nginx location block for the Nextcloud OpenID Connect callback.
    *   Configured Nextcloud with environment variables for URL, debug mode, and custom OpenID Connect scopes.
*   **Setup Script Refactoring:**
    *   Completely refactored the `setup_environment.sh` script for better organization, readability, and error handling.
    *   Introduced functions for password generation, environment file creation, and conditional password setting.
    *   Integrated Ansible for configuring Keycloak and Nextcloud after initial setup.
    *   Improved logging and feedback during the setup process.
*   **Docker Configuration:**
    *   Standardized the Keycloak database volume name.
    *   Added `extra_hosts` entries in `docker-compose.yml` to facilitate communication between containers using domain names.
    *   Adjusted health checks for Keycloak and Nextcloud.
*   **General Improvements:**
    *   Added more detailed logging in the Keycloak setup script (`setup_realm.js`).
    *   Adjusted the admin group check in the Keycloak test script.

These changes aim to provide a more secure, robust, and automated setup for the office automation platform, with a focus on seamless Keycloak and Nextcloud integration.
mrx8086 11 months ago
parent
commit
40ee243a42

+ 2 - 1
.gitignore

@@ -1,2 +1,3 @@
 data/
 data/
-config/nextcloud/
+config/credentials
+config/nextcloud/

+ 4 - 0
ansible/requirements.yml

@@ -0,0 +1,4 @@
+---
+collections:
+  - name: community.general
+    version: ">=6.0.0"

+ 23 - 0
ansible/roles/services/defaults/main.yml

@@ -0,0 +1,23 @@
+---
+# Default variables for SSO configuration
+sso_config:
+  custom_oidc:
+    - name: "keycloak"
+      title: "keycloak"
+      authorizeUrl: "https://auth.mrx8086.com/realms/office-automation/protocol/openid-connect/auth"
+      tokenUrl: "https://auth.mrx8086.com/realms/office-automation/protocol/openid-connect/token"
+      userInfoUrl: "https://auth.mrx8086.com/realms/office-automation/protocol/openid-connect/userinfo"
+      logoutUrl: "https://auth.mrx8086.com/realms/office-automation/protocol/openid-connect/logout?post_logout_redirect_uri=https%3A%2F%2Fcloud.mrx8086.com&client_id=nextcloud"
+      clientId: "nextcloud"
+      clientSecret: "{{ client_secret }}"
+      scope: "openid groups-nextcloud profile"
+      groupsClaim: "groups"
+      style: "keycloak"
+      defaultGroup: ""
+      groupMapping:
+        nextcloud-admins: "admin"
+        nextcloud-users: "users"
+        nextcloud-youpi: "youpi"
+
+# Default paths and settings
+nextcloud_data_dir: "/var/www/html/data"

+ 123 - 0
ansible/roles/services/tasks/main.yml

@@ -0,0 +1,123 @@
+---
+# Verify client secret
+- name: "Verify client secret is available"
+  fail:
+    msg: "Client secret is not set or empty"
+  when: client_secret is not defined or client_secret | default('') | trim == ''
+
+# First disable maintenance mode to ensure app commands work
+- name: "Ensure maintenance mode is off before starting"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ maintenance:mode --off"
+  ignore_errors: true
+
+- name: "Uninstall Sociallogin app"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ app:remove sociallogin"
+  ignore_errors: true
+
+- name: "Install sociallogin app"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ app:install sociallogin"
+
+- name: "Create users group"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ group:add nextcloud-users"
+  ignore_errors: true  # Falls die Gruppe bereits existiert
+
+- name: "Create admin group if not exists"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ group:add nextcloud-admins"
+  ignore_errors: true
+
+- name: "Create youpi group"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ group:add nextcloud-youpi"
+  ignore_errors: true
+
+# Configure Social Login
+- name: "Set Social Login custom providers config"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ config:app:set sociallogin custom_providers --value='{{ sso_config | to_json }}'"
+  register: config_result
+
+- name: "Debug config result"
+  debug:
+    var: config_result
+    verbosity: 1
+
+- name: "Verify Social Login config"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ config:app:get sociallogin custom_providers"
+  register: verify_config
+
+- name: "Debug verification result"
+  debug:
+    var: verify_config
+    verbosity: 1
+
+# Configure Social Login settings
+- name: "Set Social Login prevent_create_email_exists"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ config:app:set sociallogin prevent_create_email_exists --value='1'"
+
+- name: "Set Social Login update_profile_on_login"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ config:app:set sociallogin update_profile_on_login --value='1'"
+
+- name: "Set Social Login restrict_users_wo_mapped_groups"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ config:app:set sociallogin restrict_users_wo_mapped_groups --value='1'"
+
+- name: "Set Social Login restrict_users_wo_assigned_groups"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ config:app:set sociallogin restrict_users_wo_assigned_groups --value='1'"
+
+# Data directory setup
+- name: "Create .ncdata file"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud sh -c 'echo \"# Nextcloud data directory\" > {{ nextcloud_data_dir }}/.ncdata'"
+
+- name: "Set data folder ownership"
+  become: true
+  command:
+    cmd: "docker exec -u 0 nextcloud chown -R 33:33 {{ nextcloud_data_dir }}"
+
+- name: "Set data folder permissions"
+  become: true
+  command:
+     cmd: "docker exec -u 0 nextcloud chmod -R 770 {{ nextcloud_data_dir }}"
+
+# Restart the app to apply changes
+- name: "Disable sociallogin app"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ app:disable sociallogin"
+
+- name: "Enable sociallogin app"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ app:enable sociallogin"
+
+- name: "Verify sociallogin configuration"
+  become: true
+  command:
+    cmd: "docker exec -u 33 nextcloud php /var/www/html/occ config:app:get sociallogin custom_providers"
+  register: sso_config_verification
+
+- name: "Display SSO configuration"
+  debug:
+    var: sso_config_verification.stdout

+ 17 - 6
ansible/site.yml

@@ -1,11 +1,22 @@
-# automated-office/ansible/site.yml
 ---
 ---
 - name: Deploy Automated Office
 - name: Deploy Automated Office
-  hosts: all
+  hosts: localhost
   become: true
   become: true
   
   
+  pre_tasks:
+    - debug:
+        msg: 
+          - "Client secret defined: {{ client_secret is defined }}"
+          - "Client secret length: {{ client_secret | default('') | length }}"
+        verbosity: 1
+
+    - name: "Verify required variables"
+      fail:
+        msg: "Client secret is not set or empty"
+      when: client_secret is not defined or client_secret | default('') | trim == ''
+
   roles:
   roles:
-    - common
-    - docker
-    - nginx
-    - services
+    - role: common
+    - role: docker
+    - role: nginx
+    - role: services

BIN
config/credentials/credentials_2024-12-11_18-28-21.txt.gpg


+ 22 - 8
config/nginx/sites-available/keycloak

@@ -5,7 +5,7 @@ upstream keycloak_upstream {
 server {
 server {
     listen 80;
     listen 80;
     server_name auth.mrx8086.com;
     server_name auth.mrx8086.com;
-    
+
     # Redirect HTTP to HTTPS
     # Redirect HTTP to HTTPS
     return 301 https://$host$request_uri;
     return 301 https://$host$request_uri;
 }
 }
@@ -30,35 +30,49 @@ server {
     add_header X-XSS-Protection "1; mode=block" always;
     add_header X-XSS-Protection "1; mode=block" always;
     add_header X-Frame-Options SAMEORIGIN always;
     add_header X-Frame-Options SAMEORIGIN always;
     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
-    
+
     # Content Security Policy
     # Content Security Policy
     add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-src 'self'; frame-ancestors 'self'; connect-src 'self'" always;
     add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-src 'self'; frame-ancestors 'self'; connect-src 'self'" always;
 
 
-    # Proxy settings for all locations
+    # Proxy settings - Added X-Forwarded headers here to apply to all proxied locations
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto $scheme;
     proxy_set_header X-Forwarded-Proto $scheme;
+    proxy_set_header X-Forwarded-Ssl on; # Optional, but explicit
+    proxy_set_header X-Forwarded-Port $server_port;
     proxy_set_header X-Forwarded-Host $host;
     proxy_set_header X-Forwarded-Host $host;
-    proxy_set_header X-Forwarded-Port 443;
     proxy_set_header Host $host;
     proxy_set_header Host $host;
     proxy_http_version 1.1;
     proxy_http_version 1.1;
-    
+
     # Root location for the main application
     # Root location for the main application
     location / {
     location / {
         proxy_pass http://keycloak_upstream;
         proxy_pass http://keycloak_upstream;
     }
     }
 
 
+    # Specific location for the token endpoint
+    location ~ ^/auth/realms/[^/]+/protocol/openid-connect/token$ {
+        proxy_pass http://keycloak_upstream;
+        proxy_buffer_size 128k;
+        proxy_buffers 4 256k;
+        proxy_busy_buffers_size 256k;
+        # WebSocket support (likely not needed for token endpoint, but keeping for consistency)
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        # Timeouts
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 60s;
+        proxy_read_timeout 60s;
+    }
+
     # Keycloak required paths
     # Keycloak required paths
     location /realms/ {
     location /realms/ {
         proxy_pass http://keycloak_upstream;
         proxy_pass http://keycloak_upstream;
         proxy_buffer_size 128k;
         proxy_buffer_size 128k;
         proxy_buffers 4 256k;
         proxy_buffers 4 256k;
         proxy_busy_buffers_size 256k;
         proxy_busy_buffers_size 256k;
-        
         # WebSocket support
         # WebSocket support
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
         proxy_set_header Connection "upgrade";
-        
         # Timeouts
         # Timeouts
         proxy_connect_timeout 60s;
         proxy_connect_timeout 60s;
         proxy_send_timeout 60s;
         proxy_send_timeout 60s;
@@ -67,7 +81,7 @@ server {
 
 
     location /resources/ {
     location /resources/ {
         proxy_pass http://keycloak_upstream;
         proxy_pass http://keycloak_upstream;
-        
+
         # Cache settings for static resources
         # Cache settings for static resources
         proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
         proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
         proxy_cache_valid 200 1d;
         proxy_cache_valid 200 1d;

+ 18 - 23
config/nginx/sites-available/nextcloud

@@ -1,12 +1,10 @@
 upstream nextcloud_upstream {
 upstream nextcloud_upstream {
-    server 172.19.0.3:80;  # Die IP wird später durch die tatsächliche Container-IP ersetzt
+    server 172.19.0.3:80;  # SICHERSTELLEN, DASS DIES DIE KORREKTE IP IST
 }
 }
 
 
 server {
 server {
     listen 80;
     listen 80;
     server_name cloud.mrx8086.com;
     server_name cloud.mrx8086.com;
-    
-    # Redirect HTTP to HTTPS
     return 301 https://$host$request_uri;
     return 301 https://$host$request_uri;
 }
 }
 
 
@@ -14,27 +12,19 @@ server {
     listen 443 ssl;
     listen 443 ssl;
     server_name cloud.mrx8086.com;
     server_name cloud.mrx8086.com;
 
 
-    # SSL Configuration
+    # SSL Configuration (wie zuvor)
     ssl_certificate /etc/nginx/ssl/mrx8086.com/fullchain.pem;
     ssl_certificate /etc/nginx/ssl/mrx8086.com/fullchain.pem;
     ssl_certificate_key /etc/nginx/ssl/mrx8086.com/privkey.pem;
     ssl_certificate_key /etc/nginx/ssl/mrx8086.com/privkey.pem;
-    ssl_session_timeout 1d;
-    ssl_session_tickets off;
+    # ... weitere SSL-Einstellungen ...
 
 
-    # Modern SSL configuration
-    ssl_protocols TLSv1.2 TLSv1.3;
-    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
-    ssl_prefer_server_ciphers off;
-
-    # Security headers
+    # Security headers (wie zuvor)
     add_header X-Content-Type-Options nosniff always;
     add_header X-Content-Type-Options nosniff always;
     add_header X-XSS-Protection "1; mode=block" always;
     add_header X-XSS-Protection "1; mode=block" always;
     add_header X-Frame-Options SAMEORIGIN always;
     add_header X-Frame-Options SAMEORIGIN always;
     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
-    
-    # Content Security Policy für Nextcloud
     add_header Content-Security-Policy "frame-ancestors 'self'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self';" always;
     add_header Content-Security-Policy "frame-ancestors 'self'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self';" always;
 
 
-    # Proxy settings
+    # Proxy settings (wie zuvor)
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto $scheme;
     proxy_set_header X-Forwarded-Proto $scheme;
@@ -43,31 +33,36 @@ server {
     proxy_set_header Host $host;
     proxy_set_header Host $host;
     proxy_http_version 1.1;
     proxy_http_version 1.1;
 
 
-    # Nextcloud specific settings
+    # Nextcloud specific settings (wie zuvor)
     client_max_body_size 512M;
     client_max_body_size 512M;
     fastcgi_buffers 64 4K;
     fastcgi_buffers 64 4K;
-    
+
+    # Expliziter location-Block für den OpenID Connect Callback
+    location /apps/sociallogin/custom_oidc/keycloak {
+        proxy_pass http://nextcloud_upstream;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+
     # Root location
     # Root location
     location / {
     location / {
         proxy_pass http://nextcloud_upstream;
         proxy_pass http://nextcloud_upstream;
-        
-        # WebSocket support
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
         proxy_set_header Connection "upgrade";
-        
-        # Timeouts
         proxy_connect_timeout 60s;
         proxy_connect_timeout 60s;
         proxy_send_timeout 60s;
         proxy_send_timeout 60s;
         proxy_read_timeout 60s;
         proxy_read_timeout 60s;
     }
     }
 
 
-    # Block sensitive paths
+    # Block sensitive paths (wie zuvor)
     location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) {
     location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) {
         deny all;
         deny all;
         return 404;
         return 404;
     }
     }
 
 
-    # Deny access to hidden files
+    # Deny access to hidden files (wie zuvor)
     location ~ /\. {
     location ~ /\. {
         deny all;
         deny all;
         return 404;
         return 404;

+ 25 - 8
docker/docker-compose.yml

@@ -34,8 +34,10 @@ services:
       - keycloak-network
       - keycloak-network
     depends_on:
     depends_on:
       - keycloak-db
       - keycloak-db
+    extra_hosts:
+      - "cloud.mrx8086.com:172.23.171.133"         
     healthcheck:
     healthcheck:
-      test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
+      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
       interval: 30s
       interval: 30s
       timeout: 10s
       timeout: 10s
       retries: 3
       retries: 3
@@ -48,7 +50,7 @@ services:
       POSTGRES_USER: ${KC_DB_USERNAME}
       POSTGRES_USER: ${KC_DB_USERNAME}
       POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
       POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
     volumes:
     volumes:
-      - ../data/keycloak/db:/var/lib/postgresql/data
+      - ../data/keycloak-db:/var/lib/postgresql/data
     networks:
     networks:
       - keycloak-network
       - keycloak-network
     restart: unless-stopped
     restart: unless-stopped
@@ -63,6 +65,13 @@ services:
     image: nextcloud:latest
     image: nextcloud:latest
     container_name: nextcloud
     container_name: nextcloud
     restart: unless-stopped
     restart: unless-stopped
+    ports:
+        - "8081:80"
+    volumes:
+      - ../data/nextcloud:/var/www/html
+      - ../config/nextcloud/config:/var/www/html/config
+      - ../config/nextcloud/custom_apps:/var/www/html/custom_apps
+      - ../data/nextcloud-db:/var/lib/mysql
     environment:
     environment:
       - MYSQL_HOST=nextcloud-db
       - MYSQL_HOST=nextcloud-db
       - MYSQL_DATABASE=nextcloud
       - MYSQL_DATABASE=nextcloud
@@ -74,16 +83,24 @@ services:
       - OVERWRITEPROTOCOL=https
       - OVERWRITEPROTOCOL=https
       - OVERWRITEHOST=cloud.mrx8086.com
       - OVERWRITEHOST=cloud.mrx8086.com
       - OVERWRITEWEBROOT=/
       - OVERWRITEWEBROOT=/
-      - TRUSTED_PROXIES=172.18.0.0/16
-    volumes:
-      - ../data/nextcloud:/var/www/html
-      - ../config/nextcloud/config:/var/www/html/config
-      - ../config/nextcloud/custom_apps:/var/www/html/custom_apps
-      - ../config/nextcloud/data:/var/www/html/data
+      - TRUSTED_PROXIES=172.19.0.0/16
+      - NEXTCLOUD_URL=https://cloud.mrx8086.com
+      - NEXTCLOUD_DEBUG=1
+      - NEXTCLOUD_CONFIG_CUSTOM_SCOPE="openid profile groups-nextcloud"
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:80/"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
     networks:
     networks:
       - nextcloud-network
       - nextcloud-network
     depends_on:
     depends_on:
       - nextcloud-db
       - nextcloud-db
+    extra_hosts:
+      - "auth.mrx8086.com:172.23.171.133"      
+    dns:
+      - 8.8.8.8
+      - 8.8.4.4
 
 
   nextcloud-db:
   nextcloud-db:
     image: mariadb:10.6
     image: mariadb:10.6

+ 45 - 3
docs/context/scripts/setup_environment.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
 #!/bin/bash
+set -e
 
 
 # Ensure we're in the project root directory
 # Ensure we're in the project root directory
 PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
 PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -8,7 +9,8 @@ cd "${PROJECT_ROOT}"
 CREDENTIALS_DIR="config/credentials"
 CREDENTIALS_DIR="config/credentials"
 DOCKER_DIR="docker"
 DOCKER_DIR="docker"
 KEYCLOAK_SCRIPTS_DIR="scripts/setup/keycloak"
 KEYCLOAK_SCRIPTS_DIR="scripts/setup/keycloak"
-
+ANSIBLE_PLAYBOOK="ansible/site.yml"
+ANSIBLE_INVENTORY="ansible/inventory/staging/hosts"
 
 
 # Create necessary directories
 # Create necessary directories
 mkdir -p "${CREDENTIALS_DIR}"
 mkdir -p "${CREDENTIALS_DIR}"
@@ -33,7 +35,7 @@ NEXTCLOUD_DB_USER=$(generate_password)
 NEXTCLOUD_DB_PASSWORD=$(generate_password)
 NEXTCLOUD_DB_PASSWORD=$(generate_password)
 NEXTCLOUD_ADMIN_USER=$(generate_password)
 NEXTCLOUD_ADMIN_USER=$(generate_password)
 NEXTCLOUD_ADMIN_PASSWORD=$(generate_password)
 NEXTCLOUD_ADMIN_PASSWORD=$(generate_password)
-
+KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(generate_password)
 
 
 # Create .env file in docker directory
 # Create .env file in docker directory
 cat > "${DOCKER_DIR}/.env" << EOL
 cat > "${DOCKER_DIR}/.env" << EOL
@@ -64,6 +66,7 @@ PAPERLESS_CLIENT_ID=paperless
 NODERED_CLIENT_ID=nodered
 NODERED_CLIENT_ID=nodered
 TESTADMIN_PASSWORD=${TESTADMIN_PASSWORD}
 TESTADMIN_PASSWORD=${TESTADMIN_PASSWORD}
 TESTUSER_PASSWORD=${TESTUSER_PASSWORD}
 TESTUSER_PASSWORD=${TESTUSER_PASSWORD}
+KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}
 EOL
 EOL
 
 
 # Create encrypted credentials documentation
 # Create encrypted credentials documentation
@@ -77,8 +80,10 @@ Password: ${KEYCLOAK_ADMIN_PASSWORD}
 Keycloak Database Credentials:
 Keycloak Database Credentials:
 Username: keycloak
 Username: keycloak
 Password: ${KC_DB_PASSWORD}
 Password: ${KC_DB_PASSWORD}
+
 Test Admin Credentials:
 Test Admin Credentials:
 Password: ${TESTADMIN_PASSWORD}
 Password: ${TESTADMIN_PASSWORD}
+
 Test User Credentials:
 Test User Credentials:
 Password: ${TESTUSER_PASSWORD}
 Password: ${TESTUSER_PASSWORD}
 
 
@@ -86,6 +91,7 @@ Nextcloud Database Credentials:
 Root Password: ${NEXTCLOUD_DB_ROOT_PASSWORD}
 Root Password: ${NEXTCLOUD_DB_ROOT_PASSWORD}
 User: ${NEXTCLOUD_DB_USER}
 User: ${NEXTCLOUD_DB_USER}
 Password: ${NEXTCLOUD_DB_PASSWORD}
 Password: ${NEXTCLOUD_DB_PASSWORD}
+
 Nextcloud Admin Credentials:
 Nextcloud Admin Credentials:
 Username: ${NEXTCLOUD_ADMIN_USER}
 Username: ${NEXTCLOUD_ADMIN_USER}
 Password: ${NEXTCLOUD_ADMIN_PASSWORD}
 Password: ${NEXTCLOUD_ADMIN_PASSWORD}
@@ -101,4 +107,40 @@ echo ".env file for docker-compose has been created in: ${DOCKER_DIR}/.env"
 echo ".env file for setup_realm.js has been created in: ${KEYCLOAK_SCRIPTS_DIR}/.env"
 echo ".env file for setup_realm.js has been created in: ${KEYCLOAK_SCRIPTS_DIR}/.env"
 echo ""
 echo ""
 echo "To view credentials, use:"
 echo "To view credentials, use:"
-echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
+echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
+
+echo ">>> Nextcloud Konfiguration..."
+
+# Verify if variable is set from earlier in the script
+echo ">>> Debug: Checking original variable..."
+echo ">>> Debug: KEYCLOAK_NEXTCLOUD_CLIENT_SECRET = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
+
+# Try reading from .env file if variable is empty
+if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
+    echo ">>> Debug: Variable is empty, trying to read from .env file..."
+    KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(grep KEYCLOAK_NEXTCLOUD_CLIENT_SECRET "${KEYCLOAK_SCRIPTS_DIR}/.env" | cut -d '=' -f2)
+    echo ">>> Debug: Value from .env file = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
+fi
+
+# Ensure we have a value
+if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
+    echo ">>> Error: Could not get client secret value"
+    exit 1
+fi
+
+# Escape special characters in the secret for JSON
+ESCAPED_SECRET=$(echo "$KEYCLOAK_NEXTCLOUD_CLIENT_SECRET" | sed 's/["\]/\\&/g')
+echo ">>> Debug: Escaped secret = $ESCAPED_SECRET"
+
+# Create the extra vars
+EXTRA_VARS="{\"client_secret\": \"$ESCAPED_SECRET\"}"
+echo ">>> Debug: Extra vars = $EXTRA_VARS"
+
+# Run Ansible with the extra vars
+sudo ansible-playbook \
+    -i "$ANSIBLE_INVENTORY" \
+    "$ANSIBLE_PLAYBOOK" \
+    --extra-vars "$EXTRA_VARS" \
+    -v
+
+echo ">>> Fertig"

+ 1634 - 0
git_diff.txt

@@ -0,0 +1,1634 @@
+diff --git a/.gitignore b/.gitignore
+index 7e57cb3..8994b5d 100644
+--- a/.gitignore
++++ b/.gitignore
+@@ -1,2 +1,3 @@
+ data/
+-config/nextcloud/
+\ No newline at end of file
++config/credentials
++config/nextcloud/
+diff --git a/ansible/site.yml b/ansible/site.yml
+index 22ea670..ded0d95 100644
+--- a/ansible/site.yml
++++ b/ansible/site.yml
+@@ -1,11 +1,22 @@
+-# automated-office/ansible/site.yml
+ ---
+ - name: Deploy Automated Office
+-  hosts: all
++  hosts: localhost
+   become: true
+   
++  pre_tasks:
++    - debug:
++        msg: 
++          - "Client secret defined: {{ client_secret is defined }}"
++          - "Client secret length: {{ client_secret | default('') | length }}"
++        verbosity: 1
++
++    - name: "Verify required variables"
++      fail:
++        msg: "Client secret is not set or empty"
++      when: client_secret is not defined or client_secret | default('') | trim == ''
++
+   roles:
+-    - common
+-    - docker
+-    - nginx
+-    - services
+\ No newline at end of file
++    - role: common
++    - role: docker
++    - role: nginx
++    - role: services
+\ No newline at end of file
+diff --git a/config/credentials/credentials_2024-12-11_18-28-21.txt.gpg b/config/credentials/credentials_2024-12-11_18-28-21.txt.gpg
+deleted file mode 100644
+index b43471e..0000000
+Binary files a/config/credentials/credentials_2024-12-11_18-28-21.txt.gpg and /dev/null differ
+diff --git a/config/nginx/sites-available/keycloak b/config/nginx/sites-available/keycloak
+index 2c2181a..cf437a2 100644
+--- a/config/nginx/sites-available/keycloak
++++ b/config/nginx/sites-available/keycloak
+@@ -5,7 +5,7 @@ upstream keycloak_upstream {
+ server {
+     listen 80;
+     server_name auth.mrx8086.com;
+-    
++
+     # Redirect HTTP to HTTPS
+     return 301 https://$host$request_uri;
+ }
+@@ -30,35 +30,49 @@ server {
+     add_header X-XSS-Protection "1; mode=block" always;
+     add_header X-Frame-Options SAMEORIGIN always;
+     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+-    
++
+     # Content Security Policy
+     add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-src 'self'; frame-ancestors 'self'; connect-src 'self'" always;
+ 
+-    # Proxy settings for all locations
++    # Proxy settings - Added X-Forwarded headers here to apply to all proxied locations
+     proxy_set_header X-Real-IP $remote_addr;
+     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+     proxy_set_header X-Forwarded-Proto $scheme;
++    proxy_set_header X-Forwarded-Ssl on; # Optional, but explicit
++    proxy_set_header X-Forwarded-Port $server_port;
+     proxy_set_header X-Forwarded-Host $host;
+-    proxy_set_header X-Forwarded-Port 443;
+     proxy_set_header Host $host;
+     proxy_http_version 1.1;
+-    
++
+     # Root location for the main application
+     location / {
+         proxy_pass http://keycloak_upstream;
+     }
+ 
++    # Specific location for the token endpoint
++    location ~ ^/auth/realms/[^/]+/protocol/openid-connect/token$ {
++        proxy_pass http://keycloak_upstream;
++        proxy_buffer_size 128k;
++        proxy_buffers 4 256k;
++        proxy_busy_buffers_size 256k;
++        # WebSocket support (likely not needed for token endpoint, but keeping for consistency)
++        proxy_set_header Upgrade $http_upgrade;
++        proxy_set_header Connection "upgrade";
++        # Timeouts
++        proxy_connect_timeout 60s;
++        proxy_send_timeout 60s;
++        proxy_read_timeout 60s;
++    }
++
+     # Keycloak required paths
+     location /realms/ {
+         proxy_pass http://keycloak_upstream;
+         proxy_buffer_size 128k;
+         proxy_buffers 4 256k;
+         proxy_busy_buffers_size 256k;
+-        
+         # WebSocket support
+         proxy_set_header Upgrade $http_upgrade;
+         proxy_set_header Connection "upgrade";
+-        
+         # Timeouts
+         proxy_connect_timeout 60s;
+         proxy_send_timeout 60s;
+@@ -67,7 +81,7 @@ server {
+ 
+     location /resources/ {
+         proxy_pass http://keycloak_upstream;
+-        
++
+         # Cache settings for static resources
+         proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
+         proxy_cache_valid 200 1d;
+diff --git a/config/nginx/sites-available/nextcloud b/config/nginx/sites-available/nextcloud
+index c384e10..8357df3 100644
+--- a/config/nginx/sites-available/nextcloud
++++ b/config/nginx/sites-available/nextcloud
+@@ -1,12 +1,10 @@
+ upstream nextcloud_upstream {
+-    server 172.19.0.3:80;  # Die IP wird später durch die tatsächliche Container-IP ersetzt
++    server 172.19.0.3:80;  # SICHERSTELLEN, DASS DIES DIE KORREKTE IP IST
+ }
+ 
+ server {
+     listen 80;
+     server_name cloud.mrx8086.com;
+-    
+-    # Redirect HTTP to HTTPS
+     return 301 https://$host$request_uri;
+ }
+ 
+@@ -14,27 +12,19 @@ server {
+     listen 443 ssl;
+     server_name cloud.mrx8086.com;
+ 
+-    # SSL Configuration
++    # SSL Configuration (wie zuvor)
+     ssl_certificate /etc/nginx/ssl/mrx8086.com/fullchain.pem;
+     ssl_certificate_key /etc/nginx/ssl/mrx8086.com/privkey.pem;
+-    ssl_session_timeout 1d;
+-    ssl_session_tickets off;
++    # ... weitere SSL-Einstellungen ...
+ 
+-    # Modern SSL configuration
+-    ssl_protocols TLSv1.2 TLSv1.3;
+-    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+-    ssl_prefer_server_ciphers off;
+-
+-    # Security headers
++    # Security headers (wie zuvor)
+     add_header X-Content-Type-Options nosniff always;
+     add_header X-XSS-Protection "1; mode=block" always;
+     add_header X-Frame-Options SAMEORIGIN always;
+     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+-    
+-    # Content Security Policy für Nextcloud
+     add_header Content-Security-Policy "frame-ancestors 'self'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self';" always;
+ 
+-    # Proxy settings
++    # Proxy settings (wie zuvor)
+     proxy_set_header X-Real-IP $remote_addr;
+     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+     proxy_set_header X-Forwarded-Proto $scheme;
+@@ -43,31 +33,36 @@ server {
+     proxy_set_header Host $host;
+     proxy_http_version 1.1;
+ 
+-    # Nextcloud specific settings
++    # Nextcloud specific settings (wie zuvor)
+     client_max_body_size 512M;
+     fastcgi_buffers 64 4K;
+-    
++
++    # Expliziter location-Block für den OpenID Connect Callback
++    location /apps/sociallogin/custom_oidc/keycloak {
++        proxy_pass http://nextcloud_upstream;
++        proxy_set_header Host $host;
++        proxy_set_header X-Real-IP $remote_addr;
++        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++        proxy_set_header X-Forwarded-Proto $scheme;
++    }
++
+     # Root location
+     location / {
+         proxy_pass http://nextcloud_upstream;
+-        
+-        # WebSocket support
+         proxy_set_header Upgrade $http_upgrade;
+         proxy_set_header Connection "upgrade";
+-        
+-        # Timeouts
+         proxy_connect_timeout 60s;
+         proxy_send_timeout 60s;
+         proxy_read_timeout 60s;
+     }
+ 
+-    # Block sensitive paths
++    # Block sensitive paths (wie zuvor)
+     location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) {
+         deny all;
+         return 404;
+     }
+ 
+-    # Deny access to hidden files
++    # Deny access to hidden files (wie zuvor)
+     location ~ /\. {
+         deny all;
+         return 404;
+diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
+index 0163334..ccbcd3b 100644
+--- a/docker/docker-compose.yml
++++ b/docker/docker-compose.yml
+@@ -34,8 +34,10 @@ services:
+       - keycloak-network
+     depends_on:
+       - keycloak-db
++    extra_hosts:
++      - "cloud.mrx8086.com:172.23.171.133"         
+     healthcheck:
+-      test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
++      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
+       interval: 30s
+       timeout: 10s
+       retries: 3
+@@ -48,7 +50,7 @@ services:
+       POSTGRES_USER: ${KC_DB_USERNAME}
+       POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
+     volumes:
+-      - ../data/keycloak/db:/var/lib/postgresql/data
++      - ../data/keycloak-db:/var/lib/postgresql/data
+     networks:
+       - keycloak-network
+     restart: unless-stopped
+@@ -63,6 +65,13 @@ services:
+     image: nextcloud:latest
+     container_name: nextcloud
+     restart: unless-stopped
++    ports:
++        - "8081:80"
++    volumes:
++      - ../data/nextcloud:/var/www/html
++      - ../config/nextcloud/config:/var/www/html/config
++      - ../config/nextcloud/custom_apps:/var/www/html/custom_apps
++      - ../data/nextcloud-db:/var/lib/mysql
+     environment:
+       - MYSQL_HOST=nextcloud-db
+       - MYSQL_DATABASE=nextcloud
+@@ -74,16 +83,24 @@ services:
+       - OVERWRITEPROTOCOL=https
+       - OVERWRITEHOST=cloud.mrx8086.com
+       - OVERWRITEWEBROOT=/
+-      - TRUSTED_PROXIES=172.18.0.0/16
+-    volumes:
+-      - ../data/nextcloud:/var/www/html
+-      - ../config/nextcloud/config:/var/www/html/config
+-      - ../config/nextcloud/custom_apps:/var/www/html/custom_apps
+-      - ../config/nextcloud/data:/var/www/html/data
++      - TRUSTED_PROXIES=172.19.0.0/16
++      - NEXTCLOUD_URL=https://cloud.mrx8086.com
++      - NEXTCLOUD_DEBUG=1
++      - NEXTCLOUD_CONFIG_CUSTOM_SCOPE="openid profile groups-nextcloud"
++    healthcheck:
++      test: ["CMD", "curl", "-f", "http://localhost:80/"]
++      interval: 30s
++      timeout: 10s
++      retries: 3
+     networks:
+       - nextcloud-network
+     depends_on:
+       - nextcloud-db
++    extra_hosts:
++      - "auth.mrx8086.com:172.23.171.133"      
++    dns:
++      - 8.8.8.8
++      - 8.8.4.4
+ 
+   nextcloud-db:
+     image: mariadb:10.6
+diff --git a/docs/context/scripts/setup_environment.sh b/docs/context/scripts/setup_environment.sh
+index 9a1b2da..1244eb8 100644
+--- a/docs/context/scripts/setup_environment.sh
++++ b/docs/context/scripts/setup_environment.sh
+@@ -1,4 +1,5 @@
+ #!/bin/bash
++set -e
+ 
+ # Ensure we're in the project root directory
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+@@ -8,7 +9,8 @@ cd "${PROJECT_ROOT}"
+ CREDENTIALS_DIR="config/credentials"
+ DOCKER_DIR="docker"
+ KEYCLOAK_SCRIPTS_DIR="scripts/setup/keycloak"
+-
++ANSIBLE_PLAYBOOK="ansible/site.yml"
++ANSIBLE_INVENTORY="ansible/inventory/staging/hosts"
+ 
+ # Create necessary directories
+ mkdir -p "${CREDENTIALS_DIR}"
+@@ -33,7 +35,7 @@ NEXTCLOUD_DB_USER=$(generate_password)
+ NEXTCLOUD_DB_PASSWORD=$(generate_password)
+ NEXTCLOUD_ADMIN_USER=$(generate_password)
+ NEXTCLOUD_ADMIN_PASSWORD=$(generate_password)
+-
++KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(generate_password)
+ 
+ # Create .env file in docker directory
+ cat > "${DOCKER_DIR}/.env" << EOL
+@@ -64,6 +66,7 @@ PAPERLESS_CLIENT_ID=paperless
+ NODERED_CLIENT_ID=nodered
+ TESTADMIN_PASSWORD=${TESTADMIN_PASSWORD}
+ TESTUSER_PASSWORD=${TESTUSER_PASSWORD}
++KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}
+ EOL
+ 
+ # Create encrypted credentials documentation
+@@ -77,8 +80,10 @@ Password: ${KEYCLOAK_ADMIN_PASSWORD}
+ Keycloak Database Credentials:
+ Username: keycloak
+ Password: ${KC_DB_PASSWORD}
++
+ Test Admin Credentials:
+ Password: ${TESTADMIN_PASSWORD}
++
+ Test User Credentials:
+ Password: ${TESTUSER_PASSWORD}
+ 
+@@ -86,6 +91,7 @@ Nextcloud Database Credentials:
+ Root Password: ${NEXTCLOUD_DB_ROOT_PASSWORD}
+ User: ${NEXTCLOUD_DB_USER}
+ Password: ${NEXTCLOUD_DB_PASSWORD}
++
+ Nextcloud Admin Credentials:
+ Username: ${NEXTCLOUD_ADMIN_USER}
+ Password: ${NEXTCLOUD_ADMIN_PASSWORD}
+@@ -101,4 +107,40 @@ echo ".env file for docker-compose has been created in: ${DOCKER_DIR}/.env"
+ echo ".env file for setup_realm.js has been created in: ${KEYCLOAK_SCRIPTS_DIR}/.env"
+ echo ""
+ echo "To view credentials, use:"
+-echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
+\ No newline at end of file
++echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
++
++echo ">>> Nextcloud Konfiguration..."
++
++# Verify if variable is set from earlier in the script
++echo ">>> Debug: Checking original variable..."
++echo ">>> Debug: KEYCLOAK_NEXTCLOUD_CLIENT_SECRET = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
++
++# Try reading from .env file if variable is empty
++if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
++    echo ">>> Debug: Variable is empty, trying to read from .env file..."
++    KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(grep KEYCLOAK_NEXTCLOUD_CLIENT_SECRET "${KEYCLOAK_SCRIPTS_DIR}/.env" | cut -d '=' -f2)
++    echo ">>> Debug: Value from .env file = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
++fi
++
++# Ensure we have a value
++if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
++    echo ">>> Error: Could not get client secret value"
++    exit 1
++fi
++
++# Escape special characters in the secret for JSON
++ESCAPED_SECRET=$(echo "$KEYCLOAK_NEXTCLOUD_CLIENT_SECRET" | sed 's/["\]/\\&/g')
++echo ">>> Debug: Escaped secret = $ESCAPED_SECRET"
++
++# Create the extra vars
++EXTRA_VARS="{\"client_secret\": \"$ESCAPED_SECRET\"}"
++echo ">>> Debug: Extra vars = $EXTRA_VARS"
++
++# Run Ansible with the extra vars
++sudo ansible-playbook \
++    -i "$ANSIBLE_INVENTORY" \
++    "$ANSIBLE_PLAYBOOK" \
++    --extra-vars "$EXTRA_VARS" \
++    -v
++
++echo ">>> Fertig"
+\ No newline at end of file
+diff --git a/scripts/install/setup_environment.sh b/scripts/install/setup_environment.sh
+old mode 100644
+new mode 100755
+index aba63dc..5b7bc0e
+--- a/scripts/install/setup_environment.sh
++++ b/scripts/install/setup_environment.sh
+@@ -1,31 +1,163 @@
+ #!/bin/bash
++set -e
+ 
+ # Ensure we're in the project root directory
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+-cd "${PROJECT_ROOT}"
+ 
+ # Define directories relative to project root
+-CREDENTIALS_DIR="config/credentials"
+-DOCKER_DIR="docker"
++CREDENTIALS_DIR="${PROJECT_ROOT}/config/credentials"
++DOCKER_DIR="${PROJECT_ROOT}/docker"
++KEYCLOAK_SETUP_DIR="${PROJECT_ROOT}/scripts/setup/keycloak"
++ANSIBLE_PLAYBOOK="${PROJECT_ROOT}/ansible/site.yml"
++ANSIBLE_INVENTORY="${PROJECT_ROOT}/ansible/inventory/staging/hosts"
++NEXTCLOUD_DATA_DIR="${PROJECT_ROOT}/data/nextcloud/data"
++TEMP_FILE=$(mktemp)
++KEYCLOAK_DB_DIR="${PROJECT_ROOT}/data/keycloak-db"
+ 
+ # Create necessary directories
+-mkdir -p "${CREDENTIALS_DIR}"
+-mkdir -p "${DOCKER_DIR}"
++sudo mkdir -p "${CREDENTIALS_DIR}"
++sudo mkdir -p "${DOCKER_DIR}"
++sudo mkdir -p "${KEYCLOAK_SETUP_DIR}"
++
++# Initialize password variables
++KEYCLOAK_ADMIN_PASSWORD=""
++KC_DB_PASSWORD=""
++TESTADMIN_PASSWORD=""
++TESTUSER_PASSWORD=""
++TESTSERVICEUSER_PASSWORD=""
++KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=""
++
++# Function to read a password from a .env file
++read_password_from_env() {
++    local env_file="$1"
++    local variable_name="$2"
++    if [ -f "$env_file" ]; then
++        grep "^${variable_name}=" "$env_file" | cut -d '=' -f2
++    fi
++}
+ 
+ # Function to generate secure passwords
+ generate_password() {
+-    openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24
++    openssl rand -base64 32
++}
++
++# Function to generate password if empty
++generate_password_if_empty() {
++    local variable_name="$1"
++    eval "local value=\$$variable_name"
++    if [ -z "$value" ]; then
++        eval "$variable_name=\"$(generate_password)\""
++        echo ">>> Generiertes Passwort für: $variable_name"
++    fi
++}
++
++# Function to create .env file
++create_env_file() {
++    local env_file="$1"
++    local content="$2"
++    if [ ! -f "$env_file" ]; then
++        echo "$content" > "$env_file"
++        echo ">>> .env file created: $env_file"
++    else
++        echo ">>> .env file already exists: $env_file"
++    fi
+ }
+ 
++echo ">>> Überprüfe bestehende .env Dateien und lese Passwörter..."
++
++# Try reading passwords from existing .env files
++if [ -f "$DOCKER_DIR/.env" ]; then
++    KC_DB_PASSWORD=$(read_password_from_env "$DOCKER_DIR/.env" "KC_DB_PASSWORD")
++    KEYCLOAK_ADMIN_PASSWORD=$(read_password_from_env "$DOCKER_DIR/.env" "KEYCLOAK_ADMIN_PASSWORD")
++fi
++
++if [ -f "$KEYCLOAK_SETUP_DIR/.env" ]; then
++    KEYCLOAK_ADMIN_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "KEYCLOAK_ADMIN_PASSWORD") # Überschreibt ggf. den Wert aus docker/.env
++    TESTADMIN_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTADMIN_PASSWORD")
++    TESTUSER_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTUSER_PASSWORD")
++    TESTSERVICEUSER_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTSERVICEUSER_PASSWORD")
++    KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "KEYCLOAK_NEXTCLOUD_CLIENT_SECRET")
++fi
++
++echo ">>> Generiere neue Passwörter für fehlende Werte..."
++
++# Generate passwords if they are still empty
++generate_password_if_empty KEYCLOAK_ADMIN_PASSWORD
++generate_password_if_empty KC_DB_PASSWORD
++generate_password_if_empty TESTADMIN_PASSWORD
++generate_password_if_empty TESTUSER_PASSWORD
++generate_password_if_empty TESTSERVICEUSER_PASSWORD
++generate_password_if_empty KEYCLOAK_NEXTCLOUD_CLIENT_SECRET
++
+ # Date for documentation
+ SETUP_DATE=$(date '+%Y-%m-%d_%H-%M-%S')
+ 
+-# Generate passwords
+-KEYCLOAK_ADMIN_PASSWORD=$(generate_password)
+-KC_DB_PASSWORD=$(generate_password)
++# Create credentials content
++CREDENTIALS_CONTENT=$(cat <<EOL
++Setup Date: ${SETUP_DATE}
++
++Keycloak Admin Credentials:
++Username: admin
++Password: ${KEYCLOAK_ADMIN_PASSWORD}
++
++Keycloak Database Credentials:
++Username: keycloak
++Password: ${KC_DB_PASSWORD}
++
++Test User Credentials:
++Admin Password: ${TESTADMIN_PASSWORD}
++User Password: ${TESTUSER_PASSWORD}
++Service User Password: ${TESTSERVICEUSER_PASSWORD}
++Nextcloud Client Secret: ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}
++
++EOL
++)
++
++# Store credentials hash
++CREDENTIALS_HASH=$(echo "$CREDENTIALS_CONTENT" | sha256sum | awk '{print $1}')
++echo "$CREDENTIALS_HASH" > "${CREDENTIALS_DIR}/credentials_hash.txt"
++echo ">>> Credentials hash stored in: ${CREDENTIALS_DIR}/credentials_hash.txt"
++
++# Set GPG PASSPHRASE
++export GPG_PASSPHRASE=$(generate_password)
++# Set GPG agent environment variable
++export GPG_TTY=$(tty)
++
++echo ">>> Trying openssl encryption first"
++# Alternative Verschlüsselung mit Openssl
++echo "$CREDENTIALS_CONTENT" > "$TEMP_FILE"
++          if openssl enc -aes-256-cbc -pbkdf2 -salt -in "$TEMP_FILE" -out "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.enc" -k "$GPG_PASSPHRASE" ; then
++             echo ">>> Credentials encrypted successfully using openssl"
++             mv  "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.enc" "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
++        else
++           echo ">>> Openssl encryption failed, trying gpg"
++
++          # Attempt to kill existing gpg agent
++          gpgconf --kill gpg-agent 2>/dev/null
++          echo ">>> Attempting to manually start gpg-agent with pinentry-curses"
++          gpg-agent --daemon --pinentry-program /usr/bin/pinentry-curses
++          gpg-connect-agent /bye 2>/dev/null
++          eval $(gpg-agent --daemon)
++          gpg-connect-agent updatestartuptty /bye 2>/dev/null
++
++          # Attempt to encrypt credentials using GPG with error handling
++          if echo "$CREDENTIALS_CONTENT" | gpg --symmetric --cipher-algo AES256 -vvv -o "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" ; then
++                echo ">>> Credentials encrypted successfully using gpg."
++          else
++             echo ">>> GPG encryption failed. Attempting GPG encryption with password workaround."
++                # Attempt encryption with passphrase workaround
++                if echo "$CREDENTIALS_CONTENT" | gpg --batch --passphrase "$GPG_PASSPHRASE" --symmetric --cipher-algo AES256 -vvv -o "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"; then
++                      echo ">>> Credentials encrypted successfully using gpg with passphrase workaround."
++                else
++                   echo ">>> GPG encryption with passphrase workaround failed"
++                   exit 1
++                fi
++          fi
++       fi
++rm "$TEMP_FILE"
+ 
+ # Create .env file in docker directory
+-cat > "${DOCKER_DIR}/.env" << EOL
++DOCKER_ENV_CONTENT=$(cat <<EOL
+ # Generated on ${SETUP_DATE}
+ # Keycloak Admin
+ KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
+@@ -34,27 +166,69 @@ KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
+ KC_DB_USERNAME=keycloak
+ KC_DB_PASSWORD=${KC_DB_PASSWORD}
+ EOL
++)
++create_env_file "$DOCKER_DIR/.env" "$DOCKER_ENV_CONTENT"
+ 
+-# Create encrypted credentials documentation
+-cat > "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt" << EOL
+-Setup Date: ${SETUP_DATE}
++# Create .env file in scripts/setup/keycloak directory
++KEYCLOAK_ENV_CONTENT=$(cat <<EOL
++KEYCLOAK_URL=https://auth.mrx8086.com
++KEYCLOAK_ADMIN_USER=admin
++KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
++NEXTCLOUD_CLIENT_ID=nextcloud
++PAPERLESS_CLIENT_ID=paperless
++NODERED_CLIENT_ID=nodered
++TESTADMIN_PASSWORD=${TESTADMIN_PASSWORD}
++TESTUSER_PASSWORD=${TESTUSER_PASSWORD}
++TESTSERVICEUSER_PASSWORD=${TESTSERVICEUSER_PASSWORD}
++KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}
++EOL
++)
++create_env_file "$KEYCLOAK_SETUP_DIR/.env" "$KEYCLOAK_ENV_CONTENT"
+ 
+-Keycloak Admin Credentials:
+-Username: admin
+-Password: ${KEYCLOAK_ADMIN_PASSWORD}
++echo ">>> Environment setup completed!"
+ 
+-Keycloak Database Credentials:
+-Username: keycloak
+-Password: ${KC_DB_PASSWORD}
+-EOL
++# --------------- KEYCLOAK KONFIGURATION ---------------
++echo ">>> Keycloak Konfiguration..."
++cd "$KEYCLOAK_SETUP_DIR"
++
++echo ">>> Starte setup_realm.js"
++node setup_realm.js
++
++cd "$PROJECT_ROOT"
++
++# --------------- NEXTCLOUD KONFIGURATION ---------------
++echo ">>> Nextcloud Konfiguration..."
++
++# Verify if variable is set from earlier in the script
++echo ">>> Debug: Checking original variable..."
++echo ">>> Debug: KEYCLOAK_NEXTCLOUD_CLIENT_SECRET = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
++
++# Try reading from .env file if variable is empty
++if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
++    echo ">>> Debug: Variable is empty, trying to read from .env file..."
++    KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(grep KEYCLOAK_NEXTCLOUD_CLIENT_SECRET "${KEYCLOAK_SETUP_DIR}/.env" | cut -d '=' -f2)
++    echo ">>> Debug: Value from .env file = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
++fi
++
++# Ensure we have a value
++if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
++    echo ">>> Error: Could not get client secret value"
++    exit 1
++fi
++
++# Escape special characters in the secret for JSON
++ESCAPED_SECRET=$(echo "$KEYCLOAK_NEXTCLOUD_CLIENT_SECRET" | sed 's/["\]/\\&/g')
++echo ">>> Debug: Escaped secret = $ESCAPED_SECRET"
++
++# Create the extra vars
++EXTRA_VARS="{\"client_secret\": \"$ESCAPED_SECRET\"}"
++echo ">>> Debug: Extra vars = $EXTRA_VARS"
+ 
+-# Encrypt credentials file
+-gpg --symmetric --cipher-algo AES256 "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt"
+-rm "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt"
++# Run Ansible with the extra vars
++sudo ansible-playbook \
++    -i "$ANSIBLE_INVENTORY" \
++    "$ANSIBLE_PLAYBOOK" \
++    --extra-vars "$EXTRA_VARS" \
++    -v
+ 
+-echo "Environment setup completed!"
+-echo "Credentials have been saved and encrypted in: ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
+-echo ".env file has been created in: ${DOCKER_DIR}/.env"
+-echo ""
+-echo "To view credentials, use:"
+-echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
+\ No newline at end of file
++echo ">>> Fertig"
+\ No newline at end of file
+diff --git a/scripts/setup/keycloak/.env b/scripts/setup/keycloak/.env
+index 8c6b3c3..0087b01 100644
+--- a/scripts/setup/keycloak/.env
++++ b/scripts/setup/keycloak/.env
+@@ -6,4 +6,6 @@ PAPERLESS_CLIENT_ID=paperless
+ NODERED_CLIENT_ID=nodered
+ TESTADMIN_PASSWORD=TestAdminPwd
+ TESTUSER_PASSWORD=TestUserPwd
+-KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=D939xgzoxi58T2XZShdUPZP4gsI0kBOu
+\ No newline at end of file
++TESTSERVICEUSER_PASSWORD=TestServiceUserPwd
++KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=OSbJ08zyjBWChwBR7S6c1q4sU0d8zvEK
++NEXTCLOUD_URL=https://cloud.mrx8086.com
+\ No newline at end of file
+diff --git a/scripts/setup/keycloak/setup_realm.js b/scripts/setup/keycloak/setup_realm.js
+index 9f67b1e..784d543 100644
+--- a/scripts/setup/keycloak/setup_realm.js
++++ b/scripts/setup/keycloak/setup_realm.js
+@@ -1,19 +1,26 @@
+ import dotenv from 'dotenv';
+ import axios from 'axios';
+ 
+-// Lade Umgebungsvariablen
++// Load environment variables
+ dotenv.config();
++console.log('Environment variables loaded.');
+ 
+-// Konfigurationskonstanten
++// Configuration constants
+ const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
+ const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
+ const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
+ const REALM_NAME = 'office-automation';
+ 
+-// Client IDs und deren Konfiguration
++console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME });
++
++// Client IDs and their configuration
+ const CLIENTS = {
+     [process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud']: {
+-        redirectUris: ["https://cloud.mrx8086.com/*"]
++        redirectUris: [
++            `https://cloud.mrx8086.com/apps/sociallogin/custom_oidc/keycloak`,
++            `https://cloud.mrx8086.com/apps/user_oidc/code`
++        ],
++        postLogoutRedirectUris: ["https://cloud.mrx8086.com/*"]
+     },
+     [process.env.PAPERLESS_CLIENT_ID || 'paperless']: {
+         redirectUris: ["https://docs.mrx8086.com/*"]
+@@ -23,16 +30,18 @@ const CLIENTS = {
+     }
+ };
+ 
+-// Hilfsfunktion für API-Fehlerbehandlung
++console.log('CLIENTS configuration:', CLIENTS);
++
++// Helper function for API error handling
+ const handleAxiosError = (error, operation, config, response) => {
+     console.error(`Error during ${operation}:`);
+     if (config) {
+-      console.error('Request:', {
+-          method: config.method,
+-          url: config.url,
+-          headers: config.headers,
+-          data: config.data,
+-      });
++        console.error('Request:', {
++            method: config.method,
++            url: config.url,
++            headers: config.headers,
++            data: config.data,
++        });
+     }
+     if (error.response) {
+         console.error('Response:', {
+@@ -45,8 +54,9 @@ const handleAxiosError = (error, operation, config, response) => {
+     throw error;
+ };
+ 
+-// Admin Token abrufen
++// Get Admin Token
+ async function getAdminToken() {
++    console.log('Getting admin token...');
+     try {
+         const response = await axios.post(
+             `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
+@@ -62,165 +72,146 @@ async function getAdminToken() {
+                 }
+             }
+         );
++        console.log('Admin token received.');
+         return response.data.access_token;
+     } catch (error) {
+-         handleAxiosError(error, 'getting admin token');
++        handleAxiosError(error, 'getting admin token');
+     }
+ }
+ 
+-// Prüfen ob Realm existiert
++// Check if Realm exists
+ async function checkRealmExists(token) {
++    console.log(`Checking if realm ${REALM_NAME} exists...`);
+     try {
+-        await axios.get(
+-            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`,
+-            {
+-                headers: {
+-                    'Authorization': `Bearer ${token}`
+-                }
+-            }
+-        );
++        await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
++            headers: { 'Authorization': `Bearer ${token}` }
++        });
++        console.log(`Realm ${REALM_NAME} exists.`);
+         return true;
+     } catch (error) {
+         if (error.response?.status === 404) {
++            console.log(`Realm ${REALM_NAME} does not exist.`);
+             return false;
+         }
+         handleAxiosError(error, 'checking realm existence');
+     }
+ }
+ 
+-
+-// Funktion um Client Infos abzufragen
+-async function getClient(token, clientId) {
++// Function to get client information by clientId
++async function getClientByClientId(token, clientId) {
++    console.log(`Getting client information for ${clientId}...`);
+     try {
+-        const response = await axios.get(
+-            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
+-            {
+-                headers: {
+-                    'Authorization': `Bearer ${token}`
+-                },
+-                params: {
+-                    clientId: clientId
+-                }
+-            }
+-        );
++        const response = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, {
++            headers: { 'Authorization': `Bearer ${token}` },
++            params: { clientId }
++        });
+         if (response.data.length === 0) {
+-            console.error(`Client ${clientId} not found`);
++            console.log(`Client ${clientId} not found`);
+             return null;
+         }
+-
++        console.log(`Client ${clientId} found.`);
+         return response.data[0];
+     } catch (error) {
+-       handleAxiosError(error, `getting client ${clientId}`);
++        handleAxiosError(error, `getting client ${clientId}`);
++        return null;
+     }
+ }
+ 
+-// Prüfen ob Client existiert
+-async function checkClientExists(token, clientId) {
+-    const client = await getClient(token, clientId);
+-    return !!client;
+-}
+-
++// Check if client exists
++const checkClientExists = async (token, clientId) => !!await getClientByClientId(token, clientId);
+ 
++// Get client mappers by client ID
+ async function getClientMappers(token, clientId) {
++    console.log(`Getting client mappers for ${clientId}...`);
++    const client = await getClientByClientId(token, clientId);
++    if (!client) {
++        console.log(`Client ${clientId} not found, no mappers to get.`);
++        return [];
++    }
+     try {
+-        const client = await getClient(token, clientId);
+-        if (!client) {
+-            return [];
+-        }
+         const response = await axios.get(
+             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
+-            {
+-                headers: {
+-                    'Authorization': `Bearer ${token}`
+-                }
+-            }
++            { headers: { 'Authorization': `Bearer ${token}` } }
+         );
++        console.log(`Client mappers for ${clientId} retrieved.`);
+         return response.data;
+     } catch (error) {
+-      handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response);
++        handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response);
+         return [];
+     }
+ }
+ 
+-async function getClientScopes(token, clientId){
++// Get client scopes for a client
++async function getClientScopes(token, clientId) {
++    console.log(`Getting client scopes for ${clientId}...`);
++    const client = await getClientByClientId(token, clientId);
++    if (!client) {
++        console.log(`Client ${clientId} not found, no client scopes to get.`);
++        return [];
++    }
+     try {
+-        const client = await getClient(token, clientId);
+-        if(!client)
+-            return [];
+-
+         const response = await axios.get(
+             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/client-scopes`,
+-            {
+-                 headers: {
+-                        'Authorization': `Bearer ${token}`
+-                    }
+-            }
++            { headers: { 'Authorization': `Bearer ${token}` } }
+         );
+-
++        console.log(`Client scopes for ${clientId} retrieved.`);
+         return response.data;
+-
+-    } catch(error){
+-      handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response);
++    } catch (error) {
++        handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response);
+         return [];
+     }
+ }
+ 
++// Get a specific client scope by name
+ async function getClientScope(token, scopeName) {
++    console.log(`Getting client scope "${scopeName}"...`);
+     try {
+         const response = await axios.get(
+             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
+-             {
+-                 headers: {
+-                     'Authorization': `Bearer ${token}`
+-                 },
+-                params: {
+-                  name: scopeName
+-                }
+-              }
++            { headers: { 'Authorization': `Bearer ${token}` } }
+         );
+-
+-        if(response.data.length === 0){
+-            console.error(`Client Scope ${scopeName} not found`);
++        const foundScope = response.data.find(scope => scope.name === scopeName);
++        if (!foundScope) {
++            console.log(`Client Scope "${scopeName}" not found`);
+             return null;
+         }
+-
+-        return response.data[0]
+-    } catch (error){
++        console.log(`Client scope "${scopeName}" found:`, foundScope);
++        return foundScope;
++    } catch (error) {
+         handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response);
+         return null;
+     }
+ }
+ 
+-async function addDefaultClientScope(token, clientId, scopeName){
++// Add a default client scope to a client
++async function addDefaultClientScope(token, clientId, scopeName) {
++    console.log(`Adding client scope "${scopeName}" as default for client "${clientId}"...`);
++    const client = await getClientByClientId(token, clientId);
++    const scope = await getClientScope(token, scopeName);
++    if (!client || !scope) {
++        console.log(`Client "${clientId}" or scope "${scopeName}" not found, cannot add as default scope.`);
++        return;
++    }
+     try {
+-        const client = await getClient(token, clientId);
+-        const scope = await getClientScope(token, scopeName);
+-         if(!client || !scope){
+-           return null;
+-         }
+-
+-
+         await axios.put(
+-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`,
+-                null,
+-                {
+-                  headers: {
+-                        'Authorization': `Bearer ${token}`,
+-                        'Content-Type': 'application/json'
+-                    }
+-               }
++            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`,
++            null,
++            {
++                headers: {
++                    'Authorization': `Bearer ${token}`,
++                    'Content-Type': 'application/json'
++                }
++            }
+         );
+-
+-        console.log(`Client scope ${scopeName} added as default scope for client ${clientId}`)
+-
+-    } catch(error){
+-       handleAxiosError(error, `adding client scope ${scopeName} as default for client ${clientId}`);
++        console.log(`Client scope "${scopeName}" added as default scope for client "${clientId}"`);
++    } catch (error) {
++        handleAxiosError(error, `adding client scope "${scopeName}" as default for client "${clientId}"`);
+     }
+ }
+ 
+-
+-// Realm erstellen
++// Create Realm
+ async function createRealm(token) {
++    console.log(`Creating realm ${REALM_NAME}...`);
+     const realmConfig = {
+         realm: REALM_NAME,
+         enabled: true,
+@@ -250,67 +241,47 @@ async function createRealm(token) {
+         webAuthnPolicyUserVerificationRequirement: "preferred",
+         webAuthnPolicyCreateTimeout: 0,
+         webAuthnPolicyAvoidSameAuthenticatorRegister: false,
+-        defaultDefaultClientScopes: [
+-            "email",
+-            "profile",
+-            "roles",
+-            "web-origins"
+-        ],
+-        defaultOptionalClientScopes: [
+-            "address",
+-            "phone",
+-            "offline_access",
+-            "microprofile-jwt"
+-        ]
++        defaultDefaultClientScopes: ["email", "profile", "roles", "web-origins"],
++        defaultOptionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"]
+     };
+-
+     try {
+-        await axios.post(
+-            `${KEYCLOAK_URL}/admin/realms`,
+-            realmConfig,
+-            {
+-                headers: {
+-                    'Authorization': `Bearer ${token}`,
+-                    'Content-Type': 'application/json'
+-                }
++        await axios.post(`${KEYCLOAK_URL}/admin/realms`, realmConfig, {
++            headers: {
++                'Authorization': `Bearer ${token}`,
++                'Content-Type': 'application/json'
+             }
+-        );
++        });
+         console.log('Realm created successfully');
+     } catch (error) {
+         handleAxiosError(error, 'creating realm');
+     }
+ }
+ 
+-// Client erstellen
++// Create client and manage mappers
+ async function createClient(token, clientId, clientName, redirectUris) {
+-    let client;
+-    const clientExists = await checkClientExists(token, clientId);
++    console.log(`Creating client "${clientId}"...`);
++    let client = await getClientByClientId(token, clientId);
+ 
+-    if (!clientExists) {
++    if (!client) {
+         const clientConfig = {
+             clientId: clientId,
+             name: clientName,
+             enabled: true,
+             protocol: "openid-connect",
+             publicClient: false,
+-            authorizationServicesEnabled: true,
+-            serviceAccountsEnabled: true,
++            authorizationServicesEnabled: false,
++            serviceAccountsEnabled: false,
+             standardFlowEnabled: true,
+             implicitFlowEnabled: false,
+             directAccessGrantsEnabled: true,
+             redirectUris: redirectUris,
+-            webOrigins: ["+"],
+-            defaultClientScopes: [
+-                "roles"
+-            ],
+-            optionalClientScopes: [
+-                "address",
+-                "phone",
+-                "offline_access",
+-                "microprofile-jwt"
+-            ]
++             webOrigins: ["+"],
++            defaultClientScopes: ["roles"],
++            optionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"],
++            rootUrl: process.env.NEXTCLOUD_URL,
++            baseUrl: process.env.NEXTCLOUD_URL,
++            adminUrl: process.env.NEXTCLOUD_URL,
+         };
+-
+         try {
+             const response = await axios.post(
+                 `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
+@@ -322,21 +293,34 @@ async function createClient(token, clientId, clientName, redirectUris) {
+                     }
+                 }
+             );
+-            console.log(`Client ${clientId} created successfully`);
++            console.log(`Client "${clientId}" created successfully`);
+             client = response.data;
+         } catch (error) {
+             handleAxiosError(error, `creating client: ${clientId}`);
+             return;
+         }
+     } else {
+-        client = await getClient(token, clientId);
+-        console.log(`Client ${clientId} already exists, checking mappers`);
++        console.log(`Client "${clientId}" already exists, checking mappers`);
+     }
+ 
+-     if (client) {
+-        
+-        const existingMappers = await getClientMappers(token, clientId)
++    if (client) {
++        try {
++            await axios.put(
++                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`,
++                { ...client, secret: process.env[`KEYCLOAK_${clientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`] },
++                {
++                    headers: {
++                        'Authorization': `Bearer ${token}`,
++                        'Content-Type': 'application/json'
++                    }
++                }
++            );
++            console.log(`Set client secret for client: ${clientId}`);
++        } catch (error) {
++            handleAxiosError(error, `setting client secret for client: ${clientId}`, error.config, error.response);
++        }
+ 
++        const existingMappers = await getClientMappers(token, clientId);
+         const requiredMappers = [
+             {
+                 name: "groups",
+@@ -367,10 +351,8 @@ async function createClient(token, clientId, clientName, redirectUris) {
+ 
+         for (const mapper of requiredMappers) {
+             const existingMapper = existingMappers.find(m => m.name === mapper.name);
+-            
+             try {
+                 if (existingMapper) {
+-                    // Update existierenden Mapper
+                     await axios.put(
+                         `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models/${existingMapper.id}`,
+                         { ...existingMapper, ...mapper },
+@@ -381,9 +363,8 @@ async function createClient(token, clientId, clientName, redirectUris) {
+                             }
+                         }
+                     );
+-                    console.log(`Mapper ${mapper.name} updated for client ${clientId}`);
++                    console.log(`Mapper "${mapper.name}" updated for client "${clientId}"`);
+                 } else {
+-                    // Erstelle neuen Mapper
+                     await axios.post(
+                         `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
+                         mapper,
+@@ -394,70 +375,69 @@ async function createClient(token, clientId, clientName, redirectUris) {
+                             }
+                         }
+                     );
+-                    console.log(`Mapper ${mapper.name} created for client ${clientId}`);
++                    console.log(`Mapper "${mapper.name}" created for client "${clientId}"`);
+                 }
+             } catch (error) {
+-                handleAxiosError(error, `managing mapper ${mapper.name} for client ${clientId}`, error.config, error.response);
+-                // Wir werfen den Fehler nicht weiter, damit andere Mapper noch verarbeitet werden können
++                handleAxiosError(error, `managing mapper "${mapper.name}" for client "${clientId}"`, error.config, error.response);
++            }
++        }
++
++        if (clientId.includes("nextcloud")) {
++            await addDefaultClientScope(token, clientId, "openid");
++            await addDefaultClientScope(token, clientId, "profile");
++            await addDefaultClientScope(token, clientId, "groups-nextcloud");
++
++            try {
++                await axios.put(
++                    `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`,
++                    { ...client, defaultClientScopes: client.defaultClientScopes.filter(c => c !== "nextcloud-dedicated") },
++                    {
++                        headers: {
++                            'Authorization': `Bearer ${token}`,
++                            'Content-Type': 'application/json'
++                        }
++                    }
++                );
++                console.log(`Removed client scope nextcloud-dedicated from client: ${clientId}`);
++            } catch (error) {
++                handleAxiosError(error, `removing client scope nextcloud-dedicated from client: ${clientId}`, error.config, error.response);
+             }
+         }
+     }
+ }
+ 
+-
+-// Gruppen erstellen
++// Create default groups
+ async function createDefaultGroups(token) {
++    console.log('Creating default groups...');
+     const groups = [
+-        {
+-            name: "Administrators",
+-            path: "/Administrators",
+-            attributes: {
+-                "description": ["Full system access"]
+-            }
+-        },
+-        {
+-            name: "Users",
+-            path: "/Users",
+-            attributes: {
+-                "description": ["Regular system users"]
+-            }
+-        }
++        { name: "nextcloud-admins", path: "/nextcloud-admins", attributes: { "description": ["Nextcloud administrators"] } },
++        { name: "nextcloud-users", path: "/nextcloud-users", attributes: { "description": ["Nextcloud regular users"] } },
++        { name: "nextcloud-youpi", path: "/nextcloud-youpi", attributes: { "description": ["Nextcloud youpi"] } },
++        { name: "nextcloud-service", path: "/nextcloud-service", attributes: { "description": ["Nextcloud service user"] } }
+     ];
+ 
+     for (const group of groups) {
+         try {
+-            // Prüfen ob Gruppe existiert
+-            const existingGroup = await axios.get(
+-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`,
+-                {
+-                    headers: {
+-                        'Authorization': `Bearer ${token}`
+-                    },
+-                    params: {
+-                        search: group.name,
+-                    }
+-                }
+-            );
++            const existingGroups = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, {
++                headers: { 'Authorization': `Bearer ${token}` },
++                params: { search: group.name, exact: true } // Added exact: true for precise matching
++            });
+ 
+-            if (existingGroup.data.length > 0) {
+-                console.log(`Group ${group.name} already exists`);
++            if (existingGroups.data.length > 0) {
++                console.log(`Group "${group.name}" already exists`);
+                 continue;
+             }
+ 
+-            await axios.post(
+-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`,
+-                group,
+-                {
+-                    headers: {
+-                        'Authorization': `Bearer ${token}`,
+-                        'Content-Type': 'application/json'
+-                    }
++            await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, group, {
++                headers: {
++                    'Authorization': `Bearer ${token}`,
++                    'Content-Type': 'application/json'
+                 }
+-            );
+-            console.log(`Group ${group.name} created successfully`);
++            });
++            console.log(`Group "${group.name}" created successfully`);
+         } catch (error) {
+             if (error.response?.status === 409) {
+-                console.log(`Group ${group.name} already exists`);
++                console.log(`Group "${group.name}" already exists`);
+             } else {
+                 handleAxiosError(error, `creating group: ${group.name}`);
+             }
+@@ -465,44 +445,44 @@ async function createDefaultGroups(token) {
+     }
+ }
+ 
+-
++// Create a test token for a user
+ async function createTestToken(token, username) {
++    console.log(`Creating test token for user "${username}"...`);
+     try {
+-         const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud';
+-         const client = await getClient(token, nextcloudClientId);
++        const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud';
++        const client = await getClientByClientId(token, nextcloudClientId);
+ 
+-        if (!client)
++        if (!client) {
++            console.log(`Client "${nextcloudClientId}" not found, cannot create test token.`);
+             return null;
++        }
+         const response = await axios.post(
+             `${KEYCLOAK_URL}/realms/${REALM_NAME}/protocol/openid-connect/token`,
+             new URLSearchParams({
+                 'client_id': nextcloudClientId,
+-                'client_secret': process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET,
++                'client_secret': process.env[`KEYCLOAK_${nextcloudClientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`],
+                 'username': username,
+                 'password': process.env.TESTADMIN_PASSWORD || "initial123!",
+                 'grant_type': 'password'
+             }),
+-            {
+-                headers: {
+-                    'Content-Type': 'application/x-www-form-urlencoded'
+-                }
+-            }
++            { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
+         );
++        console.log(`Test token for user "${username}" created.`);
+         return response.data.access_token;
+     } catch (error) {
+         handleAxiosError(error, `getting test token for ${username}`, error.config, error.response);
++        return null;
+     }
+ }
+ 
+-// Funktion zum Decodieren eines JWT-Tokens
++// Function to decode a JWT token
+ function decodeToken(token) {
+     try {
+         const base64Url = token.split('.')[1];
+         const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+-        const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
+-            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+-        }).join(''));
+-
++        const jsonPayload = decodeURIComponent(atob(base64).split('').map(c =>
++            '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
++        ).join(''));
+         return JSON.parse(jsonPayload);
+     } catch (error) {
+         console.error("Error decoding token:", error.message);
+@@ -510,87 +490,128 @@ function decodeToken(token) {
+     }
+ }
+ 
+-// Test-User erstellen
++// Create initial users
+ async function createInitialUsers(token) {
++    console.log('Creating initial users...');
+     const users = [
+-        {
+-            username: "testadmin",
+-            enabled: true,
+-            emailVerified: true,
+-            firstName: "Test",
+-            lastName: "Admin",
+-            email: "testadmin@mrx8086.com",
+-            credentials: [{
+-                type: "password",
+-                value: process.env.TESTADMIN_PASSWORD || "initial123!",
+-                temporary: true
+-            }],
+-            groups: ["/Administrators"]
+-        },
+-        {
+-            username: "testuser",
+-            enabled: true,
+-            emailVerified: true,
+-            firstName: "Test",
+-            lastName: "User",
+-            email: "testuser@mrx8086.com",
+-            credentials: [{
+-                type: "password",
+-                value: process.env.TESTUSER_PASSWORD || "initial123!",
+-                temporary: true
+-            }],
+-            groups: ["/Users"]
+-        }
++        { username: "testadmin", enabled: true, emailVerified: true, firstName: "Test", lastName: "Admin", email: "testadmin@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTADMIN_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-admins", "nextcloud-users"] },
++        { username: "testuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "User", email: "testuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-users", "nextcloud-youpi"] },
++        { username: "testserviceuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "Service User", email: "testserviceuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTSERVICEUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-service"] }
+     ];
+ 
+     for (const user of users) {
+         try {
+-            // Prüfen ob User existiert
+-            const existingUsers = await axios.get(
+-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`,
+-                {
+-                    headers: {
+-                        'Authorization': `Bearer ${token}`
+-                    },
+-                    params: {
+-                        username: user.username,
+-                        exact: true
+-                    }
+-                }
+-            );
++            const existingUsers = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, {
++                headers: { 'Authorization': `Bearer ${token}` },
++                params: { username: user.username, exact: true } // Added exact: true for precise matching
++            });
+ 
+             if (existingUsers.data.length > 0) {
+-                console.log(`User ${user.username} already exists`);
++                console.log(`User "${user.username}" already exists`);
+                 continue;
+             }
+ 
+-            // User erstellen
+-            await axios.post(
+-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`,
+-                user,
++            await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, user, {
++                headers: {
++                    'Authorization': `Bearer ${token}`,
++                    'Content-Type': 'application/json'
++                }
++            });
++            console.log(`User "${user.username}" created successfully`);
++
++        } catch (error) {
++            handleAxiosError(error, `creating user: ${user.username}`, error.config, error.response);
++        }
++    }
++}
++
++async function createGroupsNextcloudScope(token) {
++    const scopeName = "groups-nextcloud";
++    const mapperName = "groups-mapper";
++    console.log(`Starting createGroupsNextcloudScope`);
++    let clientScope = await getClientScope(token, scopeName);
++
++    if (!clientScope) {
++        try {
++            console.log(`Creating client scope "${scopeName}"`);
++            clientScope = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
++                {
++                    "name": scopeName,
++                    "protocol": "openid-connect",
++                    "description": "Provides access to user group information for Nextcloud.", // Hinzugefügt: Beschreibung
++                    "attributes": {},
++                    "consentScreenText": "Grant access to user groups in Nextcloud",
++                    "includeInTokenScope": true
++                },
+                 {
+                     headers: {
+                         'Authorization': `Bearer ${token}`,
+                         'Content-Type': 'application/json'
+                     }
+-                }
+-            );
+-            console.log(`User ${user.username} created successfully`);
++                });
++            console.log(`Client scope "${scopeName}" created successfully`);
++            clientScope = response.data;
++        } catch (error) {
++            console.error(`Error creating client scope "${scopeName}":`, error);
++            handleAxiosError(error, `creating ${scopeName} client scope`, error.config, error.response);
++            return;
++        }
++    } else {
++        console.log(`Client scope "${scopeName}" exists, skipping creation`);
++    }
+ 
++    console.log("Client scope creation step finished");
++    if (clientScope) {
++        console.log(`Check for mapper "${mapperName}" in scope "${scopeName}"`);
++        try {
++            const mappersResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
++                { headers: { 'Authorization': `Bearer ${token}` } }
++            );
++            if (!mappersResponse.data.find(m => m.name === mapperName)) {
++                try {
++                    await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
++                        {
++                            "name": mapperName,
++                            "protocol": "openid-connect",
++                            "protocolMapper": "oidc-group-membership-mapper",
++                            "config": {
++                                "full.path": "false",
++                                "id.token.claim": "false",
++                                "access.token.claim": "true",
++                                "userinfo.token.claim": "true",
++                                "claim.name": "groups",
++                                "add.to.introspection": "false"
++                            }
++                        },
++                        {
++                            headers: {
++                                'Authorization': `Bearer ${token}`,
++                                'Content-Type': 'application/json'
++                            }
++                        });
++                    console.log(`Mapper "${mapperName}" created for client scope "${scopeName}"`);
++                } catch (error) {
++                    console.error(`Error creating mapper "${mapperName}" for scope "${scopeName}":`, error);
++                    handleAxiosError(error, `creating mapper for ${scopeName}`, error.config, error.response);
++                }
++            } else {
++                console.log(`Mapper "${mapperName}" exists in client scope "${scopeName}", skipping creation`);
++            }
+         } catch (error) {
+-            handleAxiosError(error, `creating user: ${user.username}`, error.config, error.response);
++            console.error("Error checking for mappers:", error);
++            handleAxiosError(error, `checking mappers for ${scopeName}`, error.config, error.response);
+         }
+     }
++    console.log("Finished createGroupsNextcloudScope");
+ }
+ 
+-
+-// Hauptfunktion
++// Main function
+ async function setupRealm() {
+     try {
+         console.log('Starting Keycloak setup...');
+         const token = await getAdminToken();
+ 
+-        // Prüfe ob Realm existiert
++        // Check if realm exists
+         const realmExists = await checkRealmExists(token);
+ 
+         if (!realmExists) {
+@@ -600,41 +621,37 @@ async function setupRealm() {
+             console.log('Realm already exists, skipping base setup');
+         }
+ 
+-        // Clients erstellen
++        // Create client scope groups-nextcloud
++        await createGroupsNextcloudScope(token);
++
++        // Create clients
+         for (const clientId in CLIENTS) {
+             await createClient(token, clientId, clientId, CLIENTS[clientId].redirectUris);
+         }
+ 
+-       const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud';
+-        await addDefaultClientScope(token, nextcloudClientId, "openid");
+-
+-        // Gruppen erstellen
++        // Create groups
+         await createDefaultGroups(token);
+ 
+-        // Test User erstellen
++        // Create test users
+         await createInitialUsers(token);
+ 
+-
+-        // Konfiguration des Office-Automation Realms mit Admin Token auslesen
++        // Read the configuration of the Office-Automation realm with Admin Token
+         if (token) {
+             console.log("Master Realm Admin Token:", token);
+-
+             try {
+                 const realmConfig = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
+-                    headers: {
+-                        'Authorization': `Bearer ${token}`
+-                    }
++                    headers: { 'Authorization': `Bearer ${token}` }
+                 });
+-                console.log("Office Automation Realm Configuration:", realmConfig.data)
++                console.log("Office Automation Realm Configuration:", realmConfig.data);
+             } catch (error) {
+                 handleAxiosError(error, 'getting office realm configuration', error.config, error.response);
+             }
+         } else {
+-            console.error("Error getting Master Realm admin token")
++            console.error("Error getting Master Realm admin token");
+         }
+ 
+-        // Test Token erstellen
+-        const testToken = await createTestToken(token, "testadmin@mrx8086.com");
++        // Create Test Token
++        const testToken = await createTestToken(token, "testadmin");
+ 
+         if (testToken) {
+             console.log("Test Token generated successfully!");
+@@ -644,7 +661,6 @@ async function setupRealm() {
+         } else {
+             console.error("Error generating Test Token");
+         }
+-
+         console.log('Setup completed successfully');
+ 
+     } catch (error) {
+@@ -653,5 +669,5 @@ async function setupRealm() {
+     }
+ }
+ 
+-// Script ausführen
++// Execute the script
+ setupRealm();
+\ No newline at end of file
+diff --git a/scripts/setup/keycloak/test_realm.js b/scripts/setup/keycloak/test_realm.js
+index 78c03a4..11e3254 100644
+--- a/scripts/setup/keycloak/test_realm.js
++++ b/scripts/setup/keycloak/test_realm.js
+@@ -6,7 +6,7 @@ dotenv.config();
+ const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
+ const NEXTCLOUD_CLIENT_ID = process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud';
+ const TESTADMIN_USERNAME = "testadmin@mrx8086.com";
+-const TESTADMIN_PASSWORD = process.env.TESTADMIN_PASSWORD || "initial123!";
++const TESTADMIN_PASSWORD = process.env.TESTADMIN_PASSWORD;
+ const REALM_NAME = 'office-automation';
+ const CLIENT_SECRET = process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET;
+ 
+@@ -74,28 +74,33 @@ function decodeToken(token) {
+     }
+ }
+ 
++// Prüfe ob ein Admin Token korrekt generiert werden kann
+ async function testKeycloakLogin() {
+-  try {
+-    const accessToken = await getAccessToken(TESTADMIN_USERNAME, TESTADMIN_PASSWORD);
++    try {
++        const accessToken = await getAccessToken(TESTADMIN_USERNAME, TESTADMIN_PASSWORD);
+ 
+-    if (!accessToken) {
+-      console.error('Failed to get access token.');
+-      return;
+-    }
+-    console.log('Access Token:', accessToken);
+-    const decodedToken = decodeToken(accessToken);
+-    if (decodedToken) {
+-      console.log('Decoded Access Token:', decodedToken);
+-      if (decodedToken.groups.includes('/Administrators')){
+-        console.log("Admin Group is set correctly!")
+-      } else {
+-          console.error("Admin Group is not set correctly!")
+-      }
+-    }
++        if (!accessToken) {
++            console.error('Failed to get access token.');
++            return;
++        }
++        console.log('Access Token:', accessToken);
++        const decodedToken = decodeToken(accessToken);
++        if(decodedToken) {
++             console.log('Decoded Access Token:', decodedToken);
++            if (Array.isArray(decodedToken.groups) && decodedToken.groups.includes('/nextcloud-admins')){
++                console.log("Admin Group is set correctly!")
++            } else if (typeof decodedToken.groups === 'string' && decodedToken.groups.includes('/nextcloud-admins')) {
++                console.log("Admin Group is set correctly!")
++            }
++             else {
++                 console.error("Admin Group is not set correctly!")
++            }
++        }
+ 
+-  } catch (error) {
+-    console.error('An error occurred:', error);
+-  }
++
++    } catch (error) {
++        console.error('An error occurred:', error);
++    }
+ }
+ 
+ testKeycloakLogin();
+\ No newline at end of file

+ 203 - 29
scripts/install/setup_environment.sh

@@ -1,31 +1,163 @@
 #!/bin/bash
 #!/bin/bash
+set -e
 
 
 # Ensure we're in the project root directory
 # Ensure we're in the project root directory
 PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
 PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-cd "${PROJECT_ROOT}"
 
 
 # Define directories relative to project root
 # Define directories relative to project root
-CREDENTIALS_DIR="config/credentials"
-DOCKER_DIR="docker"
+CREDENTIALS_DIR="${PROJECT_ROOT}/config/credentials"
+DOCKER_DIR="${PROJECT_ROOT}/docker"
+KEYCLOAK_SETUP_DIR="${PROJECT_ROOT}/scripts/setup/keycloak"
+ANSIBLE_PLAYBOOK="${PROJECT_ROOT}/ansible/site.yml"
+ANSIBLE_INVENTORY="${PROJECT_ROOT}/ansible/inventory/staging/hosts"
+NEXTCLOUD_DATA_DIR="${PROJECT_ROOT}/data/nextcloud/data"
+TEMP_FILE=$(mktemp)
+KEYCLOAK_DB_DIR="${PROJECT_ROOT}/data/keycloak-db"
 
 
 # Create necessary directories
 # Create necessary directories
-mkdir -p "${CREDENTIALS_DIR}"
-mkdir -p "${DOCKER_DIR}"
+sudo mkdir -p "${CREDENTIALS_DIR}"
+sudo mkdir -p "${DOCKER_DIR}"
+sudo mkdir -p "${KEYCLOAK_SETUP_DIR}"
+
+# Initialize password variables
+KEYCLOAK_ADMIN_PASSWORD=""
+KC_DB_PASSWORD=""
+TESTADMIN_PASSWORD=""
+TESTUSER_PASSWORD=""
+TESTSERVICEUSER_PASSWORD=""
+KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=""
+
+# Function to read a password from a .env file
+read_password_from_env() {
+    local env_file="$1"
+    local variable_name="$2"
+    if [ -f "$env_file" ]; then
+        grep "^${variable_name}=" "$env_file" | cut -d '=' -f2
+    fi
+}
 
 
 # Function to generate secure passwords
 # Function to generate secure passwords
 generate_password() {
 generate_password() {
-    openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24
+    openssl rand -base64 32
+}
+
+# Function to generate password if empty
+generate_password_if_empty() {
+    local variable_name="$1"
+    eval "local value=\$$variable_name"
+    if [ -z "$value" ]; then
+        eval "$variable_name=\"$(generate_password)\""
+        echo ">>> Generiertes Passwort für: $variable_name"
+    fi
+}
+
+# Function to create .env file
+create_env_file() {
+    local env_file="$1"
+    local content="$2"
+    if [ ! -f "$env_file" ]; then
+        echo "$content" > "$env_file"
+        echo ">>> .env file created: $env_file"
+    else
+        echo ">>> .env file already exists: $env_file"
+    fi
 }
 }
 
 
+echo ">>> Überprüfe bestehende .env Dateien und lese Passwörter..."
+
+# Try reading passwords from existing .env files
+if [ -f "$DOCKER_DIR/.env" ]; then
+    KC_DB_PASSWORD=$(read_password_from_env "$DOCKER_DIR/.env" "KC_DB_PASSWORD")
+    KEYCLOAK_ADMIN_PASSWORD=$(read_password_from_env "$DOCKER_DIR/.env" "KEYCLOAK_ADMIN_PASSWORD")
+fi
+
+if [ -f "$KEYCLOAK_SETUP_DIR/.env" ]; then
+    KEYCLOAK_ADMIN_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "KEYCLOAK_ADMIN_PASSWORD") # Überschreibt ggf. den Wert aus docker/.env
+    TESTADMIN_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTADMIN_PASSWORD")
+    TESTUSER_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTUSER_PASSWORD")
+    TESTSERVICEUSER_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTSERVICEUSER_PASSWORD")
+    KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "KEYCLOAK_NEXTCLOUD_CLIENT_SECRET")
+fi
+
+echo ">>> Generiere neue Passwörter für fehlende Werte..."
+
+# Generate passwords if they are still empty
+generate_password_if_empty KEYCLOAK_ADMIN_PASSWORD
+generate_password_if_empty KC_DB_PASSWORD
+generate_password_if_empty TESTADMIN_PASSWORD
+generate_password_if_empty TESTUSER_PASSWORD
+generate_password_if_empty TESTSERVICEUSER_PASSWORD
+generate_password_if_empty KEYCLOAK_NEXTCLOUD_CLIENT_SECRET
+
 # Date for documentation
 # Date for documentation
 SETUP_DATE=$(date '+%Y-%m-%d_%H-%M-%S')
 SETUP_DATE=$(date '+%Y-%m-%d_%H-%M-%S')
 
 
-# Generate passwords
-KEYCLOAK_ADMIN_PASSWORD=$(generate_password)
-KC_DB_PASSWORD=$(generate_password)
+# Create credentials content
+CREDENTIALS_CONTENT=$(cat <<EOL
+Setup Date: ${SETUP_DATE}
+
+Keycloak Admin Credentials:
+Username: admin
+Password: ${KEYCLOAK_ADMIN_PASSWORD}
+
+Keycloak Database Credentials:
+Username: keycloak
+Password: ${KC_DB_PASSWORD}
+
+Test User Credentials:
+Admin Password: ${TESTADMIN_PASSWORD}
+User Password: ${TESTUSER_PASSWORD}
+Service User Password: ${TESTSERVICEUSER_PASSWORD}
+Nextcloud Client Secret: ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}
+
+EOL
+)
+
+# Store credentials hash
+CREDENTIALS_HASH=$(echo "$CREDENTIALS_CONTENT" | sha256sum | awk '{print $1}')
+echo "$CREDENTIALS_HASH" > "${CREDENTIALS_DIR}/credentials_hash.txt"
+echo ">>> Credentials hash stored in: ${CREDENTIALS_DIR}/credentials_hash.txt"
+
+# Set GPG PASSPHRASE
+export GPG_PASSPHRASE=$(generate_password)
+# Set GPG agent environment variable
+export GPG_TTY=$(tty)
+
+echo ">>> Trying openssl encryption first"
+# Alternative Verschlüsselung mit Openssl
+echo "$CREDENTIALS_CONTENT" > "$TEMP_FILE"
+          if openssl enc -aes-256-cbc -pbkdf2 -salt -in "$TEMP_FILE" -out "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.enc" -k "$GPG_PASSPHRASE" ; then
+             echo ">>> Credentials encrypted successfully using openssl"
+             mv  "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.enc" "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
+        else
+           echo ">>> Openssl encryption failed, trying gpg"
+
+          # Attempt to kill existing gpg agent
+          gpgconf --kill gpg-agent 2>/dev/null
+          echo ">>> Attempting to manually start gpg-agent with pinentry-curses"
+          gpg-agent --daemon --pinentry-program /usr/bin/pinentry-curses
+          gpg-connect-agent /bye 2>/dev/null
+          eval $(gpg-agent --daemon)
+          gpg-connect-agent updatestartuptty /bye 2>/dev/null
+
+          # Attempt to encrypt credentials using GPG with error handling
+          if echo "$CREDENTIALS_CONTENT" | gpg --symmetric --cipher-algo AES256 -vvv -o "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" ; then
+                echo ">>> Credentials encrypted successfully using gpg."
+          else
+             echo ">>> GPG encryption failed. Attempting GPG encryption with password workaround."
+                # Attempt encryption with passphrase workaround
+                if echo "$CREDENTIALS_CONTENT" | gpg --batch --passphrase "$GPG_PASSPHRASE" --symmetric --cipher-algo AES256 -vvv -o "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"; then
+                      echo ">>> Credentials encrypted successfully using gpg with passphrase workaround."
+                else
+                   echo ">>> GPG encryption with passphrase workaround failed"
+                   exit 1
+                fi
+          fi
+       fi
+rm "$TEMP_FILE"
 
 
 # Create .env file in docker directory
 # Create .env file in docker directory
-cat > "${DOCKER_DIR}/.env" << EOL
+DOCKER_ENV_CONTENT=$(cat <<EOL
 # Generated on ${SETUP_DATE}
 # Generated on ${SETUP_DATE}
 # Keycloak Admin
 # Keycloak Admin
 KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
 KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
@@ -34,27 +166,69 @@ KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
 KC_DB_USERNAME=keycloak
 KC_DB_USERNAME=keycloak
 KC_DB_PASSWORD=${KC_DB_PASSWORD}
 KC_DB_PASSWORD=${KC_DB_PASSWORD}
 EOL
 EOL
+)
+create_env_file "$DOCKER_DIR/.env" "$DOCKER_ENV_CONTENT"
 
 
-# Create encrypted credentials documentation
-cat > "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt" << EOL
-Setup Date: ${SETUP_DATE}
+# Create .env file in scripts/setup/keycloak directory
+KEYCLOAK_ENV_CONTENT=$(cat <<EOL
+KEYCLOAK_URL=https://auth.mrx8086.com
+KEYCLOAK_ADMIN_USER=admin
+KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
+NEXTCLOUD_CLIENT_ID=nextcloud
+PAPERLESS_CLIENT_ID=paperless
+NODERED_CLIENT_ID=nodered
+TESTADMIN_PASSWORD=${TESTADMIN_PASSWORD}
+TESTUSER_PASSWORD=${TESTUSER_PASSWORD}
+TESTSERVICEUSER_PASSWORD=${TESTSERVICEUSER_PASSWORD}
+KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}
+EOL
+)
+create_env_file "$KEYCLOAK_SETUP_DIR/.env" "$KEYCLOAK_ENV_CONTENT"
 
 
-Keycloak Admin Credentials:
-Username: admin
-Password: ${KEYCLOAK_ADMIN_PASSWORD}
+echo ">>> Environment setup completed!"
 
 
-Keycloak Database Credentials:
-Username: keycloak
-Password: ${KC_DB_PASSWORD}
-EOL
+# --------------- KEYCLOAK KONFIGURATION ---------------
+echo ">>> Keycloak Konfiguration..."
+cd "$KEYCLOAK_SETUP_DIR"
+
+echo ">>> Starte setup_realm.js"
+node setup_realm.js
+
+cd "$PROJECT_ROOT"
+
+# --------------- NEXTCLOUD KONFIGURATION ---------------
+echo ">>> Nextcloud Konfiguration..."
+
+# Verify if variable is set from earlier in the script
+echo ">>> Debug: Checking original variable..."
+echo ">>> Debug: KEYCLOAK_NEXTCLOUD_CLIENT_SECRET = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
+
+# Try reading from .env file if variable is empty
+if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
+    echo ">>> Debug: Variable is empty, trying to read from .env file..."
+    KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(grep KEYCLOAK_NEXTCLOUD_CLIENT_SECRET "${KEYCLOAK_SETUP_DIR}/.env" | cut -d '=' -f2)
+    echo ">>> Debug: Value from .env file = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}"
+fi
+
+# Ensure we have a value
+if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then
+    echo ">>> Error: Could not get client secret value"
+    exit 1
+fi
+
+# Escape special characters in the secret for JSON
+ESCAPED_SECRET=$(echo "$KEYCLOAK_NEXTCLOUD_CLIENT_SECRET" | sed 's/["\]/\\&/g')
+echo ">>> Debug: Escaped secret = $ESCAPED_SECRET"
+
+# Create the extra vars
+EXTRA_VARS="{\"client_secret\": \"$ESCAPED_SECRET\"}"
+echo ">>> Debug: Extra vars = $EXTRA_VARS"
 
 
-# Encrypt credentials file
-gpg --symmetric --cipher-algo AES256 "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt"
-rm "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt"
+# Run Ansible with the extra vars
+sudo ansible-playbook \
+    -i "$ANSIBLE_INVENTORY" \
+    "$ANSIBLE_PLAYBOOK" \
+    --extra-vars "$EXTRA_VARS" \
+    -v
 
 
-echo "Environment setup completed!"
-echo "Credentials have been saved and encrypted in: ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
-echo ".env file has been created in: ${DOCKER_DIR}/.env"
-echo ""
-echo "To view credentials, use:"
-echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"
+echo ">>> Fertig"

+ 3 - 1
scripts/setup/keycloak/.env

@@ -6,4 +6,6 @@ PAPERLESS_CLIENT_ID=paperless
 NODERED_CLIENT_ID=nodered
 NODERED_CLIENT_ID=nodered
 TESTADMIN_PASSWORD=TestAdminPwd
 TESTADMIN_PASSWORD=TestAdminPwd
 TESTUSER_PASSWORD=TestUserPwd
 TESTUSER_PASSWORD=TestUserPwd
-KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=D939xgzoxi58T2XZShdUPZP4gsI0kBOu
+TESTSERVICEUSER_PASSWORD=TestServiceUserPwd
+KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=OSbJ08zyjBWChwBR7S6c1q4sU0d8zvEK
+NEXTCLOUD_URL=https://cloud.mrx8086.com

+ 214 - 0
scripts/setup/keycloak/create_groups_nextcloud_scope.js

@@ -0,0 +1,214 @@
+import dotenv from 'dotenv';
+import axios from 'axios';
+
+// Load environment variables
+dotenv.config();
+console.log('Environment variables loaded.');
+
+// Configuration constants
+const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
+const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
+const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
+const REALM_NAME = 'office-automation';
+
+console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME });
+
+// Helper function for API error handling
+const handleAxiosError = (error, operation, config, response) => {
+    console.error(`Error during ${operation}:`);
+    if (config) {
+        console.error('Request:', {
+            method: config.method,
+            url: config.url,
+            headers: config.headers,
+            data: config.data,
+        });
+    }
+    if (error.response) {
+        console.error('Response:', {
+            status: error.response.status,
+            data: error.response.data
+        });
+    } else {
+        console.error('Error Message:', error.message);
+    }
+    throw error;
+};
+
+// Get Admin Token
+async function getAdminToken() {
+    console.log('Getting admin token...');
+    try {
+        const response = await axios.post(
+            `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
+            new URLSearchParams({
+                'client_id': 'admin-cli',
+                'username': ADMIN_USERNAME,
+                'password': ADMIN_PASSWORD,
+                'grant_type': 'password'
+            }),
+            {
+                headers: {
+                    'Content-Type': 'application/x-www-form-urlencoded'
+                }
+            }
+        );
+        console.log('Admin token received.');
+        return response.data.access_token;
+    } catch (error) {
+        handleAxiosError(error, 'getting admin token');
+    }
+}
+
+async function getClientScope(token, scopeName) {
+    console.log(`Getting client scope "${scopeName}"...`);
+    try {
+        const response = await axios.get(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                },
+                params: {
+                    name: scopeName
+                }
+            }
+        );
+
+        if (response.data.length === 0) {
+            console.log(`Client Scope "${scopeName}" not found.`);
+            return null;
+        }
+        console.log(`Client scope "${scopeName}" found:`, response.data);
+        return response.data[0];
+    } catch (error) {
+        handleAxiosError(error, `getting client scope "${scopeName}"`, error.config, error.response);
+        return null;
+    }
+}
+
+async function createClientScope(token, scopeName, consentScreenText) {
+    console.log(`Attempting to create client scope "${scopeName}"...`);
+    try {
+        const response = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
+            {
+                "name": scopeName,
+                "protocol": "openid-connect",
+                "attributes": {},
+                "consentScreenText": consentScreenText,
+                "includeInTokenScope": true
+            },
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`,
+                    'Content-Type': 'application/json'
+                }
+            });
+        console.log(`Client scope "${scopeName}" created successfully`);
+        return response.data;
+    } catch (error) {
+        console.error(`Error creating client scope "${scopeName}":`, error);
+        handleAxiosError(error, `creating ${scopeName} client scope`, error.config, error.response);
+        return null;
+    }
+}
+
+async function createMapper(token, clientScopeId, mapperName) {
+    console.log(`Attempting to create mapper "${mapperName}" for client scope ID "${clientScopeId}"...`);
+    try {
+        const response = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScopeId}/protocol-mappers/models`,
+            {
+                "name": mapperName,
+                "protocol": "openid-connect",
+                "protocolMapper": "oidc-group-membership-mapper",
+                "config": {
+                    "full.path": "false",
+                    "id.token.claim": "false",
+                    "access.token.claim": "true",
+                    "userinfo.token.claim": "true",
+                    "claim.name": "groups",
+                    "add.to.introspection": "false"
+                }
+            },
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`,
+                    'Content-Type': 'application/json'
+                }
+            });
+        console.log(`Mapper "${mapperName}" created for client scope ID "${clientScopeId}"`);
+        return response.data;
+    } catch (error) {
+        console.error(`Error creating mapper "${mapperName}" for client scope ID "${clientScopeId}":`, error);
+        handleAxiosError(error, `creating mapper "${mapperName}"`, error.config, error.response);
+        return null;
+    }
+}
+
+async function createGroupsNextcloudScope(token) {
+    const scopeName = "groups-nextcloud-test";
+    const mapperName = "groups-mapper-test";
+
+    console.log("Starting createGroupsNextcloudScope");
+    let clientScope = await getClientScope(token, scopeName);
+
+    if (!clientScope) {
+        clientScope = await createClientScope(token, scopeName, "Grant access to user groups in nextcloud");
+    } else {
+        console.log(`Client scope "${scopeName}" exists, skipping creation. Details:`, clientScope);
+    }
+
+    if (clientScope) {
+        console.log("Check for mappers in groups-nextcloud-test scope");
+        try {
+            const mappersResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
+                {
+                    headers: {
+                        'Authorization': `Bearer ${token}`
+                    }
+                });
+            if (!mappersResponse.data.find(m => m.name === mapperName)) {
+                await createMapper(token, clientScope.id, mapperName);
+            } else {
+                console.log(`Mapper "${mapperName}" exists, skipping creation. Details:`, mappersResponse.data.find(m => m.name === mapperName));
+            }
+        } catch (error) {
+            console.error("Error checking for mappers:", error);
+            handleAxiosError(error, `checking mappers for ${scopeName}`, error.config, error.response);
+        }
+    }
+    console.log("Finished createGroupsNextcloudScope");
+}
+
+async function createSimpleTestScope(token) {
+    const uniqueSuffix = Date.now();
+    const simpleScopeName = `test-simple-scope-${uniqueSuffix}`;
+    console.log(`Attempting to create simple test scope with name: ${simpleScopeName}`);
+    const simpleScope = await createClientScope(token, simpleScopeName, "Simple test scope");
+    if (simpleScope) {
+        console.log(`Successfully created simple test scope:`, simpleScope);
+    }
+}
+
+// Main function
+async function setupRealm() {
+    try {
+        console.log('Starting Keycloak setup...');
+        const token = await getAdminToken();
+
+        // Create a simple test client scope
+        await createSimpleTestScope(token);
+
+        // Create client scope groups-nextcloud
+        await createGroupsNextcloudScope(token);
+
+        console.log('Setup completed successfully');
+
+    } catch (error) {
+        console.error('Setup failed:', error);
+        process.exit(1);
+    }
+}
+
+// Execute the script
+setupRealm();

+ 176 - 0
scripts/setup/keycloak/delete_groups_nextcloud_scope.js

@@ -0,0 +1,176 @@
+import dotenv from 'dotenv';
+import axios from 'axios';
+
+// Load environment variables
+dotenv.config();
+console.log('Environment variables loaded.');
+
+// Configuration constants
+const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
+const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
+const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
+const REALM_NAME = 'office-automation';
+
+console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME });
+
+
+// Helper function for API error handling
+const handleAxiosError = (error, operation, config, response) => {
+    console.error(`Error during ${operation}:`);
+    if (config) {
+        console.error('Request:', {
+            method: config.method,
+            url: config.url,
+            headers: config.headers,
+            data: config.data,
+        });
+    }
+    if (error.response) {
+        console.error('Response:', {
+            status: error.response.status,
+            data: error.response.data
+        });
+    } else {
+        console.error('Error Message:', error.message);
+    }
+    throw error;
+};
+
+
+// Get Admin Token
+async function getAdminToken() {
+    console.log('Getting admin token...');
+    try {
+        const response = await axios.post(
+            `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
+            new URLSearchParams({
+                'client_id': 'admin-cli',
+                'username': ADMIN_USERNAME,
+                'password': ADMIN_PASSWORD,
+                'grant_type': 'password'
+            }),
+            {
+                headers: {
+                    'Content-Type': 'application/x-www-form-urlencoded'
+                }
+            }
+        );
+        console.log('Admin token received.');
+        return response.data.access_token;
+    } catch (error) {
+        handleAxiosError(error, 'getting admin token');
+    }
+}
+
+
+async function getClientScope(token, scopeName) {
+    console.log(`Getting client scope ${scopeName}...`);
+    try {
+        const response = await axios.get(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                },
+                params: {
+                    name: scopeName
+                }
+            }
+        );
+
+        if (response.data.length === 0) {
+            console.log(`Client Scope ${scopeName} not found`);
+            return null;
+        }
+        console.log(`Client scope ${scopeName} found.`);
+        return response.data[0]
+    } catch (error) {
+        handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response);
+        return null;
+    }
+}
+
+
+async function deleteClientScope(token, scopeId) {
+    console.log(`Deleting client scope with ID ${scopeId}...`);
+    try {
+        await axios.delete(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${scopeId}`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                }
+            }
+        );
+        console.log(`Client scope with ID ${scopeId} deleted successfully.`);
+    } catch (error) {
+        handleAxiosError(error, `deleting client scope with ID ${scopeId}`);
+    }
+}
+
+async function deleteMapper(token, scopeId, mapperId) {
+   console.log(`Deleting mapper ${mapperId} of client scope with ID ${scopeId}`);
+    try {
+        await axios.delete(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${scopeId}/protocol-mappers/models/${mapperId}`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                }
+            }
+        );
+        console.log(`Mapper with ID ${mapperId} deleted successfully.`);
+    } catch (error) {
+         handleAxiosError(error, `deleting mapper ${mapperId} for client scope with ID ${scopeId}`);
+    }
+}
+
+async function getClientScopeMappers(token, scopeId) {
+    console.log(`Getting mappers for client scope with ID ${scopeId}...`);
+    try {
+         const response = await axios.get(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${scopeId}/protocol-mappers/models`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                }
+            }
+        );
+        console.log(`Mappers for client scope with ID ${scopeId} retrieved.`);
+        return response.data
+    } catch (error) {
+        handleAxiosError(error, `getting mappers for client scope with ID ${scopeId}`);
+        return [];
+    }
+}
+
+
+async function deleteGroupsNextcloudScope() {
+    try {
+        console.log('Starting deletion of groups-nextcloud scope and its mapper...');
+        const token = await getAdminToken();
+         const clientScope = await getClientScope(token, "groups-nextcloud");
+
+        if (!clientScope) {
+            console.log("Client scope groups-nextcloud not found, nothing to delete.")
+            return;
+        }
+
+        const mappers = await getClientScopeMappers(token, clientScope.id)
+        const groupsMapper = mappers.find(m => m.name === "groups-mapper");
+        if (groupsMapper)
+            await deleteMapper(token, clientScope.id, groupsMapper.id)
+        await deleteClientScope(token, clientScope.id);
+
+        console.log('Deletion of groups-nextcloud scope and its mapper completed successfully.');
+
+    } catch (error) {
+        console.error('Deletion failed:', error);
+        process.exit(1);
+    }
+}
+
+
+
+// Execute the deletion
+deleteGroupsNextcloudScope();

+ 302 - 286
scripts/setup/keycloak/setup_realm.js

@@ -1,19 +1,26 @@
 import dotenv from 'dotenv';
 import dotenv from 'dotenv';
 import axios from 'axios';
 import axios from 'axios';
 
 
-// Lade Umgebungsvariablen
+// Load environment variables
 dotenv.config();
 dotenv.config();
+console.log('Environment variables loaded.');
 
 
-// Konfigurationskonstanten
+// Configuration constants
 const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
 const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
 const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
 const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
 const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
 const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
 const REALM_NAME = 'office-automation';
 const REALM_NAME = 'office-automation';
 
 
-// Client IDs und deren Konfiguration
+console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME });
+
+// Client IDs and their configuration
 const CLIENTS = {
 const CLIENTS = {
     [process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud']: {
     [process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud']: {
-        redirectUris: ["https://cloud.mrx8086.com/*"]
+        redirectUris: [
+            `https://cloud.mrx8086.com/apps/sociallogin/custom_oidc/keycloak`,
+            `https://cloud.mrx8086.com/apps/user_oidc/code`
+        ],
+        postLogoutRedirectUris: ["https://cloud.mrx8086.com/*"]
     },
     },
     [process.env.PAPERLESS_CLIENT_ID || 'paperless']: {
     [process.env.PAPERLESS_CLIENT_ID || 'paperless']: {
         redirectUris: ["https://docs.mrx8086.com/*"]
         redirectUris: ["https://docs.mrx8086.com/*"]
@@ -23,16 +30,18 @@ const CLIENTS = {
     }
     }
 };
 };
 
 
-// Hilfsfunktion für API-Fehlerbehandlung
+console.log('CLIENTS configuration:', CLIENTS);
+
+// Helper function for API error handling
 const handleAxiosError = (error, operation, config, response) => {
 const handleAxiosError = (error, operation, config, response) => {
     console.error(`Error during ${operation}:`);
     console.error(`Error during ${operation}:`);
     if (config) {
     if (config) {
-      console.error('Request:', {
-          method: config.method,
-          url: config.url,
-          headers: config.headers,
-          data: config.data,
-      });
+        console.error('Request:', {
+            method: config.method,
+            url: config.url,
+            headers: config.headers,
+            data: config.data,
+        });
     }
     }
     if (error.response) {
     if (error.response) {
         console.error('Response:', {
         console.error('Response:', {
@@ -45,8 +54,9 @@ const handleAxiosError = (error, operation, config, response) => {
     throw error;
     throw error;
 };
 };
 
 
-// Admin Token abrufen
+// Get Admin Token
 async function getAdminToken() {
 async function getAdminToken() {
+    console.log('Getting admin token...');
     try {
     try {
         const response = await axios.post(
         const response = await axios.post(
             `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
             `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
@@ -62,165 +72,146 @@ async function getAdminToken() {
                 }
                 }
             }
             }
         );
         );
+        console.log('Admin token received.');
         return response.data.access_token;
         return response.data.access_token;
     } catch (error) {
     } catch (error) {
-         handleAxiosError(error, 'getting admin token');
+        handleAxiosError(error, 'getting admin token');
     }
     }
 }
 }
 
 
-// Prüfen ob Realm existiert
+// Check if Realm exists
 async function checkRealmExists(token) {
 async function checkRealmExists(token) {
+    console.log(`Checking if realm ${REALM_NAME} exists...`);
     try {
     try {
-        await axios.get(
-            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`,
-            {
-                headers: {
-                    'Authorization': `Bearer ${token}`
-                }
-            }
-        );
+        await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
+            headers: { 'Authorization': `Bearer ${token}` }
+        });
+        console.log(`Realm ${REALM_NAME} exists.`);
         return true;
         return true;
     } catch (error) {
     } catch (error) {
         if (error.response?.status === 404) {
         if (error.response?.status === 404) {
+            console.log(`Realm ${REALM_NAME} does not exist.`);
             return false;
             return false;
         }
         }
         handleAxiosError(error, 'checking realm existence');
         handleAxiosError(error, 'checking realm existence');
     }
     }
 }
 }
 
 
-
-// Funktion um Client Infos abzufragen
-async function getClient(token, clientId) {
+// Function to get client information by clientId
+async function getClientByClientId(token, clientId) {
+    console.log(`Getting client information for ${clientId}...`);
     try {
     try {
-        const response = await axios.get(
-            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
-            {
-                headers: {
-                    'Authorization': `Bearer ${token}`
-                },
-                params: {
-                    clientId: clientId
-                }
-            }
-        );
+        const response = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, {
+            headers: { 'Authorization': `Bearer ${token}` },
+            params: { clientId }
+        });
         if (response.data.length === 0) {
         if (response.data.length === 0) {
-            console.error(`Client ${clientId} not found`);
+            console.log(`Client ${clientId} not found`);
             return null;
             return null;
         }
         }
-
+        console.log(`Client ${clientId} found.`);
         return response.data[0];
         return response.data[0];
     } catch (error) {
     } catch (error) {
-       handleAxiosError(error, `getting client ${clientId}`);
+        handleAxiosError(error, `getting client ${clientId}`);
+        return null;
     }
     }
 }
 }
 
 
-// Prüfen ob Client existiert
-async function checkClientExists(token, clientId) {
-    const client = await getClient(token, clientId);
-    return !!client;
-}
-
+// Check if client exists
+const checkClientExists = async (token, clientId) => !!await getClientByClientId(token, clientId);
 
 
+// Get client mappers by client ID
 async function getClientMappers(token, clientId) {
 async function getClientMappers(token, clientId) {
+    console.log(`Getting client mappers for ${clientId}...`);
+    const client = await getClientByClientId(token, clientId);
+    if (!client) {
+        console.log(`Client ${clientId} not found, no mappers to get.`);
+        return [];
+    }
     try {
     try {
-        const client = await getClient(token, clientId);
-        if (!client) {
-            return [];
-        }
         const response = await axios.get(
         const response = await axios.get(
             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
-            {
-                headers: {
-                    'Authorization': `Bearer ${token}`
-                }
-            }
+            { headers: { 'Authorization': `Bearer ${token}` } }
         );
         );
+        console.log(`Client mappers for ${clientId} retrieved.`);
         return response.data;
         return response.data;
     } catch (error) {
     } catch (error) {
-      handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response);
+        handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response);
         return [];
         return [];
     }
     }
 }
 }
 
 
-async function getClientScopes(token, clientId){
+// Get client scopes for a client
+async function getClientScopes(token, clientId) {
+    console.log(`Getting client scopes for ${clientId}...`);
+    const client = await getClientByClientId(token, clientId);
+    if (!client) {
+        console.log(`Client ${clientId} not found, no client scopes to get.`);
+        return [];
+    }
     try {
     try {
-        const client = await getClient(token, clientId);
-        if(!client)
-            return [];
-
         const response = await axios.get(
         const response = await axios.get(
             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/client-scopes`,
             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/client-scopes`,
-            {
-                 headers: {
-                        'Authorization': `Bearer ${token}`
-                    }
-            }
+            { headers: { 'Authorization': `Bearer ${token}` } }
         );
         );
-
+        console.log(`Client scopes for ${clientId} retrieved.`);
         return response.data;
         return response.data;
-
-    } catch(error){
-      handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response);
+    } catch (error) {
+        handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response);
         return [];
         return [];
     }
     }
 }
 }
 
 
+// Get a specific client scope by name
 async function getClientScope(token, scopeName) {
 async function getClientScope(token, scopeName) {
+    console.log(`Getting client scope "${scopeName}"...`);
     try {
     try {
         const response = await axios.get(
         const response = await axios.get(
             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
-             {
-                 headers: {
-                     'Authorization': `Bearer ${token}`
-                 },
-                params: {
-                  name: scopeName
-                }
-              }
+            { headers: { 'Authorization': `Bearer ${token}` } }
         );
         );
-
-        if(response.data.length === 0){
-            console.error(`Client Scope ${scopeName} not found`);
+        const foundScope = response.data.find(scope => scope.name === scopeName);
+        if (!foundScope) {
+            console.log(`Client Scope "${scopeName}" not found`);
             return null;
             return null;
         }
         }
-
-        return response.data[0]
-    } catch (error){
+        console.log(`Client scope "${scopeName}" found:`, foundScope);
+        return foundScope;
+    } catch (error) {
         handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response);
         handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response);
         return null;
         return null;
     }
     }
 }
 }
 
 
-async function addDefaultClientScope(token, clientId, scopeName){
+// Add a default client scope to a client
+async function addDefaultClientScope(token, clientId, scopeName) {
+    console.log(`Adding client scope "${scopeName}" as default for client "${clientId}"...`);
+    const client = await getClientByClientId(token, clientId);
+    const scope = await getClientScope(token, scopeName);
+    if (!client || !scope) {
+        console.log(`Client "${clientId}" or scope "${scopeName}" not found, cannot add as default scope.`);
+        return;
+    }
     try {
     try {
-        const client = await getClient(token, clientId);
-        const scope = await getClientScope(token, scopeName);
-         if(!client || !scope){
-           return null;
-         }
-
-
         await axios.put(
         await axios.put(
-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`,
-                null,
-                {
-                  headers: {
-                        'Authorization': `Bearer ${token}`,
-                        'Content-Type': 'application/json'
-                    }
-               }
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`,
+            null,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`,
+                    'Content-Type': 'application/json'
+                }
+            }
         );
         );
-
-        console.log(`Client scope ${scopeName} added as default scope for client ${clientId}`)
-
-    } catch(error){
-       handleAxiosError(error, `adding client scope ${scopeName} as default for client ${clientId}`);
+        console.log(`Client scope "${scopeName}" added as default scope for client "${clientId}"`);
+    } catch (error) {
+        handleAxiosError(error, `adding client scope "${scopeName}" as default for client "${clientId}"`);
     }
     }
 }
 }
 
 
-
-// Realm erstellen
+// Create Realm
 async function createRealm(token) {
 async function createRealm(token) {
+    console.log(`Creating realm ${REALM_NAME}...`);
     const realmConfig = {
     const realmConfig = {
         realm: REALM_NAME,
         realm: REALM_NAME,
         enabled: true,
         enabled: true,
@@ -250,67 +241,47 @@ async function createRealm(token) {
         webAuthnPolicyUserVerificationRequirement: "preferred",
         webAuthnPolicyUserVerificationRequirement: "preferred",
         webAuthnPolicyCreateTimeout: 0,
         webAuthnPolicyCreateTimeout: 0,
         webAuthnPolicyAvoidSameAuthenticatorRegister: false,
         webAuthnPolicyAvoidSameAuthenticatorRegister: false,
-        defaultDefaultClientScopes: [
-            "email",
-            "profile",
-            "roles",
-            "web-origins"
-        ],
-        defaultOptionalClientScopes: [
-            "address",
-            "phone",
-            "offline_access",
-            "microprofile-jwt"
-        ]
+        defaultDefaultClientScopes: ["email", "profile", "roles", "web-origins"],
+        defaultOptionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"]
     };
     };
-
     try {
     try {
-        await axios.post(
-            `${KEYCLOAK_URL}/admin/realms`,
-            realmConfig,
-            {
-                headers: {
-                    'Authorization': `Bearer ${token}`,
-                    'Content-Type': 'application/json'
-                }
+        await axios.post(`${KEYCLOAK_URL}/admin/realms`, realmConfig, {
+            headers: {
+                'Authorization': `Bearer ${token}`,
+                'Content-Type': 'application/json'
             }
             }
-        );
+        });
         console.log('Realm created successfully');
         console.log('Realm created successfully');
     } catch (error) {
     } catch (error) {
         handleAxiosError(error, 'creating realm');
         handleAxiosError(error, 'creating realm');
     }
     }
 }
 }
 
 
-// Client erstellen
+// Create client and manage mappers
 async function createClient(token, clientId, clientName, redirectUris) {
 async function createClient(token, clientId, clientName, redirectUris) {
-    let client;
-    const clientExists = await checkClientExists(token, clientId);
+    console.log(`Creating client "${clientId}"...`);
+    let client = await getClientByClientId(token, clientId);
 
 
-    if (!clientExists) {
+    if (!client) {
         const clientConfig = {
         const clientConfig = {
             clientId: clientId,
             clientId: clientId,
             name: clientName,
             name: clientName,
             enabled: true,
             enabled: true,
             protocol: "openid-connect",
             protocol: "openid-connect",
             publicClient: false,
             publicClient: false,
-            authorizationServicesEnabled: true,
-            serviceAccountsEnabled: true,
+            authorizationServicesEnabled: false,
+            serviceAccountsEnabled: false,
             standardFlowEnabled: true,
             standardFlowEnabled: true,
             implicitFlowEnabled: false,
             implicitFlowEnabled: false,
             directAccessGrantsEnabled: true,
             directAccessGrantsEnabled: true,
             redirectUris: redirectUris,
             redirectUris: redirectUris,
-            webOrigins: ["+"],
-            defaultClientScopes: [
-                "roles"
-            ],
-            optionalClientScopes: [
-                "address",
-                "phone",
-                "offline_access",
-                "microprofile-jwt"
-            ]
+             webOrigins: ["+"],
+            defaultClientScopes: ["roles"],
+            optionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"],
+            rootUrl: process.env.NEXTCLOUD_URL,
+            baseUrl: process.env.NEXTCLOUD_URL,
+            adminUrl: process.env.NEXTCLOUD_URL,
         };
         };
-
         try {
         try {
             const response = await axios.post(
             const response = await axios.post(
                 `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
                 `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
@@ -322,21 +293,34 @@ async function createClient(token, clientId, clientName, redirectUris) {
                     }
                     }
                 }
                 }
             );
             );
-            console.log(`Client ${clientId} created successfully`);
+            console.log(`Client "${clientId}" created successfully`);
             client = response.data;
             client = response.data;
         } catch (error) {
         } catch (error) {
             handleAxiosError(error, `creating client: ${clientId}`);
             handleAxiosError(error, `creating client: ${clientId}`);
             return;
             return;
         }
         }
     } else {
     } else {
-        client = await getClient(token, clientId);
-        console.log(`Client ${clientId} already exists, checking mappers`);
+        console.log(`Client "${clientId}" already exists, checking mappers`);
     }
     }
 
 
-     if (client) {
-        
-        const existingMappers = await getClientMappers(token, clientId)
+    if (client) {
+        try {
+            await axios.put(
+                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`,
+                { ...client, secret: process.env[`KEYCLOAK_${clientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`] },
+                {
+                    headers: {
+                        'Authorization': `Bearer ${token}`,
+                        'Content-Type': 'application/json'
+                    }
+                }
+            );
+            console.log(`Set client secret for client: ${clientId}`);
+        } catch (error) {
+            handleAxiosError(error, `setting client secret for client: ${clientId}`, error.config, error.response);
+        }
 
 
+        const existingMappers = await getClientMappers(token, clientId);
         const requiredMappers = [
         const requiredMappers = [
             {
             {
                 name: "groups",
                 name: "groups",
@@ -367,10 +351,8 @@ async function createClient(token, clientId, clientName, redirectUris) {
 
 
         for (const mapper of requiredMappers) {
         for (const mapper of requiredMappers) {
             const existingMapper = existingMappers.find(m => m.name === mapper.name);
             const existingMapper = existingMappers.find(m => m.name === mapper.name);
-            
             try {
             try {
                 if (existingMapper) {
                 if (existingMapper) {
-                    // Update existierenden Mapper
                     await axios.put(
                     await axios.put(
                         `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models/${existingMapper.id}`,
                         `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models/${existingMapper.id}`,
                         { ...existingMapper, ...mapper },
                         { ...existingMapper, ...mapper },
@@ -381,9 +363,8 @@ async function createClient(token, clientId, clientName, redirectUris) {
                             }
                             }
                         }
                         }
                     );
                     );
-                    console.log(`Mapper ${mapper.name} updated for client ${clientId}`);
+                    console.log(`Mapper "${mapper.name}" updated for client "${clientId}"`);
                 } else {
                 } else {
-                    // Erstelle neuen Mapper
                     await axios.post(
                     await axios.post(
                         `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
                         `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
                         mapper,
                         mapper,
@@ -394,70 +375,69 @@ async function createClient(token, clientId, clientName, redirectUris) {
                             }
                             }
                         }
                         }
                     );
                     );
-                    console.log(`Mapper ${mapper.name} created for client ${clientId}`);
+                    console.log(`Mapper "${mapper.name}" created for client "${clientId}"`);
                 }
                 }
             } catch (error) {
             } catch (error) {
-                handleAxiosError(error, `managing mapper ${mapper.name} for client ${clientId}`, error.config, error.response);
-                // Wir werfen den Fehler nicht weiter, damit andere Mapper noch verarbeitet werden können
+                handleAxiosError(error, `managing mapper "${mapper.name}" for client "${clientId}"`, error.config, error.response);
+            }
+        }
+
+        if (clientId.includes("nextcloud")) {
+            await addDefaultClientScope(token, clientId, "openid");
+            await addDefaultClientScope(token, clientId, "profile");
+            await addDefaultClientScope(token, clientId, "groups-nextcloud");
+
+            try {
+                await axios.put(
+                    `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`,
+                    { ...client, defaultClientScopes: client.defaultClientScopes.filter(c => c !== "nextcloud-dedicated") },
+                    {
+                        headers: {
+                            'Authorization': `Bearer ${token}`,
+                            'Content-Type': 'application/json'
+                        }
+                    }
+                );
+                console.log(`Removed client scope nextcloud-dedicated from client: ${clientId}`);
+            } catch (error) {
+                handleAxiosError(error, `removing client scope nextcloud-dedicated from client: ${clientId}`, error.config, error.response);
             }
             }
         }
         }
     }
     }
 }
 }
 
 
-
-// Gruppen erstellen
+// Create default groups
 async function createDefaultGroups(token) {
 async function createDefaultGroups(token) {
+    console.log('Creating default groups...');
     const groups = [
     const groups = [
-        {
-            name: "Administrators",
-            path: "/Administrators",
-            attributes: {
-                "description": ["Full system access"]
-            }
-        },
-        {
-            name: "Users",
-            path: "/Users",
-            attributes: {
-                "description": ["Regular system users"]
-            }
-        }
+        { name: "nextcloud-admins", path: "/nextcloud-admins", attributes: { "description": ["Nextcloud administrators"] } },
+        { name: "nextcloud-users", path: "/nextcloud-users", attributes: { "description": ["Nextcloud regular users"] } },
+        { name: "nextcloud-youpi", path: "/nextcloud-youpi", attributes: { "description": ["Nextcloud youpi"] } },
+        { name: "nextcloud-service", path: "/nextcloud-service", attributes: { "description": ["Nextcloud service user"] } }
     ];
     ];
 
 
     for (const group of groups) {
     for (const group of groups) {
         try {
         try {
-            // Prüfen ob Gruppe existiert
-            const existingGroup = await axios.get(
-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`,
-                {
-                    headers: {
-                        'Authorization': `Bearer ${token}`
-                    },
-                    params: {
-                        search: group.name,
-                    }
-                }
-            );
+            const existingGroups = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, {
+                headers: { 'Authorization': `Bearer ${token}` },
+                params: { search: group.name, exact: true } // Added exact: true for precise matching
+            });
 
 
-            if (existingGroup.data.length > 0) {
-                console.log(`Group ${group.name} already exists`);
+            if (existingGroups.data.length > 0) {
+                console.log(`Group "${group.name}" already exists`);
                 continue;
                 continue;
             }
             }
 
 
-            await axios.post(
-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`,
-                group,
-                {
-                    headers: {
-                        'Authorization': `Bearer ${token}`,
-                        'Content-Type': 'application/json'
-                    }
+            await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, group, {
+                headers: {
+                    'Authorization': `Bearer ${token}`,
+                    'Content-Type': 'application/json'
                 }
                 }
-            );
-            console.log(`Group ${group.name} created successfully`);
+            });
+            console.log(`Group "${group.name}" created successfully`);
         } catch (error) {
         } catch (error) {
             if (error.response?.status === 409) {
             if (error.response?.status === 409) {
-                console.log(`Group ${group.name} already exists`);
+                console.log(`Group "${group.name}" already exists`);
             } else {
             } else {
                 handleAxiosError(error, `creating group: ${group.name}`);
                 handleAxiosError(error, `creating group: ${group.name}`);
             }
             }
@@ -465,44 +445,44 @@ async function createDefaultGroups(token) {
     }
     }
 }
 }
 
 
-
+// Create a test token for a user
 async function createTestToken(token, username) {
 async function createTestToken(token, username) {
+    console.log(`Creating test token for user "${username}"...`);
     try {
     try {
-         const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud';
-         const client = await getClient(token, nextcloudClientId);
+        const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud';
+        const client = await getClientByClientId(token, nextcloudClientId);
 
 
-        if (!client)
+        if (!client) {
+            console.log(`Client "${nextcloudClientId}" not found, cannot create test token.`);
             return null;
             return null;
+        }
         const response = await axios.post(
         const response = await axios.post(
             `${KEYCLOAK_URL}/realms/${REALM_NAME}/protocol/openid-connect/token`,
             `${KEYCLOAK_URL}/realms/${REALM_NAME}/protocol/openid-connect/token`,
             new URLSearchParams({
             new URLSearchParams({
                 'client_id': nextcloudClientId,
                 'client_id': nextcloudClientId,
-                'client_secret': process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET,
+                'client_secret': process.env[`KEYCLOAK_${nextcloudClientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`],
                 'username': username,
                 'username': username,
                 'password': process.env.TESTADMIN_PASSWORD || "initial123!",
                 'password': process.env.TESTADMIN_PASSWORD || "initial123!",
                 'grant_type': 'password'
                 'grant_type': 'password'
             }),
             }),
-            {
-                headers: {
-                    'Content-Type': 'application/x-www-form-urlencoded'
-                }
-            }
+            { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
         );
         );
+        console.log(`Test token for user "${username}" created.`);
         return response.data.access_token;
         return response.data.access_token;
     } catch (error) {
     } catch (error) {
         handleAxiosError(error, `getting test token for ${username}`, error.config, error.response);
         handleAxiosError(error, `getting test token for ${username}`, error.config, error.response);
+        return null;
     }
     }
 }
 }
 
 
-// Funktion zum Decodieren eines JWT-Tokens
+// Function to decode a JWT token
 function decodeToken(token) {
 function decodeToken(token) {
     try {
     try {
         const base64Url = token.split('.')[1];
         const base64Url = token.split('.')[1];
         const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
         const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
-        const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
-            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
-        }).join(''));
-
+        const jsonPayload = decodeURIComponent(atob(base64).split('').map(c =>
+            '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
+        ).join(''));
         return JSON.parse(jsonPayload);
         return JSON.parse(jsonPayload);
     } catch (error) {
     } catch (error) {
         console.error("Error decoding token:", error.message);
         console.error("Error decoding token:", error.message);
@@ -510,87 +490,128 @@ function decodeToken(token) {
     }
     }
 }
 }
 
 
-// Test-User erstellen
+// Create initial users
 async function createInitialUsers(token) {
 async function createInitialUsers(token) {
+    console.log('Creating initial users...');
     const users = [
     const users = [
-        {
-            username: "testadmin",
-            enabled: true,
-            emailVerified: true,
-            firstName: "Test",
-            lastName: "Admin",
-            email: "testadmin@mrx8086.com",
-            credentials: [{
-                type: "password",
-                value: process.env.TESTADMIN_PASSWORD || "initial123!",
-                temporary: true
-            }],
-            groups: ["/Administrators"]
-        },
-        {
-            username: "testuser",
-            enabled: true,
-            emailVerified: true,
-            firstName: "Test",
-            lastName: "User",
-            email: "testuser@mrx8086.com",
-            credentials: [{
-                type: "password",
-                value: process.env.TESTUSER_PASSWORD || "initial123!",
-                temporary: true
-            }],
-            groups: ["/Users"]
-        }
+        { username: "testadmin", enabled: true, emailVerified: true, firstName: "Test", lastName: "Admin", email: "testadmin@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTADMIN_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-admins", "nextcloud-users"] },
+        { username: "testuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "User", email: "testuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-users", "nextcloud-youpi"] },
+        { username: "testserviceuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "Service User", email: "testserviceuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTSERVICEUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-service"] }
     ];
     ];
 
 
     for (const user of users) {
     for (const user of users) {
         try {
         try {
-            // Prüfen ob User existiert
-            const existingUsers = await axios.get(
-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`,
-                {
-                    headers: {
-                        'Authorization': `Bearer ${token}`
-                    },
-                    params: {
-                        username: user.username,
-                        exact: true
-                    }
-                }
-            );
+            const existingUsers = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, {
+                headers: { 'Authorization': `Bearer ${token}` },
+                params: { username: user.username, exact: true } // Added exact: true for precise matching
+            });
 
 
             if (existingUsers.data.length > 0) {
             if (existingUsers.data.length > 0) {
-                console.log(`User ${user.username} already exists`);
+                console.log(`User "${user.username}" already exists`);
                 continue;
                 continue;
             }
             }
 
 
-            // User erstellen
-            await axios.post(
-                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`,
-                user,
+            await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, user, {
+                headers: {
+                    'Authorization': `Bearer ${token}`,
+                    'Content-Type': 'application/json'
+                }
+            });
+            console.log(`User "${user.username}" created successfully`);
+
+        } catch (error) {
+            handleAxiosError(error, `creating user: ${user.username}`, error.config, error.response);
+        }
+    }
+}
+
+async function createGroupsNextcloudScope(token) {
+    const scopeName = "groups-nextcloud";
+    const mapperName = "groups-mapper";
+    console.log(`Starting createGroupsNextcloudScope`);
+    let clientScope = await getClientScope(token, scopeName);
+
+    if (!clientScope) {
+        try {
+            console.log(`Creating client scope "${scopeName}"`);
+            clientScope = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
+                {
+                    "name": scopeName,
+                    "protocol": "openid-connect",
+                    "description": "Provides access to user group information for Nextcloud.", // Hinzugefügt: Beschreibung
+                    "attributes": {},
+                    "consentScreenText": "Grant access to user groups in Nextcloud",
+                    "includeInTokenScope": true
+                },
                 {
                 {
                     headers: {
                     headers: {
                         'Authorization': `Bearer ${token}`,
                         'Authorization': `Bearer ${token}`,
                         'Content-Type': 'application/json'
                         'Content-Type': 'application/json'
                     }
                     }
-                }
-            );
-            console.log(`User ${user.username} created successfully`);
+                });
+            console.log(`Client scope "${scopeName}" created successfully`);
+            clientScope = response.data;
+        } catch (error) {
+            console.error(`Error creating client scope "${scopeName}":`, error);
+            handleAxiosError(error, `creating ${scopeName} client scope`, error.config, error.response);
+            return;
+        }
+    } else {
+        console.log(`Client scope "${scopeName}" exists, skipping creation`);
+    }
 
 
+    console.log("Client scope creation step finished");
+    if (clientScope) {
+        console.log(`Check for mapper "${mapperName}" in scope "${scopeName}"`);
+        try {
+            const mappersResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
+                { headers: { 'Authorization': `Bearer ${token}` } }
+            );
+            if (!mappersResponse.data.find(m => m.name === mapperName)) {
+                try {
+                    await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
+                        {
+                            "name": mapperName,
+                            "protocol": "openid-connect",
+                            "protocolMapper": "oidc-group-membership-mapper",
+                            "config": {
+                                "full.path": "false",
+                                "id.token.claim": "false",
+                                "access.token.claim": "true",
+                                "userinfo.token.claim": "true",
+                                "claim.name": "groups",
+                                "add.to.introspection": "false"
+                            }
+                        },
+                        {
+                            headers: {
+                                'Authorization': `Bearer ${token}`,
+                                'Content-Type': 'application/json'
+                            }
+                        });
+                    console.log(`Mapper "${mapperName}" created for client scope "${scopeName}"`);
+                } catch (error) {
+                    console.error(`Error creating mapper "${mapperName}" for scope "${scopeName}":`, error);
+                    handleAxiosError(error, `creating mapper for ${scopeName}`, error.config, error.response);
+                }
+            } else {
+                console.log(`Mapper "${mapperName}" exists in client scope "${scopeName}", skipping creation`);
+            }
         } catch (error) {
         } catch (error) {
-            handleAxiosError(error, `creating user: ${user.username}`, error.config, error.response);
+            console.error("Error checking for mappers:", error);
+            handleAxiosError(error, `checking mappers for ${scopeName}`, error.config, error.response);
         }
         }
     }
     }
+    console.log("Finished createGroupsNextcloudScope");
 }
 }
 
 
-
-// Hauptfunktion
+// Main function
 async function setupRealm() {
 async function setupRealm() {
     try {
     try {
         console.log('Starting Keycloak setup...');
         console.log('Starting Keycloak setup...');
         const token = await getAdminToken();
         const token = await getAdminToken();
 
 
-        // Prüfe ob Realm existiert
+        // Check if realm exists
         const realmExists = await checkRealmExists(token);
         const realmExists = await checkRealmExists(token);
 
 
         if (!realmExists) {
         if (!realmExists) {
@@ -600,41 +621,37 @@ async function setupRealm() {
             console.log('Realm already exists, skipping base setup');
             console.log('Realm already exists, skipping base setup');
         }
         }
 
 
-        // Clients erstellen
+        // Create client scope groups-nextcloud
+        await createGroupsNextcloudScope(token);
+
+        // Create clients
         for (const clientId in CLIENTS) {
         for (const clientId in CLIENTS) {
             await createClient(token, clientId, clientId, CLIENTS[clientId].redirectUris);
             await createClient(token, clientId, clientId, CLIENTS[clientId].redirectUris);
         }
         }
 
 
-       const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud';
-        await addDefaultClientScope(token, nextcloudClientId, "openid");
-
-        // Gruppen erstellen
+        // Create groups
         await createDefaultGroups(token);
         await createDefaultGroups(token);
 
 
-        // Test User erstellen
+        // Create test users
         await createInitialUsers(token);
         await createInitialUsers(token);
 
 
-
-        // Konfiguration des Office-Automation Realms mit Admin Token auslesen
+        // Read the configuration of the Office-Automation realm with Admin Token
         if (token) {
         if (token) {
             console.log("Master Realm Admin Token:", token);
             console.log("Master Realm Admin Token:", token);
-
             try {
             try {
                 const realmConfig = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
                 const realmConfig = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
-                    headers: {
-                        'Authorization': `Bearer ${token}`
-                    }
+                    headers: { 'Authorization': `Bearer ${token}` }
                 });
                 });
-                console.log("Office Automation Realm Configuration:", realmConfig.data)
+                console.log("Office Automation Realm Configuration:", realmConfig.data);
             } catch (error) {
             } catch (error) {
                 handleAxiosError(error, 'getting office realm configuration', error.config, error.response);
                 handleAxiosError(error, 'getting office realm configuration', error.config, error.response);
             }
             }
         } else {
         } else {
-            console.error("Error getting Master Realm admin token")
+            console.error("Error getting Master Realm admin token");
         }
         }
 
 
-        // Test Token erstellen
-        const testToken = await createTestToken(token, "testadmin@mrx8086.com");
+        // Create Test Token
+        const testToken = await createTestToken(token, "testadmin");
 
 
         if (testToken) {
         if (testToken) {
             console.log("Test Token generated successfully!");
             console.log("Test Token generated successfully!");
@@ -644,7 +661,6 @@ async function setupRealm() {
         } else {
         } else {
             console.error("Error generating Test Token");
             console.error("Error generating Test Token");
         }
         }
-
         console.log('Setup completed successfully');
         console.log('Setup completed successfully');
 
 
     } catch (error) {
     } catch (error) {
@@ -653,5 +669,5 @@ async function setupRealm() {
     }
     }
 }
 }
 
 
-// Script ausführen
+// Execute the script
 setupRealm();
 setupRealm();

+ 25 - 20
scripts/setup/keycloak/test_realm.js

@@ -6,7 +6,7 @@ dotenv.config();
 const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
 const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
 const NEXTCLOUD_CLIENT_ID = process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud';
 const NEXTCLOUD_CLIENT_ID = process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud';
 const TESTADMIN_USERNAME = "testadmin@mrx8086.com";
 const TESTADMIN_USERNAME = "testadmin@mrx8086.com";
-const TESTADMIN_PASSWORD = process.env.TESTADMIN_PASSWORD || "initial123!";
+const TESTADMIN_PASSWORD = process.env.TESTADMIN_PASSWORD;
 const REALM_NAME = 'office-automation';
 const REALM_NAME = 'office-automation';
 const CLIENT_SECRET = process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET;
 const CLIENT_SECRET = process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET;
 
 
@@ -74,28 +74,33 @@ function decodeToken(token) {
     }
     }
 }
 }
 
 
+// Prüfe ob ein Admin Token korrekt generiert werden kann
 async function testKeycloakLogin() {
 async function testKeycloakLogin() {
-  try {
-    const accessToken = await getAccessToken(TESTADMIN_USERNAME, TESTADMIN_PASSWORD);
+    try {
+        const accessToken = await getAccessToken(TESTADMIN_USERNAME, TESTADMIN_PASSWORD);
 
 
-    if (!accessToken) {
-      console.error('Failed to get access token.');
-      return;
-    }
-    console.log('Access Token:', accessToken);
-    const decodedToken = decodeToken(accessToken);
-    if (decodedToken) {
-      console.log('Decoded Access Token:', decodedToken);
-      if (decodedToken.groups.includes('/Administrators')){
-        console.log("Admin Group is set correctly!")
-      } else {
-          console.error("Admin Group is not set correctly!")
-      }
-    }
+        if (!accessToken) {
+            console.error('Failed to get access token.');
+            return;
+        }
+        console.log('Access Token:', accessToken);
+        const decodedToken = decodeToken(accessToken);
+        if(decodedToken) {
+             console.log('Decoded Access Token:', decodedToken);
+            if (Array.isArray(decodedToken.groups) && decodedToken.groups.includes('/nextcloud-admins')){
+                console.log("Admin Group is set correctly!")
+            } else if (typeof decodedToken.groups === 'string' && decodedToken.groups.includes('/nextcloud-admins')) {
+                console.log("Admin Group is set correctly!")
+            }
+             else {
+                 console.error("Admin Group is not set correctly!")
+            }
+        }
 
 
-  } catch (error) {
-    console.error('An error occurred:', error);
-  }
+
+    } catch (error) {
+        console.error('An error occurred:', error);
+    }
 }
 }
 
 
 testKeycloakLogin();
 testKeycloakLogin();