diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..df5fc6a --- /dev/null +++ b/.env.example @@ -0,0 +1,35 @@ +# AOVI Environment Configuration +# Copy this file to .env and fill in your values + +# Application Security +SUPERADMIN_DEFAULT_PASSWORD=your-superadmin-password-here +JWT_SECRET=your-jwt-secret-here-make-it-long-and-random-64-characters-minimum + +# Keycloak Configuration +KEYCLOAK_ADMIN_PASSWORD=admin-password-here +KEYCLOAK_DB_PASSWORD=postgres-password-here +KEYCLOAK_CLIENT_SECRET=your-keycloak-client-secret-change-in-production + +# Email Configuration for Keycloak SMTP +KEYCLOAK_SMTP_HOST=smtp.gmail.com +KEYCLOAK_SMTP_PORT=587 +KEYCLOAK_SMTP_FROM=noreply@aovi.local +KEYCLOAK_SMTP_USERNAME=your-smtp-username +KEYCLOAK_SMTP_PASSWORD=your-smtp-password + +# AOVI App SMTP Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=your-gmail-address@gmail.com +SMTP_PASS=your-gmail-app-password-16-characters +MAIL_FROM=your-gmail-address@gmail.com +MAIL_FROM_NAME=AOVI Platform + +# Application URLs +APP_URL=http://localhost:1041 +KEYCLOAK_URL=http://localhost:8080 + +# Port Configuration +SERVER_PORT=3000 +EXTERNAL_PORT=1041 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 14b4269..588a5bf 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,17 @@ web_modules/ .env.test.local .env.production.local .env.local +.env.production + +# Production files +logs/ +*.log.* +deployment/ +ssl/ +secrets/ + +# Keycloak files with secrets (use template instead) +keycloak/import/aovi-realm.json # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/Dockerfile b/Dockerfile index f4992ea..8f9999b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,11 +12,6 @@ RUN npm install # Copy the application files COPY . . -# Copy the script to generate secret JWT token (user data encryption) and make it executable -# COPY generate_secret_JWT.sh . -# RUN chmod +x generate_secret_JWT.sh - - # Expose the port your app is running on EXPOSE 3000 diff --git a/backup-infrastructure/README.md b/backup-infrastructure/README.md new file mode 100644 index 0000000..1b7aa0c --- /dev/null +++ b/backup-infrastructure/README.md @@ -0,0 +1,329 @@ +# Remote Database Backup + +Automated backup solution for MongoDB and PostgreSQL databases to a remote server via SSH. + +## Prerequisites + +Before running backups, you **MUST** complete these steps: + +1. **Remote server accessible via SSH** +2. **SSH key-based authentication configured** +3. **Docker containers running**: `aovi-mongodb` and `aovi-postgres` +4. **Sufficient disk space on remote server** + +--- + +## Setup Instructions (Run Once) + +### Step 1: Configure Remote Server Details + +Open `remote-backup.sh` in an editor: +```bash +nano remote-backup.sh +``` + +**Find lines 13-16 and update with YOUR values:** + +```bash +REMOTE_USER="backup" # ← Change to your SSH username +REMOTE_HOST="backup.example.com" # ← Change to your server IP/hostname +REMOTE_PATH="/backups/aovi" # ← Change to your backup directory path +SSH_KEY="$HOME/.ssh/id_rsa" # ← Change if using different SSH key +``` + +**Example:** +```bash +REMOTE_USER="ubuntu" +REMOTE_HOST="192.168.1.100" +REMOTE_PATH="/home/ubuntu/backups" +SSH_KEY="$HOME/.ssh/id_rsa" +``` + +Save and exit (Ctrl+X, Y, Enter in nano). + +### Step 2: Setup SSH Key Authentication + +**A. Generate SSH key (if you don't have one):** +```bash +ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa +# Press Enter for all prompts (no passphrase recommended for automation) +``` + +**B. Copy SSH key to remote server:** +```bash +ssh-copy-id -i ~/.ssh/id_rsa.pub YOUR_USER@YOUR_SERVER +# Replace YOUR_USER and YOUR_SERVER with actual values +``` + +Example: +```bash +ssh-copy-id -i ~/.ssh/id_rsa.pub ubuntu@192.168.1.100 +``` + +**C. Test SSH connection (IMPORTANT):** +```bash +ssh YOUR_USER@YOUR_SERVER +# You should login WITHOUT password prompt +# Type 'exit' to logout +``` + +If password is requested, SSH key setup failed - repeat Step 2B. + +### Step 3: Create Backup Directory on Remote Server + +```bash +ssh YOUR_USER@YOUR_SERVER "mkdir -p /path/to/backups" +``` + +Example: +```bash +ssh ubuntu@192.168.1.100 "mkdir -p /home/ubuntu/backups" +``` + +### Step 4: Test Manual Backup + +```bash +cd /Users/sreekarvarma/UN/aovi/backup-infrastructure +./remote-backup.sh +``` + +**Expected output:** +``` +[INFO] Starting remote backup process... +[INFO] Creating temporary backup directory... +[INFO] Backing up MongoDB database... +[INFO] MongoDB backup completed +[INFO] Backing up PostgreSQL database... +[INFO] PostgreSQL backup completed +[INFO] Creating backup metadata... +[INFO] Creating remote backup directory... +[INFO] Transferring backups to remote server... +[INFO] Verifying remote backup... +[INFO] ✓ Backup successfully transferred to remote server +[INFO] Remote location: user@host:/path/YYYY-MM-DD +[INFO] Cleaning up old backups on remote server... +[INFO] ✓ Backup process completed successfully! +``` + +--- + +## 🤖 Automated Backups + +**Current Status:** Daily backups configured at 2:00 AM + +### View Scheduled Jobs +```bash +crontab -l +``` + +### Change Schedule +```bash +./setup-automation.sh +``` + +Options: +- **Daily** (2 AM) - Recommended for production +- **Weekly** (Sunday 2 AM) - For low-change environments +- **Custom** - Specify your own cron schedule +- **Manual only** - Disable automation + +### View Backup Logs +```bash +# View recent logs +tail -50 backup-infrastructure/backup.log + +# Monitor live +tail -f backup-infrastructure/backup.log +``` + +### Disable Automated Backups +```bash +crontab -e +# Delete the line containing "remote-backup.sh" +# Save and exit +``` + +--- + +## What Gets Backed Up + +| Database | Container | Content | Format | +|----------|-----------|---------|--------| +| MongoDB | `aovi-mongodb` | `aovi` database | Compressed archive (.gz) | +| PostgreSQL | `aovi-postgres` | `keycloak` database | Custom format dump | +| Metadata | - | Backup info & timestamp | Text file | + +**Backup Structure on Remote Server:** +``` +/your/backup/path/ +├── 2025-10-21/ +│ ├── mongodb-2025-10-21_02-00-00.archive.gz +│ ├── postgresql-2025-10-21_02-00-00.dump +│ └── backup-info.txt +├── 2025-10-22/ +│ └── ... +``` + +**Retention:** Old backups (>7 days) are automatically deleted. + +--- + +## Restore Backups + +### List Available Backups +```bash +ssh YOUR_USER@YOUR_SERVER "ls -lh /path/to/backups/" +``` + +### Restore MongoDB +```bash +# 1. Copy backup from remote server +scp YOUR_USER@YOUR_SERVER:/path/to/backups/2025-10-21/mongodb-*.archive.gz /tmp/ + +# 2. Copy to MongoDB container +docker cp /tmp/mongodb-*.archive.gz aovi-mongodb:/tmp/restore.archive.gz + +# 3. Restore (WARNING: This will replace current data!) +docker exec aovi-mongodb mongorestore \ + --archive=/tmp/restore.archive.gz \ + --gzip \ + --drop +``` + +### Restore PostgreSQL +```bash +# 1. Copy backup from remote server +scp YOUR_USER@YOUR_SERVER:/path/to/backups/2025-10-21/postgresql-*.dump /tmp/ + +# 2. Copy to PostgreSQL container +docker cp /tmp/postgresql-*.dump aovi-postgres:/tmp/restore.dump + +# 3. Restore (WARNING: This will replace current data!) +docker exec -e PGPASSWORD="${KEYCLOAK_DB_PASSWORD:-keycloak}" aovi-postgres \ + pg_restore -U keycloak -d keycloak -c /tmp/restore.dump +``` + +--- + +## 🔧 Troubleshooting + +### "Cannot connect to remote server" + +**Problem:** SSH connection failed + +**Solutions:** +1. **Verify remote server is accessible:** + ```bash + ping YOUR_SERVER + ``` + +2. **Check SSH key permissions:** + ```bash + chmod 600 ~/.ssh/id_rsa + chmod 644 ~/.ssh/id_rsa.pub + ``` + +3. **Test SSH connection manually:** + ```bash + ssh -i ~/.ssh/id_rsa YOUR_USER@YOUR_SERVER + ``` + - Should login WITHOUT password + - If it asks for password, run: `ssh-copy-id -i ~/.ssh/id_rsa.pub YOUR_USER@YOUR_SERVER` + +4. **Check SSH key path in script:** + ```bash + ls -l ~/.ssh/id_rsa # Should exist + ``` + +--- + +### "Container 'aovi-mongodb' is not running" + +**Problem:** Database containers are stopped + +**Solution:** +```bash +cd /Users/sreekarvarma/UN/aovi +docker-compose up -d +docker ps # Verify containers are running +``` + +--- + +### "Permission denied" on Remote Server + +**Problem:** No write access to backup directory + +**Solution:** +```bash +# Create directory with proper permissions +ssh YOUR_USER@YOUR_SERVER "mkdir -p /path/to/backups && chmod 755 /path/to/backups" +``` + +--- + +### Cron Job Not Running + +**Check if cron job exists:** +```bash +crontab -l | grep remote-backup +``` + +**Check system logs:** +```bash +# macOS +log show --predicate 'eventMessage contains "cron"' --last 1h + +# View backup script logs +cat backup-infrastructure/backup.log +``` + +**Test backup manually:** +```bash +./remote-backup.sh +``` + +--- + +## 🔒 Security Features + +- SSH key-based authentication (passwordless) +- Automatic cleanup of old backups (7-day retention) +- Backups stored on separate server (disaster recovery) +- Docker internal network for database access +- No credentials stored in plain text + +--- + +## 📋 System Requirements + +| Component | Requirement | +|-----------|-------------| +| **Local** | Docker, Docker Compose, SSH client | +| **Remote** | SSH server, sufficient disk space | +| **Containers** | `aovi-mongodb`, `aovi-postgres` (running) | +| **Network** | SSH port 22 accessible to remote server | + +--- + +## 📞 Quick Reference + +```bash +# Run backup manually +./remote-backup.sh + +# Setup automation +./setup-automation.sh + +# View scheduled jobs +crontab -l + +# View logs +tail -f backup-infrastructure/backup.log + +# List backups on remote server +ssh YOUR_USER@YOUR_SERVER "ls -lh /path/to/backups/" + +# Check containers +docker ps | grep aovi +``` diff --git a/backup-infrastructure/remote-backup.sh b/backup-infrastructure/remote-backup.sh new file mode 100755 index 0000000..89df5bf --- /dev/null +++ b/backup-infrastructure/remote-backup.sh @@ -0,0 +1,149 @@ +#!/bin/bash + +####################################### +# Remote Database Backup Script +# Backs up MongoDB & PostgreSQL to a remote server +####################################### + +set -e # Exit on any error + +# ===== CONFIGURATION ===== +REMOTE_USER="backup" # SSH user on remote server +REMOTE_HOST="backup.example.com" # Remote server hostname/IP +REMOTE_PATH="/home//backups/aovi" # Remote backup directory +SSH_KEY="$HOME/.ssh/id_rsa" # SSH private key path + +BACKUP_DIR="/tmp/aovi-backup-$(date +%Y%m%d_%H%M%S)" +DATE=$(date +%Y-%m-%d_%H-%M-%S) + +# Container names +MONGO_CONTAINER="aovi-mongodb" +POSTGRES_CONTAINER="aovi-postgres" + +# ===== COLORS FOR OUTPUT ===== +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# ===== FUNCTIONS ===== +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +cleanup() { + if [ -d "$BACKUP_DIR" ]; then + log_info "Cleaning up temporary backup directory..." + rm -rf "$BACKUP_DIR" + fi +} + +trap cleanup EXIT + +# ===== PRE-FLIGHT CHECKS ===== +log_info "Starting remote backup process..." + +# Check if containers are running +if ! docker ps | grep -q "$MONGO_CONTAINER"; then + log_error "MongoDB container '$MONGO_CONTAINER' is not running" + exit 1 +fi + +if ! docker ps | grep -q "$POSTGRES_CONTAINER"; then + log_error "PostgreSQL container '$POSTGRES_CONTAINER' is not running" + exit 1 +fi + +# Check SSH connectivity +if ! ssh -i "$SSH_KEY" -o BatchMode=yes -o ConnectTimeout=5 "$REMOTE_USER@$REMOTE_HOST" "echo 2>&1" > /dev/null 2>&1; then + log_error "Cannot connect to remote server $REMOTE_HOST" + log_error "Please ensure:" + log_error " 1. Remote server is accessible" + log_error " 2. SSH key is set up correctly" + log_error " 3. SSH key path is correct: $SSH_KEY" + exit 1 +fi + +# ===== CREATE BACKUP DIRECTORY ===== +log_info "Creating temporary backup directory..." +mkdir -p "$BACKUP_DIR" + +# ===== BACKUP MONGODB ===== +log_info "Backing up MongoDB database..." +docker exec "$MONGO_CONTAINER" mongodump \ + --db=aovi \ + --archive=/tmp/mongodb-backup.archive \ + --gzip + +docker cp "$MONGO_CONTAINER:/tmp/mongodb-backup.archive" \ + "$BACKUP_DIR/mongodb-${DATE}.archive.gz" + +docker exec "$MONGO_CONTAINER" rm /tmp/mongodb-backup.archive + +log_info "MongoDB backup completed" + +# ===== BACKUP POSTGRESQL ===== +log_info "Backing up PostgreSQL database..." + +# Get PostgreSQL password from environment or .env file +if [ -f ".env" ]; then + export $(grep KEYCLOAK_DB_PASSWORD .env | xargs) +fi + +docker exec -e PGPASSWORD="${KEYCLOAK_DB_PASSWORD:-keycloak}" "$POSTGRES_CONTAINER" \ + pg_dump -U keycloak -d keycloak -F c -f /tmp/postgresql-backup.dump + +docker cp "$POSTGRES_CONTAINER:/tmp/postgresql-backup.dump" \ + "$BACKUP_DIR/postgresql-${DATE}.dump" + +docker exec "$POSTGRES_CONTAINER" rm /tmp/postgresql-backup.dump + +log_info "PostgreSQL backup completed" + +# ===== CREATE METADATA ===== +log_info "Creating backup metadata..." +cat > "$BACKUP_DIR/backup-info.txt" << EOF +Backup Date: $(date) +MongoDB Database: aovi +PostgreSQL Database: keycloak +MongoDB Container: $MONGO_CONTAINER +PostgreSQL Container: $POSTGRES_CONTAINER +Hostname: $(hostname) +EOF + +# ===== TRANSFER TO REMOTE SERVER ===== +log_info "Creating remote backup directory..." +ssh -i "$SSH_KEY" "$REMOTE_USER@$REMOTE_HOST" \ + "mkdir -p $REMOTE_PATH/$(date +%Y-%m-%d)" + +log_info "Transferring backups to remote server..." +scp -i "$SSH_KEY" -r "$BACKUP_DIR/"* \ + "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/$(date +%Y-%m-%d)/" + +# ===== VERIFY TRANSFER ===== +log_info "Verifying remote backup..." +REMOTE_FILES=$(ssh -i "$SSH_KEY" "$REMOTE_USER@$REMOTE_HOST" \ + "ls -1 $REMOTE_PATH/$(date +%Y-%m-%d) | wc -l") + +if [ "$REMOTE_FILES" -ge 3 ]; then + log_info "✓ Backup successfully transferred to remote server" + log_info "Remote location: $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/$(date +%Y-%m-%d)" +else + log_error "Backup transfer verification failed" + exit 1 +fi + +# ===== CLEANUP OLD BACKUPS (Keep last 7 days) ===== +log_info "Cleaning up old backups on remote server..." +ssh -i "$SSH_KEY" "$REMOTE_USER@$REMOTE_HOST" \ + "find $REMOTE_PATH -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \; 2>/dev/null || true" + +log_info "✓ Backup process completed successfully!" diff --git a/backup-infrastructure/setup-automation.sh b/backup-infrastructure/setup-automation.sh new file mode 100755 index 0000000..f879268 --- /dev/null +++ b/backup-infrastructure/setup-automation.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +####################################### +# Setup Automated Remote Backup +# Configures cron job for daily/weekly backups +####################################### + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKUP_SCRIPT="$SCRIPT_DIR/remote-backup.sh" + +echo -e "${GREEN}=== Remote Backup Automation Setup ===${NC}" +echo + +# Make backup script executable +chmod +x "$BACKUP_SCRIPT" + +echo "Choose backup frequency:" +echo "1) Daily (at 2 AM)" +echo "2) Weekly (Sunday at 2 AM)" +echo "3) Custom cron expression" +echo "4) Manual only (no automation)" +echo +read -p "Enter choice (1-4): " choice + +case $choice in + 1) + CRON_SCHEDULE="0 2 * * *" + DESCRIPTION="Daily at 2 AM" + ;; + 2) + CRON_SCHEDULE="0 2 * * 0" + DESCRIPTION="Weekly on Sunday at 2 AM" + ;; + 3) + echo "Enter cron expression (e.g., '0 3 * * *' for daily at 3 AM):" + read -p "Cron: " CRON_SCHEDULE + DESCRIPTION="Custom: $CRON_SCHEDULE" + ;; + 4) + echo -e "${YELLOW}No automation configured. Run manually with:${NC}" + echo " $BACKUP_SCRIPT" + exit 0 + ;; + *) + echo "Invalid choice" + exit 1 + ;; +esac + +# Remove existing cron job for this script if any +crontab -l 2>/dev/null | grep -v "$BACKUP_SCRIPT" | crontab - 2>/dev/null || true + +# Add new cron job +(crontab -l 2>/dev/null; echo "$CRON_SCHEDULE $BACKUP_SCRIPT >> $SCRIPT_DIR/backup.log 2>&1") | crontab - + +echo -e "${GREEN}✓ Cron job configured successfully!${NC}" +echo +echo "Schedule: $DESCRIPTION" +echo "Command: $BACKUP_SCRIPT" +echo "Logs: $SCRIPT_DIR/backup.log" +echo +echo "To view scheduled jobs: crontab -l" +echo "To remove automation: crontab -e (and delete the line)" +echo +echo -e "${YELLOW}Test the backup now with:${NC}" +echo " $BACKUP_SCRIPT" diff --git a/docker-compose.yml b/docker-compose.yml index 9a6659d..d18a8f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,32 @@ -version: '3' services: web: + build: . depends_on: - mongodb - image: aovi + - keycloak container_name: aovi-web ports: - # 3000:3000 - "1041:3000" + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules environment: - DOCKER=true - - JWT_SECRET_FILE=/run/secrets/jwt_secret - - SUPERADMIN_DEFAULT_PASSWORD=${SUPERADMIN_DEFAULT_PASSWORD} # Pass from shell - volumes: - - jwt_secret_volume:/run/secrets + - NODE_ENV=development + - JWT_SECRET=${JWT_SECRET} + - SUPERADMIN_DEFAULT_PASSWORD=${SUPERADMIN_DEFAULT_PASSWORD} + # Email Configuration + - SMTP_HOST=${SMTP_HOST:-smtp.gmail.com} + - SMTP_PORT=${SMTP_PORT:-587} + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + # Keycloak Configuration + - KEYCLOAK_URL=http://keycloak:8080 + - KEYCLOAK_REALM=aovi + - KEYCLOAK_CLIENT_ID=aovi-app + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET} restart: always + mongodb: image: mongo container_name: aovi-mongodb @@ -22,6 +34,42 @@ services: volumes: - aovi-mongodb-data:/data/db + postgres: + image: postgres:13 + container_name: aovi-postgres + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD} + volumes: + - keycloak-postgres-data:/var/lib/postgresql/data + restart: always + + keycloak: + image: quay.io/keycloak/keycloak:22.0 + container_name: aovi-keycloak + command: start-dev --import-realm + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD} + KC_HOSTNAME: aovi.unitac-hamburg.com + KC_HOSTNAME_PATH: /auth + KC_HOSTNAME_STRICT: "false" + KC_HTTP_ENABLED: "true" + KC_PROXY: edge + KC_HOSTNAME_ADMIN_URL: https://aovi.unitac-hamburg.com/auth + ports: + - "8081:8080" + depends_on: + - postgres + volumes: + - ./keycloak/import:/opt/keycloak/data/import + restart: always + volumes: aovi-mongodb-data: - jwt_secret_volume: \ No newline at end of file + keycloak-postgres-data: \ No newline at end of file diff --git a/generate_secret_JWT.sh b/generate_secret_JWT.sh deleted file mode 100644 index 0773d9f..0000000 --- a/generate_secret_JWT.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -# Check if .env file exists -if [ -f .env ]; then - # Source the .env file to set environment variables - source .env - - # Check if JWT_SECRET is already defined - if [ -n "$JWT_SECRET" ]; then - echo "JWT_SECRET is already defined in .env" - exit 0 - fi -fi - -# Check if JWT_SECRET is already defined in the persistent volume -if [ -f /run/secrets/jwt_secret ]; then - source /run/secrets/jwt_secret - - # Check if JWT_SECRET is already defined - if [ -n "$JWT_SECRET" ]; then - echo "JWT_SECRET is already defined in /run/secrets/jwt_secret" - exit 0 - fi -fi - -# If JWT_SECRET is still not defined, generate a new one -JWT_SECRET=$(openssl rand -hex 32) - -# Output the secret to the volume -echo "JWT_SECRET=$JWT_SECRET" > /run/secrets/jwt_secret - -# Append the secret to .env file -echo "JWT_SECRET=$JWT_SECRET" >> .env - -echo "JWT_SECRET generated and saved to /run/secrets/jwt_secret and .env" diff --git a/keycloak/README.md b/keycloak/README.md new file mode 100644 index 0000000..0a4d748 --- /dev/null +++ b/keycloak/README.md @@ -0,0 +1,28 @@ +# Keycloak Configuration + +## Setup Instructions + +1. **Copy the template file:** + ```bash + cp keycloak/import/aovi-realm.template.json keycloak/import/aovi-realm.json + ``` + +2. **Edit the copied file and replace:** + - `CHANGE_THIS_PASSWORD_IN_PRODUCTION` with a secure password + - `REPLACE_WITH_SECURE_CLIENT_SECRET` with a secure client secret + +3. **For development, you can use:** + - Password: `admin123` (change for production) + - Client Secret: `aovi-client-secret-dev` (change for production) + +## Security Note + +The `aovi-realm.json` file is gitignored to prevent accidental commits of secrets. + +## Realm Configuration + +This Keycloak realm provides: +- **Role-based access control** (superadmin, admin, basic) +- **Email-based authentication** +- **Password reset capabilities** +- **Client configuration** for AOVI app integration \ No newline at end of file diff --git a/keycloak/import/aovi-realm.template.json b/keycloak/import/aovi-realm.template.json new file mode 100644 index 0000000..533dd8b --- /dev/null +++ b/keycloak/import/aovi-realm.template.json @@ -0,0 +1,76 @@ +{ + "realm": "aovi", + "enabled": true, + "displayName": "AOVI Authentication", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "rememberMe": true, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "users": [ + { + "username": "superadmin", + "email": "superadmin@aovi.local", + "emailVerified": true, + "enabled": true, + "firstName": "Super", + "lastName": "Admin", + "realmRoles": ["superadmin", "admin", "basic"], + "credentials": [ + { + "type": "password", + "value": "CHANGE_THIS_PASSWORD_IN_PRODUCTION", + "temporary": true + } + ] + } + ], + "roles": { + "realm": [ + { + "name": "superadmin", + "description": "Super Administrator with full system access" + }, + { + "name": "admin", + "description": "Administrator with event and user management access" + }, + { + "name": "basic", + "description": "Basic user with standard access" + } + ] + }, + "clients": [ + { + "clientId": "aovi-app", + "name": "AOVI Application", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "REPLACE_WITH_SECURE_CLIENT_SECRET", + "redirectUris": [ + "http://localhost:1041/*", + "http://localhost:3000/*", + "http://localhost:1041/aovi/*", + "http://localhost:1041/aovi/views/*", + "http://localhost:1041/aovi/auth/*" + ], + "webOrigins": [ + "http://localhost:1041", + "http://localhost:3000", + "*" + ], + "standardFlowEnabled": true, + "directAccessGrantsEnabled": true, + "publicClient": false, + "protocol": "openid-connect", + "fullScopeAllowed": true + } + ], + "eventsEnabled": true, + "eventsListeners": ["jboss-logging"], + "adminEventsEnabled": true +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 626c0c5..e5a7118 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@keycloak/keycloak-admin-client": "^24.0.0", "axios": "^1.8.4", "bcryptjs": "^3.0.2", "child_process": "^1.0.2", @@ -17,11 +18,14 @@ "ejs": "^3.1.10", "exceljs": "^4.3.0", "express": "^4.21.2", + "express-session": "^1.18.2", "htmlparser2": "^10.0.0", "jsonwebtoken": "^9.0.2", + "keycloak-connect": "^24.0.0", "mongoose": "^8.12.1", "mongoose-delete": "^1.0.2", "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.8", "qr-image": "^3.2.0", "sanitize-html": "^2.14.0", "socket.io": "^4.8.1", @@ -72,6 +76,20 @@ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", "license": "MIT" }, + "node_modules/@keycloak/keycloak-admin-client": { + "version": "24.0.5", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-24.0.5.tgz", + "integrity": "sha512-SXDVtQ3ov7GQbxXq51Uq8lzhwzQwNg6XiY50ZA9whuUe2t/0zPT4Zd/LcULcjweIjSNWWgfbDyN1E3yRSL8Qqw==", + "license": "Apache-2.0", + "dependencies": { + "camelize-ts": "^3.0.0", + "url-join": "^5.0.0", + "url-template": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", @@ -87,6 +105,20 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@testim/chrome-version": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", + "integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==", + "license": "MIT", + "optional": true + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT", + "optional": true + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -120,6 +152,16 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -142,6 +184,16 @@ "node": ">=0.8" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -236,6 +288,31 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -249,13 +326,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -294,6 +371,16 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", @@ -369,6 +456,12 @@ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "license": "MIT" }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -416,6 +509,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/bson": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", @@ -536,6 +635,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelize-ts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", + "integrity": "sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -608,6 +716,29 @@ "fsevents": "~2.3.2" } }, + "node_modules/chromedriver": { + "version": "140.0.4", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-140.0.4.tgz", + "integrity": "sha512-/NUoxYBNkJeoNj1B5ux3KxGShITlxJctkbApgVAa3ZC8EvCLKaBclwU3/IEj5MJHnBJzqOVDxs/eTyaF9k2fOg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@testim/chrome-version": "^1.1.4", + "axios": "^1.12.0", + "compare-versions": "^6.1.0", + "extract-zip": "^2.0.1", + "proxy-agent": "^6.4.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.2" + }, + "bin": { + "chromedriver": "bin/chromedriver" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/codepage": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", @@ -647,6 +778,13 @@ "node": ">= 0.8" } }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "license": "MIT", + "optional": true + }, "node_modules/compress-commons": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", @@ -804,6 +942,16 @@ "node": ">= 6" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -819,6 +967,13 @@ "ms": "2.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT", + "optional": true + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -828,6 +983,21 @@ "node": ">=0.10.0" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -988,6 +1158,21 @@ "node": ">=0.10.0" } }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1133,6 +1318,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1222,6 +1463,31 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -1231,6 +1497,52 @@ "node": ">= 0.6" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, "node_modules/fast-csv": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", @@ -1244,6 +1556,16 @@ "node": ">=10.0.0" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -1326,14 +1648,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1456,6 +1779,62 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "optional": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1544,6 +1923,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1556,6 +1945,17 @@ "node": ">= 0.4" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -1591,6 +1991,84 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1653,6 +2131,26 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1717,6 +2215,28 @@ "node": ">=0.10.0" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT", + "optional": true + }, + "node_modules/is2": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", + "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", + "license": "MIT", + "optional": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1792,6 +2312,17 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwk-to-pem": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", + "integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==", + "license": "Apache-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "elliptic": "^6.6.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -1811,6 +2342,21 @@ "node": ">=12.0.0" } }, + "node_modules/keycloak-connect": { + "version": "24.0.5", + "resolved": "https://registry.npmjs.org/keycloak-connect/-/keycloak-connect-24.0.5.tgz", + "integrity": "sha512-UtDzfsAF4IimlnLjt4Cu0XGOtMUwWFmBmEnzeybQZgxrkwXclHJ0iybpRe6k3tliB2b3/+bL1yOEsFaUmllR4Q==", + "license": "Apache-2.0", + "dependencies": { + "jwk-to-pem": "^2.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "chromedriver": "latest" + } + }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -1947,6 +2493,16 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2022,6 +2578,18 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2243,6 +2811,25 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", @@ -2362,6 +2949,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2371,6 +2967,65 @@ "wrappy": "1" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "optional": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -2407,6 +3062,13 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2473,6 +3135,51 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2486,6 +3193,17 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2516,6 +3234,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2869,6 +3596,17 @@ "node": ">=10" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -2979,6 +3717,71 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3083,6 +3886,42 @@ "node": ">= 6" } }, + "node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT", + "optional": true + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -3145,6 +3984,13 @@ "node": "*" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3164,6 +4010,18 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3204,6 +4062,24 @@ "setimmediate": "~1.0.4" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/url-template": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==", + "license": "BSD-3-Clause", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3340,6 +4216,17 @@ "node": ">=0.4" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/zip-stream": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", diff --git a/package.json b/package.json index 4910ad2..89d70c1 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,16 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "docker": "docker build -t aovi . && docker compose up", - "deploy": "git pull && docker build -t aovi . && docker compose up", + "start": "node server.js", "dev": "nodemon server.js", - "watch": "browser-sync start --proxy 'localhost:3000' --watch true" + "test": "echo \"Error: no test specified\" && exit 1", + "docker:dev": "docker compose up", + "docker:prod": "docker compose up -d", + "docker:build": "docker build -t aovi .", + "deploy": "git pull && npm run docker:build && npm run docker:prod", + "logs": "docker compose logs -f", + "health": "curl -f http://localhost:3000/health || exit 1", + "clean": "docker system prune -f && docker volume prune -f" }, "author": "", "license": "ISC", @@ -16,6 +21,7 @@ "nodemon": "^3.1.9" }, "dependencies": { + "@keycloak/keycloak-admin-client": "^24.0.0", "axios": "^1.8.4", "bcryptjs": "^3.0.2", "child_process": "^1.0.2", @@ -24,11 +30,14 @@ "ejs": "^3.1.10", "exceljs": "^4.3.0", "express": "^4.21.2", + "express-session": "^1.18.2", "htmlparser2": "^10.0.0", "jsonwebtoken": "^9.0.2", + "keycloak-connect": "^24.0.0", "mongoose": "^8.12.1", "mongoose-delete": "^1.0.2", "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.8", "qr-image": "^3.2.0", "sanitize-html": "^2.14.0", "socket.io": "^4.8.1", diff --git a/server.js b/server.js index fd63b46..72603fd 100644 --- a/server.js +++ b/server.js @@ -7,23 +7,171 @@ const transcriptionService = require("./services/transcription"); const aoviService = require("./services/aovi"); const geodataService = require("./services/geodata"); const qr = require("qr-image"); -const port = 3000; -const { exec } = require("child_process"); -const fs = require("fs"); +const KeycloakAuthService = require("./services/auth/keycloak-auth.service"); +const { createAccessControlRouter } = require("./services/access-control"); + +require('dotenv').config(); + +// Rate limiting for magic link requests +const magicLinkRequestLog = new Map(); // email -> { lastRequest: timestamp } +const port = 3000; const app = express(); const server = require("http").createServer(app); +const authService = new KeycloakAuthService(); +app.set('authService', authService); + +// Initialize Keycloak middleware +try { + app.use(authService.getSessionMiddleware()); + app.use(authService.getKeycloakMiddleware()); +} catch (error) { + console.error('Error initializing Keycloak:', error); +} + app.use(cookieParser()); app.use(express.json({ limit: "2mb" })); -// Use the user service -app.use("/aovi/user", userService.app); - userService.connectDB().catch((err) => { console.error("Error connecting to database", err); }); +// Passwordless Authentication Endpoints +app.post("/aovi/auth/magic-link", async (req, res) => { + try { + const { email, redirectUrl } = req.body; + + if (!email) { + return res.status(400).json({ + success: false, + message: 'Email is required' + }); + } + + // Rate limiting: 1 minute between requests + const emailLower = email.toLowerCase(); + const requestInfo = magicLinkRequestLog.get(emailLower); + const now = Date.now(); + + if (requestInfo && (now - requestInfo.lastRequest) < 60000) { + const waitTime = Math.ceil((60000 - (now - requestInfo.lastRequest)) / 1000); + return res.status(429).json({ + success: false, + message: `Please wait ${waitTime} seconds before requesting another sign-in link.` + }); + } + + magicLinkRequestLog.set(emailLower, { lastRequest: now }); + + const result = await authService.generateMagicLink(email, redirectUrl); + res.json(result); + } catch (error) { + console.error('Error generating magic link:', error); + res.status(500).json({ + success: false, + message: 'Failed to generate sign-in link' + }); + } +}); + +app.get("/aovi/auth/verify-signin/:token", async (req, res) => { + try { + const { token } = req.params; + const { redirect } = req.query; + + const result = await authService.verifyMagicLink(token); + + if (result.success) { + // Set session + req.session = req.session || {}; + req.session.user = { + id: result.user_id, + email: result.email, + name: result.email, + authenticated: true, + auth_method: 'passwordless' + }; + + // Save session explicitly + req.session.save((err) => { + if (err) { + console.error('Session save error:', err); + return res.redirect('/aovi/views/login?error=' + encodeURIComponent('Failed to save session')); + } + + // Normalize redirect URL + let redirectUrl = redirect || result.redirect_url || '/aovi/views/events'; + + res.redirect(redirectUrl); + }); + } else { + console.error('Magic link verification failed:', result); + res.redirect('/aovi/views/login?error=' + encodeURIComponent(result.error || 'Invalid sign-in link')); + } + } catch (error) { + console.error('Error verifying sign-in link:', error); + res.redirect('/aovi/views/login?error=' + encodeURIComponent('Failed to verify sign-in link')); + } +}); + +app.post("/aovi/auth/logout", (req, res) => { + req.session.destroy((err) => { + res.json({ success: true }); + }); +}); + +app.get("/aovi/auth/logout", (req, res) => { + req.session.destroy((err) => { + res.redirect('/aovi/views/login'); + }); +}); + +// Health check endpoint +app.get("/health", (req, res) => { + res.status(200).json({ + status: "healthy", + timestamp: new Date().toISOString(), + version: process.env.npm_package_version || "1.0.0" + }); +}); + +app.get("/aovi/auth/status", (req, res) => { + const user = req.session?.user || null; + + res.json({ + authenticated: !!user, + user: user + }); +}); + +// User info endpoint +app.get("/aovi/user/me", (req, res) => { + const user = req.session?.user || null; + + if (!user) { + return res.status(401).json({ + success: false, + message: "Not authenticated" + }); + } + + const userResponse = { + ...user, + role: user.roles || user.role || [] + }; + + res.json(userResponse); +}); + +// Complete Access Control API +try { + const accessControlRouter = createAccessControlRouter(authService); + app.use("/aovi/access-control", accessControlRouter); +} catch (error) { + console.error('Error setting up access control routes:', error); +} + const io = require("socket.io")(server, { path: "/aovi-socket-io", maxHttpBufferSize: 1 * 1024 * 1024, // 1 MB @@ -47,34 +195,43 @@ io.on("connection", (socket) => { }); }); -redirectUnauthorized = (req, res, next) => { - if (req.body.authorized) { +// New authentication middleware using passwordless sessions +const requireAuthentication = (req, res, next) => { + if (req.session?.user?.authenticated) { + req.body = req.body || {}; + req.body.user = req.session.user; + req.body.authorized = true; next(); } else { - originalTarget = req.originalUrl; + const originalTarget = req.originalUrl; res.redirect("/aovi/views/login?targeturl=" + originalTarget); } }; -sendUnauthorizedStatus = (req, res, next) => { - if (!req.body.authorized) { - res.status(401).send("Unauthorized"); - return; - } else { +const requireAuthenticationAPI = (req, res, next) => { + if (req.session?.user?.authenticated) { + req.body = req.body || {}; + req.body.user = req.session.user; + req.body.authorized = true; next(); + } else { + res.status(401).json({ + success: false, + message: "Authentication required", + redirect: "/aovi/views/login" + }); } }; +// Protected routes using new passwordless authentication app.use( "/aovi/comments", - userService.authorizeBasic, - sendUnauthorizedStatus, + requireAuthenticationAPI, commentService.router(io) ); app.use( "/aovi/rooms", - userService.authorizeBasic, - redirectUnauthorized, + requireAuthentication, roomService.router ); app.use("/aovi/transcription", transcriptionService(io)); diff --git a/services/access-control/access-control.router.js b/services/access-control/access-control.router.js new file mode 100644 index 0000000..16cd0cb --- /dev/null +++ b/services/access-control/access-control.router.js @@ -0,0 +1,234 @@ +const express = require('express'); +const AccessControlService = require('./access-control.service'); + +function createAccessControlRouter() { + const router = express.Router(); + const accessControl = new AccessControlService(); + + // Simple authentication middleware + const requireAuth = (req, res, next) => { + if (!req.session?.user) { + return res.status(401).json({ success: false, message: 'Authentication required' }); + } + next(); + }; + + // Test endpoint + router.get('/test', (req, res) => { + res.json({ + success: true, + message: 'Access control operational', + initialized: accessControl.initialized + }); + }); + + // Create item access control + router.post('/items', requireAuth, async (req, res) => { + try { + const { itemId, itemType, ownerId } = req.body; + const createdBy = req.session.user.id; + + if (!itemId || !itemType || !ownerId) { + return res.status(400).json({ + success: false, + message: 'itemId, itemType, and ownerId are required' + }); + } + + const result = await accessControl.createItemAccess(itemId, itemType, ownerId, createdBy); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to create item access control', + error: error.message + }); + } + }); + + // Check permission for item + router.get('/items/:itemId/check/:action', requireAuth, async (req, res) => { + try { + const { itemId, action } = req.params; + const userId = req.session.user.id; + const userRoles = req.session.user.roles || []; + + const hasPermission = await accessControl.checkPermission(userId, itemId, action, userRoles); + + res.json({ + success: true, + hasPermission, + user: userId, + item: itemId, + action + }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to check permission', + error: error.message + }); + } + }); + + // Request access to an item + router.post('/requests', requireAuth, async (req, res) => { + try { + const { itemId, requestedPermissions, reason, itemType } = req.body; + const userId = req.session.user.id; + const userEmail = req.session.user.email; + + if (!itemId || !requestedPermissions || !Array.isArray(requestedPermissions)) { + return res.status(400).json({ + success: false, + message: 'itemId and requestedPermissions array are required' + }); + } + + const result = await accessControl.requestAccess(userId, itemId, requestedPermissions, reason, itemType, userEmail); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to create access request', + error: error.message + }); + } + }); + + // Get pending access requests (for administrators) + router.get('/requests/pending', requireAuth, async (req, res) => { + try { + const userRoles = req.session.user.roles || []; + + // Check if user has admin role + const isAdmin = userRoles.includes('admin') || userRoles.includes('superadmin'); + if (!isAdmin) { + return res.status(403).json({ + success: false, + message: 'Admin access required' + }); + } + + const requests = await accessControl.getPendingRequests(); + res.json({ success: true, data: requests || [] }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to get pending requests', + error: error.message + }); + } + }); + + // Approve or deny access request + router.put('/requests/:id', requireAuth, async (req, res) => { + try { + const { id } = req.params; + const { action, adminNote } = req.body; + const adminId = req.session.user.id; + const userRoles = req.session.user.roles || []; + + // Check if user has admin role + const isAdmin = userRoles.includes('admin') || userRoles.includes('superadmin'); + if (!isAdmin) { + return res.status(403).json({ + success: false, + message: 'Admin access required' + }); + } + + if (!action || !['approve', 'deny'].includes(action)) { + return res.status(400).json({ + success: false, + message: 'action must be either "approve" or "deny"' + }); + } + + const result = await accessControl.processAccessRequest(id, action, adminId, adminNote); + res.json({ + success: true, + message: `Access request ${action}d`, + data: result + }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to process access request', + error: error.message + }); + } + }); + + // Grant direct access (for administrators) + router.post('/grants', requireAuth, async (req, res) => { + try { + const { userId, itemId, permissions } = req.body; + const adminId = req.session.user.id; + const userRoles = req.session.user.roles || []; + + // Check if user has admin role + const isAdmin = userRoles.includes('admin') || userRoles.includes('superadmin'); + if (!isAdmin) { + return res.status(403).json({ + success: false, + message: 'Admin access required' + }); + } + + if (!userId || !itemId || !permissions || !Array.isArray(permissions)) { + return res.status(400).json({ + success: false, + message: 'userId, itemId, and permissions array are required' + }); + } + + const result = await accessControl.grantDirectAccess(userId, itemId, permissions, adminId); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to grant direct access', + error: error.message + }); + } + }); + + // Revoke access (for administrators) + router.delete('/grants', requireAuth, async (req, res) => { + try { + const { userId, itemId, permissions } = req.body; + const adminId = req.session.user.id; + const userRoles = req.session.user.roles || []; + + // Check if user has admin role + const isAdmin = userRoles.includes('admin') || userRoles.includes('superadmin'); + if (!isAdmin) { + return res.status(403).json({ + success: false, + message: 'Admin access required' + }); + } + + if (!userId || !itemId) { + return res.status(400).json({ + success: false, + message: 'userId and itemId are required' + }); + } + + const result = await accessControl.revokeAccess(userId, itemId, permissions); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to revoke access', + error: error.message + }); + } + }); + + return router; +} + +module.exports = createAccessControlRouter; \ No newline at end of file diff --git a/services/access-control/access-control.service.js b/services/access-control/access-control.service.js new file mode 100644 index 0000000..598214e --- /dev/null +++ b/services/access-control/access-control.service.js @@ -0,0 +1,159 @@ +const { ItemAccess, AccessRequest } = require('./models/access.model'); + +class AccessControlService { + constructor() { + this.ItemAccess = ItemAccess; + this.AccessRequest = AccessRequest; + this.initialized = true; + } + + async createItemAccess(itemId, itemType, ownerId, createdBy = ownerId, permissions = {}) { + if (!this.initialized) return null; + + const defaultPermissions = { + public: { read: false, write: false, delete: false }, + roles: { + basic: { read: true, write: false, delete: false }, + admin: { read: true, write: true, delete: false }, + superadmin: { read: true, write: true, delete: true } + }, + users: [] + }; + + const itemAccess = new this.ItemAccess({ + itemId, + itemType, + ownerId, + created_by: createdBy, + permissions: { ...defaultPermissions, ...permissions } + }); + + return await itemAccess.save(); + } + + async checkPermission(userId, itemId, action, userRoles = []) { + if (!this.initialized) { + return { granted: true, reason: 'Fallback access' }; + } + + try { + const access = await this.ItemAccess.findOne({ itemId }); + if (!access) { + return { granted: false, reason: 'No permission' }; + } + + // Owner has full access + if (access.ownerId === userId) { + return { granted: true, reason: 'Owner' }; + } + + // Check public permissions + if (access.permissions.public?.[action]) { + return { granted: true, reason: 'Public' }; + } + + // Check role permissions + if (userRoles && userRoles.length > 0) { + for (const role of userRoles) { + if (access.permissions.roles?.[role]?.[action]) { + return { granted: true, reason: `Role: ${role}` }; + } + } + } + + // Check user-specific permissions + const userPerm = access.permissions.users.find(u => u.userId === userId); + if (userPerm?.[action]) { + return { granted: true, reason: 'User-specific' }; + } + + return { granted: false, reason: 'Access denied' }; + + } catch (error) { + return { granted: false, reason: 'Permission error' }; + } + } + + async requestAccess(userId, itemId, requestedPermissions, reason = '', itemType = 'document', requesterEmail = null) { + if (!this.initialized) return null; + + // First get the item's access control to find the owner + const itemAccess = await this.ItemAccess.findOne({ itemId }); + if (!itemAccess) { + throw new Error('Item access control not found'); + } + + const permissions = { + read: requestedPermissions.includes('read'), + write: requestedPermissions.includes('write'), + delete: requestedPermissions.includes('delete') + }; + + const accessRequest = new this.AccessRequest({ + itemId, + itemType: itemType || itemAccess.itemType, + requesterId: userId, + requesterEmail: requesterEmail, + ownerId: itemAccess.ownerId, + requestedPermissions: permissions, + reason + }); + + return await accessRequest.save(); + } + + async getPendingRequests() { + if (!this.initialized) return []; + return await this.AccessRequest.find({ status: 'pending' }); + } + + async processAccessRequest(requestId, action, adminId, adminNote = '') { + if (!this.initialized) return null; + + const request = await this.AccessRequest.findById(requestId); + if (!request) throw new Error('Request not found'); + + request.status = action === 'approve' ? 'approved' : 'denied'; + request.response = { + message: adminNote, + decided_by: adminId, + decided_at: new Date() + }; + + return await request.save(); + } + + async grantDirectAccess(userId, itemId, permissions, adminId) { + if (!this.initialized) return null; + + const access = await this.ItemAccess.findOne({ itemId }); + if (!access) throw new Error('Item access not found'); + + const userPerm = { + userId, + read: permissions.includes('read'), + write: permissions.includes('write'), + delete: permissions.includes('delete'), + granted_by: adminId, + granted_at: new Date() + }; + + access.permissions.users.push(userPerm); + return await access.save(); + } + + async revokeAccess(userId, itemId, permissions = []) { + if (!this.initialized) return null; + + const access = await this.ItemAccess.findOne({ itemId }); + if (!access) throw new Error('Item access not found'); + + access.permissions.users = access.permissions.users.filter( + user => user.userId !== userId + ); + + return await access.save(); + } +} + +module.exports = AccessControlService; diff --git a/services/access-control/index.js b/services/access-control/index.js new file mode 100644 index 0000000..4d30a6c --- /dev/null +++ b/services/access-control/index.js @@ -0,0 +1,10 @@ +const AccessControlService = require('./access-control.service'); +const createAccessControlRouter = require('./access-control.router'); +const { ItemAccess, AccessRequest } = require('./models/access.model'); + +module.exports = { + AccessControlService, + createAccessControlRouter, + ItemAccess, + AccessRequest +}; \ No newline at end of file diff --git a/services/access-control/models/access.model.js b/services/access-control/models/access.model.js new file mode 100644 index 0000000..ecbf9b7 --- /dev/null +++ b/services/access-control/models/access.model.js @@ -0,0 +1,126 @@ +const mongoose = require('mongoose'); + +const ITEM_TYPES = ['room', 'comment', 'event', 'document', 'transcription', 'geodata']; +const DEFAULT_REQUEST_EXPIRY_DAYS = 7; + +const permissionSchema = { + read: { type: Boolean, default: false }, + write: { type: Boolean, default: false }, + delete: { type: Boolean, default: false } +}; + +const itemAccessSchema = new mongoose.Schema({ + itemId: { + type: String, + required: true, + index: true + }, + + itemType: { + type: String, + required: true, + enum: ITEM_TYPES, + index: true + }, + + ownerId: { + type: String, + required: true, + index: true + }, + + permissions: { + public: permissionSchema, + + roles: { + basic: { ...permissionSchema, read: { type: Boolean, default: true } }, + admin: { ...permissionSchema, read: { type: Boolean, default: true }, write: { type: Boolean, default: true } }, + superadmin: { + read: { type: Boolean, default: true }, + write: { type: Boolean, default: true }, + delete: { type: Boolean, default: true } + } + }, + + users: [{ + userId: { type: String, required: true }, + ...permissionSchema, + granted_by: { type: String }, + granted_at: { type: Date, default: Date.now } + }] + }, + + inherits_from: { + type: String, + default: null + }, + + created_by: { type: String, required: true }, + + privacy: { + soft_delete: { type: Boolean, default: false }, + retention_days: { type: Number, default: null }, + anonymize_after_days: { type: Number, default: null } + } +}, { + timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } +}); + +itemAccessSchema.index({ itemId: 1, itemType: 1 }, { unique: true }); +itemAccessSchema.index({ ownerId: 1 }); +itemAccessSchema.index({ 'permissions.users.userId': 1 }); + +const accessRequestSchema = new mongoose.Schema({ + itemId: { + type: String, + required: true, + index: true + }, + + itemType: { + type: String, + required: true, + enum: ITEM_TYPES + }, + + requesterId: { type: String, required: true }, + requesterEmail: { type: String, required: true }, + ownerId: { type: String, required: true }, + + requestedPermissions: { + ...permissionSchema, + read: { type: Boolean, default: true } + }, + + reason: { type: String, maxlength: 500 }, + + status: { + type: String, + enum: ['pending', 'approved', 'denied', 'expired'], + default: 'pending', + index: true + }, + + response: { + message: { type: String, maxlength: 500 }, + decided_by: { type: String }, + decided_at: { type: Date } + }, + + expires_at: { + type: Date, + default: () => new Date(Date.now() + DEFAULT_REQUEST_EXPIRY_DAYS * 24 * 60 * 60 * 1000) + } +}, { + timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } +}); + +accessRequestSchema.index({ requesterId: 1 }); +accessRequestSchema.index({ ownerId: 1 }); +accessRequestSchema.index({ status: 1 }); +accessRequestSchema.index({ expires_at: 1 }); + +module.exports = { + ItemAccess: mongoose.model('ItemAccess', itemAccessSchema), + AccessRequest: mongoose.model('AccessRequest', accessRequestSchema) +}; \ No newline at end of file diff --git a/services/aovi/client/createevent.html b/services/aovi/client/createevent.html index 74b2dab..7d28b21 100644 --- a/services/aovi/client/createevent.html +++ b/services/aovi/client/createevent.html @@ -107,7 +107,7 @@

Create Event

diff --git a/services/aovi/client/eventlist.html b/services/aovi/client/eventlist.html index 31b1514..e104681 100644 --- a/services/aovi/client/eventlist.html +++ b/services/aovi/client/eventlist.html @@ -196,7 +196,7 @@
diff --git a/services/aovi/client/login.html b/services/aovi/client/login.html index 9b5c299..b8136fe 100644 --- a/services/aovi/client/login.html +++ b/services/aovi/client/login.html @@ -3,96 +3,438 @@ - Register + AOVI Login - - + + +
+ +
- +