From 2742dcab940dd42f81f8ccf852b48a22a3322a50 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 30 Apr 2026 15:06:41 -0400 Subject: [PATCH 1/5] docs: added multi-user mode and moved workflows under features --- Makefile | 3 +- docs/astro.config.mjs | 5 - .../docs/features/{ => Canvas}/text-tool.mdx | 2 + .../features/Multi-User Mode/admin-guide.mdx | 845 +++++++++++ .../features/Multi-User Mode/api-guide.mdx | 1231 +++++++++++++++++ .../assets/admin-add-user-1.png | Bin 0 -> 12975 bytes .../assets/admin-add-user-2.png | Bin 0 -> 14788 bytes .../assets/admin-add-user-3.png | Bin 0 -> 18632 bytes .../Multi-User Mode/assets/admin-setup.png | Bin 0 -> 40724 bytes .../Multi-User Mode/assets/user-login-1.png | Bin 0 -> 17252 bytes .../Multi-User Mode/specification.mdx | 959 +++++++++++++ .../features/Multi-User Mode/user-guide.mdx | 390 ++++++ .../Workflows}/adding-nodes.mdx | 0 .../Workflows}/assets/groupsallscale.png | Bin .../Workflows}/assets/groupsconditioning.png | Bin .../Workflows}/assets/groupscontrol.png | Bin .../Workflows}/assets/groupsimgvae.png | Bin .../Workflows}/assets/groupsiterate.png | Bin .../Workflows}/assets/groupslora.png | Bin .../assets/groupsmultigenseeding.png | Bin .../Workflows}/assets/groupsnoise.png | Bin .../Workflows}/assets/linearview.png | Bin .../Workflows}/assets/nodescontrol.png | Bin .../Workflows}/assets/nodesi2i.png | Bin .../Workflows}/assets/nodest2i.png | Bin .../Workflows}/assets/workflow_library.png | Bin .../Workflows}/comfyui-migration.mdx | 0 .../Workflows}/community-nodes.mdx | 0 .../Workflows}/editor-interface.mdx | 0 .../Workflows}/index.mdx | 0 docs/src/content/docs/features/gallery.mdx | 2 + docs/src/content/docs/features/hotkeys.mdx | 2 + 32 files changed, 3433 insertions(+), 6 deletions(-) rename docs/src/content/docs/features/{ => Canvas}/text-tool.mdx (98%) create mode 100644 docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx create mode 100644 docs/src/content/docs/features/Multi-User Mode/api-guide.mdx create mode 100644 docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-1.png create mode 100644 docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-2.png create mode 100644 docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-3.png create mode 100644 docs/src/content/docs/features/Multi-User Mode/assets/admin-setup.png create mode 100644 docs/src/content/docs/features/Multi-User Mode/assets/user-login-1.png create mode 100644 docs/src/content/docs/features/Multi-User Mode/specification.mdx create mode 100644 docs/src/content/docs/features/Multi-User Mode/user-guide.mdx rename docs/src/content/docs/{workflows => features/Workflows}/adding-nodes.mdx (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupsallscale.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupsconditioning.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupscontrol.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupsimgvae.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupsiterate.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupslora.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupsmultigenseeding.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/groupsnoise.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/linearview.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/nodescontrol.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/nodesi2i.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/nodest2i.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/assets/workflow_library.png (100%) rename docs/src/content/docs/{workflows => features/Workflows}/comfyui-migration.mdx (100%) rename docs/src/content/docs/{workflows => features/Workflows}/community-nodes.mdx (100%) rename docs/src/content/docs/{workflows => features/Workflows}/editor-interface.mdx (100%) rename docs/src/content/docs/{workflows => features/Workflows}/index.mdx (100%) diff --git a/Makefile b/Makefile index ecf101f1d55..2c7d762f83e 100644 --- a/Makefile +++ b/Makefile @@ -91,4 +91,5 @@ openapi: # Serve the mkdocs site w/ live reload .PHONY: docs docs: - mkdocs serve + cd docs && pnpm install && \ + pnpm run dev diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 46bfcf28d39..265a28482b5 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -82,11 +82,6 @@ export default defineConfig({ label: 'Features', autogenerate: { directory: 'features' }, }, - { - label: 'Workflows', - autogenerate: { directory: 'workflows' }, - collapsed: true, - }, { label: 'Development', autogenerate: { directory: 'development', collapsed: true }, diff --git a/docs/src/content/docs/features/text-tool.mdx b/docs/src/content/docs/features/Canvas/text-tool.mdx similarity index 98% rename from docs/src/content/docs/features/text-tool.mdx rename to docs/src/content/docs/features/Canvas/text-tool.mdx index a12a63c7e20..f131895e26f 100644 --- a/docs/src/content/docs/features/text-tool.mdx +++ b/docs/src/content/docs/features/Canvas/text-tool.mdx @@ -1,5 +1,7 @@ --- title: Text Tool +sidebar: + order: 2 --- import { LinkCard } from '@astrojs/starlight/components'; diff --git a/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx new file mode 100644 index 00000000000..5027a79a03a --- /dev/null +++ b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx @@ -0,0 +1,845 @@ +--- +title: Multi-User Administrator Guide +description: How to set up and manage a multi-user InvokeAI installation. +sidebar: + order: 4 +--- + +## Overview + +This guide is for administrators managing a multi-user InvokeAI installation. It covers initial setup, user management, security best practices, and troubleshooting. + +## Prerequisites + +Before enabling multi-user support, ensure you have: + +- InvokeAI installed and running +- Access to the server filesystem (for initial setup) +- Understanding of your deployment environment +- Backup of your existing data (recommended) + +## Initial Setup + +### Activating Multiuser Mode + +To put InvokeAI into multiuser mode, you will need to add the option `multiuser: true` to its configuration file. This file is located at `INVOKEAI_ROOT/invokeai.yaml`. With the InvokeAI backend halted, add the new configuration option to the end of the file with a text editor so that it looks like this: + +```yaml +# Internal metadata - do not edit: +schema_version: 4.0.2 + +# Enable/disable multi-user mode +multiuser: true +``` + +Then restart the InvokeAI server backend from the command line or using the launcher. + +:::note[Reverting to single-user mode] +If at any time you wish to revert to single-user mode, simply comment out the `multiuser` line, or change "true" to "false". Then restart the server. Because of the way that browsers cache pages, users with open InvokeAI sessions may need to force-refresh their browsers. +::: + +### First Administrator Account + +When InvokeAI starts for the first time in multi-user mode, you'll see the **Administrator Setup** dialog. + +**Setup Steps:** + +1. **Email Address**: Enter a valid email address (this becomes your username) + + - Example: `admin@example.com` or `admin@localhost` for testing + - Must be a valid email format + - Cannot be changed later without database access + +2. **Display Name**: Enter a friendly name + + - Example: "System Administrator" or your real name + - Can be changed later in your profile + - Visible to other users in shared contexts + +3. **Password**: Create a strong administrator password + + - **Minimum requirements:** + + - At least 8 characters long + - Contains uppercase letters (A-Z) + - Contains lowercase letters (a-z) + - Contains numbers (0-9) + + - **Recommended:** + + - Use 12+ characters + - Include special characters (!@#$%^&*) + - Use a password manager to generate and store + - Don't reuse passwords from other services + +4. **Confirm Password**: Re-enter the password + +5. Click **Create Administrator Account** + +:::caution[Important] +Store these credentials securely! The first administrator account can reset the password to something new, but cannot retrieve a lost one. +::: + +### Configuration + +InvokeAI can run in single-user or multi-user mode, controlled by the `multiuser` configuration option in `invokeai.yaml`: + +```yaml +# Enable/disable multi-user mode +multiuser: true # Enable multi-user mode (requires authentication) +# multiuser: false # Single-user mode (no authentication required) +# If the multiuser option is absent, single-user mode is used + +# Database configuration +use_memory_db: false # Use persistent database +db_path: databases/invokeai.db # Database location + +# Session configuration (multi-user mode only) +jwt_secret_key: "your-secret-key-here" # Auto-generated if not specified +jwt_token_expiry_hours: 24 # Default session timeout +jwt_remember_me_days: 7 # "Remember me" duration +``` + +**Single-User Mode** (`multiuser: false` or option absent): + +- No authentication required +- All functionality enabled by default +- All boards and images visible in unified view +- Ideal for personal use or trusted environments + +**Multi-User Mode** (`multiuser: true`): + +- Authentication required for access +- User isolation for boards, images, and workflows +- Role-based permissions enforced +- Ideal for shared servers or team environments + +:::caution[Mode Switching Behavior] +**Switching to Single-User Mode:** If boards or images were created in multi-user mode, they will all be combined into a single unified view when switching to single-user mode. + +**Switching to Multi-User Mode:** Legacy boards and images created under single-user mode will be owned by an internal user named "system." Only the Administrator will have access to these legacy assets. A utility to migrate these legacy assets to another user will be part of a future release. +::: + +### Migration from Single-User + +When upgrading from a single-user installation or switching modes: + +1. **Automatic Migration**: The database will automatically migrate to multi-user schema when multi-user mode is first enabled +2. **Legacy Data Ownership**: Existing data (boards, images, workflows) created in single-user mode is assigned to an internal user named "system" +3. **Administrator Access**: Only administrators will have access to legacy "system"-owned assets when in multi-user mode +4. **No Data Loss**: All existing content is preserved + +**Migration Process:** + +```bash +# Backup your database first +cp databases/invokeai.db databases/invokeai.db.backup + +# Enable multi-user mode in invokeai.yaml +# multiuser: true + +# Start InvokeAI (migration happens automatically) +invokeai-web + +# Complete the administrator setup dialog +# Legacy data will be owned by "system" user +``` + +:::note[Legacy Asset Migration] +A utility to migrate legacy "system"-owned assets to specific user accounts will be available in a future release. Until then, administrators can access and manage all legacy content. +::: + +## User Management + +### Creating Users + +**Via Web Interface (Coming Soon):** + +:::note[Web UI for User Management] +A web-based user interface that allows administrators to manage users is coming in a future release. Until then, use the command-line scripts described below. +::: + +**Via Command Line Scripts:** + +InvokeAI provides several command-line scripts in the `scripts/` directory for user management: + +**useradd.py** — Add a new user: + +```bash +# Interactive mode (prompts for details) +python scripts/useradd.py + +# Create a regular user +python scripts/useradd.py \ + --email user@example.com \ + --password TempPass123 \ + --name "User Name" + +# Create an administrator +python scripts/useradd.py \ + --email admin@example.com \ + --password AdminPass123 \ + --name "Admin Name" \ + --admin +``` + +**userlist.py** — List all users: + +```bash +# List all users +python scripts/userlist.py + +# Show detailed information +python scripts/userlist.py --verbose +``` + +**usermod.py** — Modify an existing user: + +```bash +# Change display name +python scripts/usermod.py --email user@example.com --name "New Name" + +# Promote to administrator +python scripts/usermod.py --email user@example.com --admin + +# Demote from administrator +python scripts/usermod.py --email user@example.com --no-admin + +# Deactivate account +python scripts/usermod.py --email user@example.com --deactivate + +# Reactivate account +python scripts/usermod.py --email user@example.com --activate + +# Change password +python scripts/usermod.py --email user@example.com --password NewPassword123 +``` + +**userdel.py** — Delete a user: + +```bash +# Delete a user (prompts for confirmation) +python scripts/userdel.py --email user@example.com + +# Delete without confirmation +python scripts/userdel.py --email user@example.com --force +``` + +:::tip[Script Usage] +Run any script with `--help` to see all available options: + +```bash +python scripts/useradd.py --help +``` +::: + +:::caution[Command Line Management] +- These scripts directly modify the database +- Always backup your database before making changes +- Changes take effect immediately (users may need to log in again) +- Deleting a user permanently removes all their content +::: + +### Editing Users + +**Via Command Line:** + +Use `usermod.py` as described above to modify user properties. + +:::caution[Last Administrator] +You cannot remove admin privileges from the last remaining administrator account. +::: + +### Resetting User Passwords + +**Via Web Interface (Coming Soon):** + +Web-based password reset functionality for administrators is coming in a future release. + +**Via Command Line:** + +```bash +# Reset a user's password +python scripts/usermod.py --email user@example.com --password NewTempPassword123 +``` + +**Security Note:** Never send passwords via email or unsecured channels. Use secure communication methods. + +### Deactivating Users + +**Via Command Line:** + +```bash +# Deactivate a user account +python scripts/usermod.py --email user@example.com --deactivate + +# Reactivate a user account +python scripts/usermod.py --email user@example.com --activate +``` + +**Effects:** + +- User cannot log in when deactivated +- Existing sessions are immediately invalidated +- User's data is preserved +- Can be reactivated at any time + +### Deleting Users + +**Via Command Line:** + +```bash +# Delete a user (prompts for confirmation) +python scripts/userdel.py --email user@example.com + +# Delete without confirmation prompt +python scripts/userdel.py --email user@example.com --force +``` + +**Important:** + +- This action is **permanent** +- User's boards, images, and workflows are deleted +- Cannot be undone +- Consider deactivating instead of deleting + +:::danger[Data Loss] +Deleting a user permanently removes all their content. Back up the database first if recovery might be needed. +::: + +### Viewing User Activity + +**Queue Management:** + +1. Navigate to **Admin** → **Queue Overview** +2. View all users' active and pending generations +3. Filter by user +4. Cancel stuck or problematic tasks + +**User Statistics:** + +- Number of boards created +- Number of images generated +- Storage usage (if enabled) +- Last login time + +## Model Management + +As an administrator, you have full access to model management. + +### Adding Models + +**Via Model Manager UI:** + +1. Go to **Models** tab +2. Click **Add Model** +3. Choose installation method: + - **From URL**: Provide HuggingFace repo or download URL + - **From Local Path**: Scan local directories + - **Import**: Import model from filesystem + +**Supported Model Types:** + +- Main models (Stable Diffusion, SDXL, FLUX) +- LoRA models +- ControlNet models +- VAE models +- Textual Inversions +- IP-Adapters + +### Configuring Models + +**Model Settings:** + +- Display name +- Description +- Default generation settings (CFG, steps, scheduler) +- Variant selection (fp16/fp32) +- Model thumbnail image + +**Default Settings:** + +Set default parameters that users will start with: + +1. Select a model +2. Go to **Default Settings** tab +3. Configure: + - CFG Scale + - Steps + - Scheduler + - VAE selection +4. Save settings + +### Removing Models + +1. Go to **Models** tab +2. Select model(s) to remove +3. Click **Delete** +4. Confirm deletion + +:::caution[Impact] +Removing a model affects all users who may be using it in workflows or saved settings. +::: + +## Shared Boards + +Shared boards enable collaboration between users while maintaining control. + +:::note[Future Feature] +Board sharing will be implemented in a future release. +::: + +### Creating Shared Boards + +1. Log in as administrator +2. Create a new board (or use existing board) +3. Right-click the board → **Share Board** +4. Add users and set permissions +5. Click **Save Sharing Settings** + +### Permission Levels + +| Level | View | Add Images | Edit/Delete | Manage Sharing | +|-------|------|------------|-------------|----------------| +| **Read** | Yes | No | No | No | +| **Write** | Yes | Yes | Yes | No | +| **Admin** | Yes | Yes | Yes | Yes | + +**Permission Recommendations:** + +- **Read**: For viewers who should see but not modify content +- **Write**: For active collaborators who add and organize images +- **Admin**: For trusted users who help manage the shared board + +### Managing Shared Boards + +**Add Users to Shared Board:** + +1. Right-click shared board → **Manage Sharing** +2. Click **Add User** +3. Select user from dropdown +4. Choose permission level +5. Save changes + +**Remove Users from Shared Board:** + +1. Right-click shared board → **Manage Sharing** +2. Find user in list +3. Click **Remove** +4. Confirm removal + +**Change User Permissions:** + +1. Right-click shared board → **Manage Sharing** +2. Find user in list +3. Change permission dropdown +4. Save changes + +### Shared Board Best Practices + +- Give meaningful names to shared boards +- Document the board's purpose in the description +- Assign minimum necessary permissions +- Regularly audit access lists +- Remove users who no longer need access + +## Security + +### Password Policies + +**Enforced Requirements:** + +- Minimum 8 characters +- Must contain uppercase letters +- Must contain lowercase letters +- Must contain numbers + +**Recommended Policies:** + +- Require 12+ character passwords +- Include special characters +- Implement password rotation every 90 days +- Prevent password reuse +- Use multi-factor authentication (when available) + +### Session Management + +**Session Security and Token Management:** + +This system uses stateless JWT tokens with HMAC signatures to identify users after they provide their initial credentials. The tokens will persist for 24 hours by default, or for 7 days if the user clicks the "Remember me" checkbox at login. Expired tokens are automatically rejected and the user will have to log in again. + +At the client side, tokens are stored in browser localStorage. Logging out clears them. No server-side session storage is required. + +The tokens include the user's ID, email, and admin status, along with an HMAC signature. + +### Secret Key Management + +**Important:** The JWT secret key must be kept confidential. + +To generate tokens, each InvokeAI instance has a distinct secret JWT key that must be kept confidential. The key is stored in the `app_settings` table of the InvokeAI database with in a field value named `jwt_secret`. + +The secret key is automatically generated during database creation or migration. If you wish to change the key, you may generate a replacement using either of these commands: + +```bash +# Python +python -c "import secrets; print(secrets.token_urlsafe(32))" + +# OpenSSL +openssl rand -base64 32 +``` + +Then cut and paste the printed secret into this Sqlite3 command: + +```bash +sqlite3 INVOKE_ROOT/databases/invokeai.db 'update app_settings set value="THE_SECRET" where key="jwt_secret"' +``` + +(replace INVOKE_ROOT with your InvokeAI root directory and THE_SECRET with the new secret). + +After this, restart the server. All logged in users will be logged out and will need to provide their usernames and passwords again. + +### Hosting a Shared InvokeAI Instance + +The multiuser feature allows you to run an InvokeAI backend that can be accessed by your friends and family across your home network. It is also possible to host a backend that is accessible over the Internet. + +By default, InvokeAI runs on `localhost`, IP address `127.0.0.1`, which is only accessible to browsers running on the same machine as the backend. To make the backend accessible to any machine on your home or work LAN, add the line `host: 0.0.0.0` to the InvokeAI configuration file, usually stored at `INVOKE_ROOT/invokeai.yaml`. + +Here is a minimal example. + +```yaml +# Internal metadata - do not edit: +schema_version: 4.0.2 + +# Put user settings here - see https://invoke-ai.github.io/InvokeAI/configuration/: +multiuser: true +host: 0.0.0.0 +``` + +After relaunching the backend you will be able to reach the server from other machines on the LAN using the server machine's IP address or hostname and port 9090. + +#### Connecting to the Internet + +:::danger[Use at your own risk] +The InvokeAI team has done its best to make the software free of exploitable bugs, but the software has not undergone a rigorous security audit or intrusion testing. Use at your own risk. +::: + +It is also possible to create a (semi) public server accessible from the Internet. The details of how to do this depend very much on your home or corporate router/firewall system and are beyond the scope of this document. + +If you expose InvokeAI to the Internet, there are a number of precautions to take. Here is a brief list of recommended network security practices. + +**HTTPS Configuration:** + +For internet deployments, always use HTTPS: + +```nginx +# Use a reverse proxy like nginx or Traefik +# Example nginx configuration: + +server { + listen 443 ssl http2; + server_name invoke.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://localhost:9090; + 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; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +**Firewall Rules:** + +It is best to restrict access to trusted networks and remote IP addresses, or use a VPN to connect to your home network. Rate limit connections to InvokeAI's authentication endpoint `http://your.host:9090/login`. + +**Backup and Recovery:** + +It is a good idea to periodically backup your InvokeAI database, images, and possibly models in the event of unauthorized use of a publicly-accessible server. + +**Manual Backup:** + +```bash +# Stop InvokeAI +# Copy database file +cd INVOKE_ROOT +cp databases/invokeai.db databases/invokeai.db.$(date +%Y%m%d) + +# Or create compressed backup +tar -czf invokeai_backup_$(date +%Y%m%d).tar.gz databases/ +``` + +**Automated Backup Script:** + +```bash +#!/bin/bash +# backup_invokeai.sh + +INVOKE_ROOT="/path/to/invoke_root" +BACKUP_DIR="/path/to/backups" +DB_PATH="$INVOKE_ROOT/databases/invokeai.db" +DATE=$(date +%Y%m%d_%H%M%S) + +# Create backup directory +mkdir -p "$BACKUP_DIR" + +# Copy database +cp "$DB_PATH" "$BACKUP_DIR/invokeai_$DATE.db" + +# Keep only last 30 days +find "$BACKUP_DIR" -name "invokeai_*.db" -mtime +30 -delete + +echo "Backup completed: invokeai_$DATE.db" +``` + +**Schedule with cron:** + +```bash +# Edit crontab +crontab -e + +# Add daily backup at 2 AM +0 2 * * * /path/to/backup_invokeai.sh +``` + +**Restore from Backup:** + +```bash +# Stop InvokeAI +# Replace current database with backup +cd INVOKE_ROOT +cp databases/invokeai.db databases/invokeai.db.old # Save current +cp databases/invokeai_backup.db databases/invokeai.db + +# Restart InvokeAI +invokeai-web +``` + +**Disaster Recovery — Complete System Backup:** + +Include these directories/files: + +- `databases/` — All database files +- `models/` — Installed models (if locally stored) +- `outputs/` — Generated images +- `invokeai.yaml` — Configuration file +- Any custom scripts or modifications + +**Recovery Process:** + +1. Install InvokeAI on new system +2. Restore configuration file +3. Restore database directory +4. Restore models and outputs +5. Verify file permissions +6. Start InvokeAI and test + +## Troubleshooting + +### User Cannot Login + +**Symptom:** User reports unable to log in + +**Diagnosis:** + +1. Verify account exists and is active + + ```bash + sqlite3 databases/invokeai.db "SELECT * FROM users WHERE email = 'user@example.com';" + ``` + +2. Check password (have user try resetting) +3. Verify account is active (`is_active = 1`) +4. Check for account lockout (if implemented) + +**Solutions:** + +- Reset user password +- Reactivate disabled account +- Verify email address is correct +- Check system logs for auth errors + +### Database Locked Errors + +**Symptom:** "Database is locked" errors + +**Causes:** + +- Concurrent write operations +- Long-running transactions +- Backup process accessing database +- File system issues + +**Solutions:** + +```bash +# Check for locks +fuser databases/invokeai.db + +# Increase timeout (in config) +# Or switch to WAL mode: +sqlite3 databases/invokeai.db "PRAGMA journal_mode=WAL;" +``` + +### Forgotten Admin Password + +**Recovery Process:** + +1. Stop InvokeAI +2. Direct database access: + + ```bash + sqlite3 databases/invokeai.db + ``` + +3. Reset admin password (requires password hash): + + ```sql + -- Generate hash first using Python: + -- from passlib.context import CryptContext + -- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + -- print(pwd_context.hash("NewPassword123")) + + UPDATE users + SET password_hash = '$2b$12$...' + WHERE email = 'admin@example.com'; + ``` + +4. Restart InvokeAI + +**Alternative:** Remove `jwt_secret_key` from config to trigger setup wizard (will create new admin). + +### Performance Issues + +**Symptom:** Slow generation or UI + +**Diagnosis:** + +1. Check active generation count +2. Review resource usage (CPU/GPU/RAM) +3. Check database size and performance +4. Review network latency + +**Solutions:** + +- Limit concurrent generations +- Increase hardware resources +- Optimize database (`VACUUM`, `ANALYZE`) +- Add indexes for slow queries +- Consider load balancing + +### Migration Failures + +**Symptom:** Database migration fails on upgrade + +**Prevention:** + +- Always backup before upgrading +- Test migration on copy of database +- Review migration logs + +**Recovery:** + +```bash +# Restore backup +cp databases/invokeai.db.backup databases/invokeai.db + +# Try migration again with verbose logging +invokeai-web --log-level DEBUG +``` + +## Configuration Reference + +### Complete Configuration Example for a Public Site + +```yaml +# invokeai.yaml - Multi-user configuration + +# Internal metadata - do not edit: +schema_version: 4.0.2 + +# Put user settings here +multiuser: true + +# Server +host: "0.0.0.0" +port: 9090 + +# Performance +enable_partial_loading: true +precision: float16 +pytorch_cuda_alloc_conf: "backend:cudaMallocAsync" +hashing_algorithm: blake3_multi +``` + +## Frequently Asked Questions + +### How many users can InvokeAI support? + +The backend will support dozens of concurrent users. However, because the image generation queue is single-threaded, image generation tasks are processed on a first-come, first-serve basis. This means that a user may have to wait for all the other users' image generation jobs to complete before their generation job starts to execute. + +A future version of InvokeAI may support concurrent execution on systems with multiple GPUs/graphics cards. + +### Can I integrate with existing authentication systems? + +OAuth2/OpenID Connect support is planned for a future release. Currently, InvokeAI uses its own authentication system. + +### How do I audit user actions? + +Full audit logging is planned for a future release. Currently, you can: + +- Monitor the generation queue +- Review database changes +- Check application logs + +### Can users have different model access? + +Not in the current release. All users can view and use all installed models. Per-user model access is a possible enhancement. + +### How do I handle user data when they leave? + +Best practice: + +1. Deactivate the account first +2. Transfer ownership of shared boards +3. After transition period, delete the account +4. Or keep the account deactivated for audit purposes + +### What's the licensing impact of multi-user mode? + +InvokeAI remains under its existing license. Multi-user mode does not change licensing terms. + +## Getting Help + +### Support Resources + +- **Documentation**: [InvokeAI Docs](https://invoke.ai/) +- **Discord**: [Join Community](https://discord.gg/ZmtBAhwWhy) +- **GitHub Issues**: [Report Problems](https://github.com/invoke-ai/InvokeAI/issues) +- **User Guide**: [For Users](/features/multi-user/user-guide/) +- **API Guide**: [For Developers](/features/multi-user/api-guide/) + +### Reporting Issues + +When reporting administrator issues, include: + +- InvokeAI version +- Operating system and version +- Database size and user count +- Relevant log excerpts +- Steps to reproduce +- Expected vs actual behavior + +## Additional Resources + +- [User Guide](/features/multi-user/user-guide/) — For end users +- [API Guide](/features/multi-user/api-guide/) — For API consumers +- [Multi-User Specification](/features/multi-user/specification/) — Technical details diff --git a/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx new file mode 100644 index 00000000000..fa5077261b2 --- /dev/null +++ b/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx @@ -0,0 +1,1231 @@ +--- +title: Multi-User API Guide +description: How to authenticate and interact with the InvokeAI API in multi-user mode. +sidebar: + order: 5 +--- + +## Overview + +This guide explains how to interact with InvokeAI's API in both single-user and multi-user modes. The API behavior depends on the `multiuser` configuration setting. + +### Single-User vs Multi-User Mode + +**Single-User Mode** (`multiuser: false` or option absent): + +- No authentication required +- All API endpoints accessible without tokens +- Direct API access like previous InvokeAI versions +- All content visible in unified view + +**Multi-User Mode** (`multiuser: true`): + +- JWT token authentication required +- User-scoped access to resources +- Role-based authorization (admin vs regular user) +- Data isolation between users + +## Authentication (Multi-User Mode Only) + +### Authentication Flow + +When multi-user mode is enabled, all API endpoints (except `/api/v1/auth/setup` and `/api/v1/auth/login`) require authentication using JWT (JSON Web Token) bearer tokens. + +**Authentication Process:** + +1. **Obtain Token**: POST credentials to `/api/v1/auth/login` +2. **Store Token**: Save the JWT token securely +3. **Use Token**: Include token in `Authorization` header for all requests +4. **Refresh**: Re-authenticate when token expires + +:::note[Single-User Mode] +When running in single-user mode (`multiuser: false`), authentication endpoints are not available and authentication headers are not required. +::: + +### Login Endpoint + +**Endpoint:** `POST /api/v1/auth/login` + +**Request:** + +```json +{ + "email": "user@example.com", + "password": "SecurePassword123", + "remember_me": false +} +``` + +**Response (Success):** + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "user": { + "user_id": "abc123", + "email": "user@example.com", + "display_name": "John Doe", + "is_admin": false, + "is_active": true, + "created_at": "2024-01-15T10:00:00Z" + }, + "expires_in": 86400 +} +``` + +**Response (Error):** + +```json +{ + "detail": "Incorrect email or password" +} +``` + +**Status Codes:** + +- `200 OK` — Authentication successful +- `401 Unauthorized` — Invalid credentials +- `403 Forbidden` — Account disabled +- `422 Unprocessable Entity` — Invalid request format + +### Using the Token + +Include the JWT token in the `Authorization` header with the `Bearer` scheme: + +**HTTP Header:** + +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +**Example HTTP Request:** + +```http +GET /api/v1/boards HTTP/1.1 +Host: localhost:9090 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +Content-Type: application/json +``` + +### Token Expiration + +Tokens have a limited lifetime: + +- **Default**: 24 hours (86400 seconds) +- **Remember Me**: 7 days (604800 seconds) + +**Handling Expiration:** + +```python +import requests +import time + +def api_request(url, token, max_retries=1): + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(url, headers=headers) + + if response.status_code == 401: # Token expired + # Re-authenticate and retry + new_token = login() + headers = {"Authorization": f"Bearer {new_token}"} + response = requests.get(url, headers=headers) + + return response +``` + +### Logout Endpoint + +**Endpoint:** `POST /api/v1/auth/logout` + +**Request:** + +```http +POST /api/v1/auth/logout HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +**Response:** + +```json +{ + "success": true +} +``` + +**Note:** With JWT tokens, logout is primarily client-side (delete token). Server-side session invalidation may be added in future releases. + +## Code Examples + +### Python + +**Using `requests` library:** + +```python +import requests +import json + +class InvokeAIClient: + def __init__(self, base_url="http://localhost:9090"): + self.base_url = base_url + self.token = None + + def login(self, email, password, remember_me=False): + """Authenticate and store token.""" + url = f"{self.base_url}/api/v1/auth/login" + payload = { + "email": email, + "password": password, + "remember_me": remember_me + } + + response = requests.post(url, json=payload) + response.raise_for_status() + + data = response.json() + self.token = data["token"] + return data["user"] + + def _get_headers(self): + """Get headers with authentication token.""" + if not self.token: + raise Exception("Not authenticated. Call login() first.") + + return { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json" + } + + def get_boards(self): + """Get user's boards.""" + url = f"{self.base_url}/api/v1/boards/" + response = requests.get(url, headers=self._get_headers()) + response.raise_for_status() + return response.json() + + def create_board(self, board_name): + """Create a new board.""" + url = f"{self.base_url}/api/v1/boards/" + payload = {"board_name": board_name} + + response = requests.post( + url, + json=payload, + headers=self._get_headers() + ) + response.raise_for_status() + return response.json() + + def logout(self): + """Logout and clear token.""" + url = f"{self.base_url}/api/v1/auth/logout" + response = requests.post(url, headers=self._get_headers()) + self.token = None + return response.json() + +# Usage +client = InvokeAIClient() +user = client.login("user@example.com", "SecurePassword123") +print(f"Logged in as: {user['display_name']}") + +boards = client.get_boards() +print(f"User has {len(boards['items'])} boards") + +new_board = client.create_board("My New Board") +print(f"Created board: {new_board['board_name']}") + +client.logout() +``` + +**Error Handling:** + +```python +import requests +from requests.exceptions import HTTPError + +def safe_api_call(client, method, *args, **kwargs): + """Make API call with error handling.""" + try: + func = getattr(client, method) + return func(*args, **kwargs) + + except HTTPError as e: + if e.response.status_code == 401: + print("Authentication failed or token expired") + # Re-authenticate + client.login(email, password) + # Retry + return func(*args, **kwargs) + elif e.response.status_code == 403: + print("Permission denied") + elif e.response.status_code == 404: + print("Resource not found") + else: + print(f"API error: {e.response.status_code}") + print(e.response.text) + + raise + +# Usage +try: + boards = safe_api_call(client, "get_boards") +except Exception as e: + print(f"Failed to get boards: {e}") +``` + +### JavaScript/TypeScript + +**Using `fetch` API:** + +```javascript +class InvokeAIClient { + constructor(baseUrl = 'http://localhost:9090') { + this.baseUrl = baseUrl; + this.token = null; + } + + async login(email, password, rememberMe = false) { + const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + password, + remember_me: rememberMe, + }), + }); + + if (!response.ok) { + throw new Error(`Login failed: ${response.statusText}`); + } + + const data = await response.json(); + this.token = data.token; + + // Store token in localStorage + localStorage.setItem('invokeai_token', data.token); + + return data.user; + } + + getHeaders() { + if (!this.token) { + throw new Error('Not authenticated. Call login() first.'); + } + + return { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + }; + } + + async getBoards() { + const response = await fetch(`${this.baseUrl}/api/v1/boards/`, { + headers: this.getHeaders(), + }); + + if (!response.ok) { + throw new Error(`Failed to get boards: ${response.statusText}`); + } + + return response.json(); + } + + async createBoard(boardName) { + const response = await fetch(`${this.baseUrl}/api/v1/boards/`, { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify({ board_name: boardName }), + }); + + if (!response.ok) { + throw new Error(`Failed to create board: ${response.statusText}`); + } + + return response.json(); + } + + async logout() { + const response = await fetch(`${this.baseUrl}/api/v1/auth/logout`, { + method: 'POST', + headers: this.getHeaders(), + }); + + this.token = null; + localStorage.removeItem('invokeai_token'); + + return response.json(); + } +} + +// Usage +(async () => { + const client = new InvokeAIClient(); + + try { + const user = await client.login('user@example.com', 'SecurePassword123'); + console.log(`Logged in as: ${user.display_name}`); + + const boards = await client.getBoards(); + console.log(`User has ${boards.items.length} boards`); + + const newBoard = await client.createBoard('My New Board'); + console.log(`Created board: ${newBoard.board_name}`); + + await client.logout(); + } catch (error) { + console.error('Error:', error.message); + } +})(); +``` + +**TypeScript with Types:** + +```typescript +interface LoginRequest { + email: string; + password: string; + remember_me?: boolean; +} + +interface User { + user_id: string; + email: string; + display_name: string; + is_admin: boolean; + is_active: boolean; + created_at: string; +} + +interface LoginResponse { + token: string; + user: User; + expires_in: number; +} + +interface Board { + board_id: string; + board_name: string; + created_at: string; + updated_at: string; + deleted_at?: string; + cover_image_name?: string; +} + +class InvokeAIClient { + private baseUrl: string; + private token: string | null = null; + + constructor(baseUrl: string = 'http://localhost:9090') { + this.baseUrl = baseUrl; + } + + async login( + email: string, + password: string, + rememberMe: boolean = false + ): Promise { + const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password, remember_me: rememberMe }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Login failed'); + } + + const data: LoginResponse = await response.json(); + this.token = data.token; + return data.user; + } + + private getHeaders(): HeadersInit { + if (!this.token) { + throw new Error('Not authenticated'); + } + return { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + }; + } + + async getBoards(): Promise<{ items: Board[] }> { + const response = await fetch(`${this.baseUrl}/api/v1/boards/`, { + headers: this.getHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to get boards'); + } + + return response.json(); + } +} +``` + +### cURL + +**Login:** + +```bash +# Login and extract token +TOKEN=$(curl -X POST http://localhost:9090/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "user@example.com", + "password": "SecurePassword123", + "remember_me": false + }' | jq -r '.token') + +echo "Token: $TOKEN" +``` + +**Get Boards:** + +```bash +curl -X GET http://localhost:9090/api/v1/boards/ \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" +``` + +**Create Board:** + +```bash +curl -X POST http://localhost:9090/api/v1/boards/ \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "board_name": "My API Board" + }' +``` + +**Generate Image:** + +```bash +curl -X POST http://localhost:9090/api/v1/sessions/ \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "prompt": "A beautiful landscape", + "width": 512, + "height": 512, + "steps": 30 + }' +``` + +## API Endpoint Changes + +### Authentication Required + +All endpoints now require authentication except: + +- `POST /api/v1/auth/setup` — Initial admin setup +- `POST /api/v1/auth/login` — User login + +### User-Scoped Resources + +Resources are now filtered by the authenticated user: + +**Boards:** + +```python +# Before (single-user) +GET /api/v1/boards/ # Returns all boards + +# After (multi-user) +GET /api/v1/boards/ # Returns only current user's boards +``` + +**Images:** + +```python +# Images are filtered by board ownership +GET /api/v1/images/ # Only shows images on user's boards +``` + +**Workflows:** + +```python +# Returns user's workflows + public workflows +GET /api/v1/workflows/ +``` + +**Queue:** + +```python +# Regular users see only their queue items +GET /api/v1/queue/ # User's queue items + +# Administrators see all queue items +GET /api/v1/queue/ # All users' queue items +``` + +### Administrator Endpoints + +Some endpoints require administrator privileges: + +**User Management:** + +```python +GET /api/v1/users # List users (admin only) +POST /api/v1/users # Create user (admin only) +GET /api/v1/users/{id} # Get user (admin only) +PATCH /api/v1/users/{id} # Update user (admin only) +DELETE /api/v1/users/{id} # Delete user (admin only) +``` + +**Model Management (Write Operations):** + +```python +POST /api/v1/models/install # Install model (admin only) +DELETE /api/v1/models/i/{key} # Delete model (admin only) +PATCH /api/v1/models/i/{key} # Update model (admin only) +PUT /api/v1/models/convert/{key} # Convert model (admin only) +``` + +**Model Management (Read Operations):** + +```python +GET /api/v1/models/ # List models (all users) +GET /api/v1/models/i/{key} # Get model details (all users) +``` + +### Error Responses + +**401 Unauthorized:** + +```json +{ + "detail": "Invalid authentication credentials" +} +``` + +Occurs when: + +- Token is missing +- Token is invalid +- Token is expired +- Token signature is invalid + +**403 Forbidden:** + +```json +{ + "detail": "Admin privileges required" +} +``` + +Occurs when: + +- User attempts admin-only operation +- Account is disabled +- Insufficient permissions + +**404 Not Found:** + +```json +{ + "detail": "Resource not found" +} +``` + +Occurs when: + +- Resource doesn't exist +- User doesn't have access to resource + +## New API Endpoints + +### Authentication Endpoints + +#### Setup Administrator + +**Endpoint:** `POST /api/v1/auth/setup` + +**Description:** Create initial administrator account (only works if no admin exists) + +**Request:** + +```json +{ + "email": "admin@example.com", + "display_name": "Administrator", + "password": "SecureAdminPass123" +} +``` + +**Response:** + +```json +{ + "success": true, + "user": { + "user_id": "abc123", + "email": "admin@example.com", + "display_name": "Administrator", + "is_admin": true, + "is_active": true + } +} +``` + +#### Get Current User + +**Endpoint:** `GET /api/v1/auth/me` + +**Description:** Get currently authenticated user's information + +**Request:** + +```http +GET /api/v1/auth/me +Authorization: Bearer +``` + +**Response:** + +```json +{ + "user_id": "abc123", + "email": "user@example.com", + "display_name": "John Doe", + "is_admin": false, + "is_active": true, + "created_at": "2024-01-15T10:00:00Z", + "updated_at": "2024-01-15T10:00:00Z", + "last_login_at": "2024-01-15T15:30:00Z" +} +``` + +#### Change Password + +**Endpoint:** `POST /api/v1/auth/change-password` + +**Description:** Change current user's password + +**Request:** + +```json +{ + "current_password": "OldPassword123", + "new_password": "NewPassword456" +} +``` + +**Response:** + +```json +{ + "success": true +} +``` + +### User Management Endpoints (Admin Only) + +#### List Users + +**Endpoint:** `GET /api/v1/users` + +**Request:** + +```http +GET /api/v1/users?page=1&per_page=20 +Authorization: Bearer +``` + +**Response:** + +```json +{ + "items": [ + { + "user_id": "abc123", + "email": "user@example.com", + "display_name": "John Doe", + "is_admin": false, + "is_active": true, + "created_at": "2024-01-15T10:00:00Z", + "last_login_at": "2024-01-15T15:30:00Z" + } + ], + "page": 1, + "pages": 1, + "per_page": 20, + "total": 5 +} +``` + +#### Create User + +**Endpoint:** `POST /api/v1/users` + +**Request:** + +```json +{ + "email": "newuser@example.com", + "display_name": "New User", + "password": "TempPassword123", + "is_admin": false +} +``` + +**Response:** + +```json +{ + "user_id": "xyz789", + "email": "newuser@example.com", + "display_name": "New User", + "is_admin": false, + "is_active": true, + "created_at": "2024-01-15T16:00:00Z" +} +``` + +#### Update User + +**Endpoint:** `PATCH /api/v1/users/{user_id}` + +**Request:** + +```json +{ + "display_name": "Updated Name", + "is_active": true, + "is_admin": false +} +``` + +**Response:** + +```json +{ + "user_id": "xyz789", + "email": "newuser@example.com", + "display_name": "Updated Name", + "is_admin": false, + "is_active": true +} +``` + +#### Delete User + +**Endpoint:** `DELETE /api/v1/users/{user_id}` + +**Response:** + +```json +{ + "success": true +} +``` + +#### Reset User Password + +**Endpoint:** `POST /api/v1/users/{user_id}/reset-password` + +**Request:** + +```json +{ + "new_password": "NewTempPass123" +} +``` + +**Response:** + +```json +{ + "success": true +} +``` + +### Board Sharing Endpoints + +#### Share Board + +**Endpoint:** `POST /api/v1/boards/{board_id}/share` + +**Request:** + +```json +{ + "user_id": "user123", + "permission": "write" +} +``` + +**Response:** + +```json +{ + "success": true, + "share": { + "board_id": "board456", + "user_id": "user123", + "permission": "write", + "shared_at": "2024-01-15T16:00:00Z" + } +} +``` + +#### List Board Shares + +**Endpoint:** `GET /api/v1/boards/{board_id}/shares` + +**Response:** + +```json +{ + "items": [ + { + "user_id": "user123", + "display_name": "John Doe", + "permission": "write", + "shared_at": "2024-01-15T16:00:00Z" + } + ] +} +``` + +#### Remove Board Share + +**Endpoint:** `DELETE /api/v1/boards/{board_id}/share/{user_id}` + +**Response:** + +```json +{ + "success": true +} +``` + +## Best Practices + +### Token Storage + +**Do:** + +- Store tokens securely (keychain, secure storage) +- Use HTTPS to transmit tokens +- Clear tokens on logout +- Handle token expiration gracefully + +**Don't:** + +- Store tokens in URL parameters +- Log tokens in plain text +- Share tokens between users +- Store tokens in version control + +### Error Handling + +Always handle authentication errors: + +```python +def make_request(client, func, *args, **kwargs): + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + try: + return func(*args, **kwargs) + except AuthenticationError: + if retry_count >= max_retries - 1: + raise + # Re-authenticate + client.login(email, password) + retry_count += 1 + except Exception as e: + logger.error(f"Request failed: {e}") + raise +``` + +### Rate Limiting + +Be mindful of API rate limits: + +- Implement exponential backoff for retries +- Cache frequently accessed data +- Batch requests when possible +- Don't hammer the login endpoint + +### Connection Management + +```python +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +def create_session(): + """Create session with retry logic.""" + session = requests.Session() + + retry = Retry( + total=3, + backoff_factor=0.3, + status_forcelist=[500, 502, 503, 504], + ) + + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + + return session +``` + +## Migration Guide + +### Updating Existing Code + +**Before (single-user mode):** + +```python +import requests + +def get_boards(): + response = requests.get("http://localhost:9090/api/v1/boards/") + return response.json() +``` + +**After (multi-user mode):** + +```python +import requests + +class APIClient: + def __init__(self): + self.token = None + + def login(self, email, password): + response = requests.post( + "http://localhost:9090/api/v1/auth/login", + json={"email": email, "password": password} + ) + self.token = response.json()["token"] + + def get_boards(self): + headers = {"Authorization": f"Bearer {self.token}"} + response = requests.get( + "http://localhost:9090/api/v1/boards/", + headers=headers + ) + return response.json() + +# Usage +client = APIClient() +client.login("user@example.com", "password") +boards = client.get_boards() +``` + +### Backward Compatibility + +InvokeAI supports both single-user and multi-user modes via the `multiuser` configuration option. + +**Configuration:** + +```yaml +# invokeai.yaml + +# Single-user mode (no authentication) +multiuser: false # or omit the option entirely + +# Multi-user mode (authentication required) +multiuser: true +``` + +**Checking Mode Programmatically:** + +```python +def is_multiuser_enabled(base_url): + """Check if multi-user mode is enabled (authentication required).""" + response = requests.get(f"{base_url}/api/v1/boards/") + return response.status_code == 401 # 401 = auth required + +# Example usage +base_url = "http://localhost:9090" +if is_multiuser_enabled(base_url): + print("Multi-user mode: authentication required") + # Use authenticated API calls +else: + print("Single-user mode: no authentication needed") + # Use direct API calls +``` + +**Adaptive Client:** + +```python +class AdaptiveInvokeAIClient: + def __init__(self, base_url="http://localhost:9090"): + self.base_url = base_url + self.token = None + self.multiuser_mode = self._check_multiuser_mode() + + def _check_multiuser_mode(self): + """Detect if multi-user mode is enabled.""" + try: + response = requests.get(f"{self.base_url}/api/v1/boards/") + return response.status_code == 401 + except: + return False + + def login(self, email, password): + """Login (only needed in multi-user mode).""" + if not self.multiuser_mode: + print("Single-user mode: login not required") + return + + response = requests.post( + f"{self.base_url}/api/v1/auth/login", + json={"email": email, "password": password} + ) + self.token = response.json()["token"] + + def _get_headers(self): + """Get headers (with auth token if in multi-user mode).""" + if self.multiuser_mode and self.token: + return {"Authorization": f"Bearer {self.token}"} + return {} + + def get_boards(self): + """Get boards (works in both modes).""" + response = requests.get( + f"{self.base_url}/api/v1/boards/", + headers=self._get_headers() + ) + return response.json() +``` + +## OpenAPI/Swagger Documentation + +InvokeAI provides OpenAPI documentation for all endpoints. + +**Access Swagger UI:** + +``` +http://localhost:9090/docs +``` + +**Download OpenAPI Schema:** + +```bash +curl http://localhost:9090/openapi.json > invokeai_openapi.json +``` + +**Generate Client Code:** + +Use tools like `openapi-generator` to generate client libraries: + +```bash +# Generate Python client +openapi-generator generate \ + -i http://localhost:9090/openapi.json \ + -g python \ + -o ./invokeai-client + +# Generate TypeScript client +openapi-generator generate \ + -i http://localhost:9090/openapi.json \ + -g typescript-fetch \ + -o ./invokeai-client-ts +``` + +## Security Considerations + +### HTTPS + +Always use HTTPS in production: + +```python +# Development +client = InvokeAIClient("http://localhost:9090") + +# Production +client = InvokeAIClient("https://invoke.example.com") +``` + +### Token Security + +Protect JWT tokens: + +```python +# Never log tokens +logger.info(f"User logged in") # Good +logger.info(f"Token: {token}") # Bad! + +# Use environment variables for credentials +import os +email = os.environ.get("INVOKEAI_EMAIL") +password = os.environ.get("INVOKEAI_PASSWORD") +``` + +### Input Validation + +Always validate user input: + +```python +import re + +def validate_email(email): + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + +def validate_password(password): + """Check password meets requirements.""" + if len(password) < 8: + return False, "Password must be at least 8 characters" + if not any(c.isupper() for c in password): + return False, "Password must contain uppercase letters" + if not any(c.islower() for c in password): + return False, "Password must contain lowercase letters" + if not any(c.isdigit() for c in password): + return False, "Password must contain numbers" + return True, "" +``` + +## Troubleshooting + +### Common Issues + +**Issue: "Invalid authentication credentials"** + +- Token expired — re-authenticate +- Token malformed — check token string +- Token signature invalid — check secret key hasn't changed + +**Issue: "Admin privileges required"** + +- User is not an administrator +- Use admin account for this operation + +**Issue: Token not being sent** + +- Check `Authorization` header is present +- Verify `Bearer` prefix is included +- Check token isn't truncated + +**Issue: CORS errors** + +Configure CORS in InvokeAI: + +```yaml +# invokeai.yaml +cors_origins: + - "http://localhost:3000" + - "https://myapp.example.com" +``` + +## Additional Resources + +- [User Guide](/features/multi-user/user-guide/) — For end users +- [Administrator Guide](/features/multi-user/admin-guide/) — For administrators +- [Multi-User Specification](/features/multi-user/specification/) — Technical details +- [GitHub Repository](https://github.com/invoke-ai/InvokeAI) — Source code + +--- + +**Questions?** Visit the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy) or check the [FAQ](/troubleshooting/faq/). diff --git a/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-1.png b/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-1.png new file mode 100644 index 0000000000000000000000000000000000000000..706039d50cbc205d867862ddba0b30282c4f6c58 GIT binary patch literal 12975 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

{W2@H@Tu?}UG!ePp#KZv5L~Cm_;9 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 literal 0 HcmV?d00001 diff --git a/docs/src/content/docs/features/Multi-User Mode/assets/user-login-1.png b/docs/src/content/docs/features/Multi-User Mode/assets/user-login-1.png new file mode 100644 index 0000000000000000000000000000000000000000..8c4bec22943150152f0886e18d3ce4c720c0d7b9 GIT binary patch literal 17252 zcmcG0WmH^Ex8^|-2p-%C0YY#B!5xCTyL;pA5`qMGm&P@?y9IZ5cXzkx_q+4mnY-?+ z`8Ct)^g7jbY@b?HTb}*w4wjPCM@-`*H7wYIS`rgJc~H#W9*Ftc$ygK6glfKPz<4*@0D zw38J#Eew}ezVo|nTzHhA4|-@ja{R&-a+vP~U_(N5r$IBbCB)k0Dn}}F+S+%TZP%)* zE~VC`bvN_wjz{umze-i*Is8LD7ykD9iWcyL@nb)~p8d-!{0FG6EJBG#Z{8;QWEwEi zcKjF%Mwk3NTn&)r1U^{3=(C^jY6O-4fNxb_diPP5AQm~iYZH0wE}(nf-hLuNcc&r%sS2TB!pXNzEx?G1;=C&yPhAO6hlfh zP35iMal6%V^`NWlCU*+Nk~sqsKot3fqmjc@N4CO<`;p0}eE#5Q4B$KqY4C1+tAr_6?zYc2Bopu!NZbjk`@16fV48H;q9NKNgs2EQA#!K6Dvxm`*3X38(2+k8yf z03w+%JE4r%AdAmOA+IClrS(;5b{iDZX*#s=LUoM-f%$`28N-(M4BGtgPGlk<3d~!y zg7Q4(Co|s(!aKn&_Tp^L@#+1X$P`{d80PkuV>7_Cd8`cVEm0rOWepMtLOT?ZqEze7 z`?E{!0eO~J5-1TLxu+^+1)tQvDK??l@=tFG;}M$RO2ojIk_l)bKB6y#g8fL-k>&UW z;G$WYKw`l{0`oFK)0*Eiu83hA{&MwGmlegc_f4uD)I@@XPv4aDEIi-QmP}Cp$3f4h z+K7R()Q$!rh`e9ON(5>nj9btwoZz!g#h2^}5{wP-Ac70g`Qw(v-#{d4O=?RNe<4GH z$X=gsMc<>AWCKs?F!ijR%6N_@XwtDg4x997LWtC=Dsl;vT_BBj(1ocT$!E`-6Xv$^ zPH^8P^QM2&zCv*B=Y40Wp{mF^l%A`eFIlVWi}5!TB0+8Gl6Tn?iQXIaM0M{Dwn;d4 z)1xv;iLFOHLy&8v6z)GUlN~%?_!B0}C#YBG>f#}b=}$M6R^~iFRyW7RUY=&z6p^hO z>kf;F#_%TuAX6jF^-wA>_m@ITEW)zDedmyib<#d80BqhnUDfAK9in(640`OUYVJKn z1-^^9pT{C7AC$5@;Cf3fWp14Zfa?(}$=M95zmb#JW=HKi<3^MESmneAXzYeNmjRuE zTLGB5`xt~L7=%2AGJ|E(ODLwZFY+Na8NA+8HXCxh{Z%N$l58aiM5&kf9Ci|4-vJ%l zL$RYSYLhRk^44U6tzuggUF-7RGDY*B%$jG(LVvL_nL|+_XO2R218bba#uc;Akf!GWqZlgudNraaZs@yGmt zME7w;n$zX}o~G7eNk@+rtHO3_7LT~;1p&2Gr;2N6KOo9F*1+WtQZ4}Sd(vB9GdlCb5G@eOL2h$T<10(5hhPjH{RnkY$Xj-#jTrAuJ-JIyRD zXv0-7HuG}#$WLeu6}2d^7Y*sFrG)cTr7J6oo~zi3z;t$vB$du7vzIHt|1RwEDE(8H zbwh+foFhBJ5_~FoDH2JE0sYTp=4H`Olr z)BX}A`G^R2w_5+^Hcs`Jz(A4rj4o0eS6``~f+|aN{CY-pt7XGd)*nlu@Knslu=D&wVzJ2>d&h60P5!ixSH-BFMUSRg_tvi;5nsqw zo84@?^I^>C@|kyd>_!m0+@@phOH)w~3?jDSTIxMVy3i=Q=P`R+u8A_Rf2e zn-vI%6EETMHx%xwY2QxMr9_yf`iKBueDueNp-=4Gg7Q|#2d@Nx; z7k$IYSjNlg*(QHorZ8d^(aZD6SBDd8&*28{QX3r;NOzI?p)eAq?{cJ0vepD~S#7+N zDlQBLU3N{Gp&!b*M~Cjf&HYjMH?q5|<`3C&Q*>V%+4;)(4W9AMg9|G@@zsl|AOeG;A?eeazYeEC%=SU+H?A8l=SEu?h)LwZtL_mS#w#^bABEf>Sg9R9t)#w#=iMRql6 z51m?#d!qg&T&xebbSg8fFDTwclCLdvQ?k3C9?lc-fw`-_cH**1od{yP>P7K2WTkFpQ`w#!IW)-!BzdZh zUv}>|RS-V$)?0PUQn~do*~tvPmU1!RNp!h5fhZ_6`6<2I-R64Ix|q~;4l75N#xU_Y zF`gQdB$^VUAsQZehU}up%8qL^cSqtc2USqL^}KZwrpXb}?%SaDaVd5Z)id2g8L!Gp z;KR&2ScvM`(7p@q*rwDODwqL0&nh+^-K##EzVWf0wIwNk{b>C6EPTmv_`GwMg~^dW z7Q|lh4$4854zY}j+{I?%nPTKvrCw;mD_Y+}BmsP(_GWDoJ3CvRLJ;=# z%+AZ^qqZZ%()do_k^YFsKu)wLgRGGMKvg-Obe@S3-kklljV7q$6HaVxE(#T+Vw%%| z%*p^MPaqt7EC6^$u9F64O3Kh?LiwV*yL0TLYT5EgE3I`)@U>)ondC!rcjf%f+bz?1 zHQQED4#r7asMwLp(6@;>1F@rq{TRH3$0tJ1e`1*LZqo8mxhHOWY=?VYj{;1E#3!97 z^at(BOz^Wuxh!Etv^}QB$iESL^iXRvo5^oxH2me^=X8ZdLPo;heY=(8t8YT}oTPJK z*xT9UI~k2hKD48Bi>uv`tra9MV+@gGp`(jo99NJIl4tCF&?sT#28qH&q!bzBa~p+8 zyiWERE5>!mnZ{#=ga?`6HmY5xLGgY7_%9z=Eg9VsI5RuK$jPWeQBbk~-)ngHr5!nI z-W|MsMI6xZO0MIIFN^uylm!!?bKwQdF~=6_RsHbVf*#>WM7G?N zIbGAyQ6G>4$-_2iV-q{#b_&vmWVu|>=HoQ?|FD!4Jjwz5?&0Z}o~)Fs+uX8s@5~GP z8If1BfsSb=sb)b$JjTi4?V75nuMl}!1D_hx<3EV(=lG9`w~)FI$PcS{(>+gIVXrpf zxI7x8B9cdvL*c@mO}yUxlGdrN&j4G2Ebcl!jltlttz`~vHBq4e&XdU^ZnE0V+>7yW zK-m4@XahLR#JuRBYue$$rS}2$=*zU5>rGbX{NI2OB(ef5pb6C@MD_wW>ngMK#MOrTyfR-qRKqs0ardE%_FK37Cy_brEVWf9ye zPbqY}WmebGYP)3jAUWa_;*T3Ct-#g|{nk74N+$BDELCS>naj0)cRR%rs_$#>V&?$a z?%?rBh^8yz#UXRc?a)v{dZ;Azp}H(;8^0-u@VOWFZt#jnB=UOS-_M0kB;juS9N_`# z%)bH8{qbvJMWy%ScBaGCkMt*J<)PCrd7(G5MC)heHcgXw;s+FupC@C2+#aoYC&}!G zJ08DTw!Jlc3)KBh-d;E`P5Ss)$5$G*R0<|Vs{0v=Eqy)tg2h<%8S)BRo;(Cd;jt+6 zI>ds5=gpJ>ykswn?5?M*pa0e$%)`5j2j+HDYa51Q$ zfZ!w9R&sW-88rH4>70r2p#vk3POa9g@U=VhGv$Rz!5TXM?I(4<+>SeBRL3c-O4=7t zzP#Ch{iO9FoS#@CstSDs9zPVY!{sy+vi}%SZ86}IdL1z^L{3qawzWpW=$IzoZR*9; zDdZ9#I&sgycbL=TAjI<(9T01CxgHE(JA2sR+hfq?Y`1WWf{8*|&3U;ID8MGtVc{UB z9lUS-x zacI-;iZ3JNI$a)T1FCwC#Ak=hc&j7mn|W zwy%BFk5}u2Rp&!d0Z4QSKVv7RhhmoI9E@}Iq|9E1jpS7pyL(Au!)W(F`Lv)gYclu-Wx-TQ(xw^~`cp@5L?3^tiybRXlvz}8B^9Oi zFqZDB*VmFKM;2x`)zJbLB*(TAQyXI6lu`B?HxEGvW|O2Yhb1=Rv4arR;(Uklyls|y zV=<-}dwgS9#i$Cep~~ak_uq}xo9mC8rzgFBB(*qjxlj=7j@^&ExMoL|ZgBrTWFx2A zO2W`xske8H(){^M&zw{m|Zs#{OGvkk?V%X11lHj!L$1glM7COq8 zem3jrZKW*R#gsUtKh-yiX&D|0a`{W=>SMkfY{K`9Y&3UQrioq8!^>0t=QL6y!Oe0d z#VR;Y#D;Pv^)-6r_o$Ux^@eTn1k(4AvAt*jz( z^Oyiz9zx%cVfNfeCL!B0=a~EZBjwq%<3ZEOo<07syFNKyL4B!OjrE%}#xF_auRkvM zjwQce-#v_K+-8vU%%%rnAYQCbhMl1l5Qr9@l{47i+<)1=vVX0Pb2H?Ec?y$*s|i z5xS4==E9Lz^MrNQ9r2JD89w1+okjrn@t^{K&aDvY<5@V->#gQ0j3q_Ttj&Bm7)loT zchWZJEJOr!{RhAw1)zv27{j6-d-$<&k}edAn4(l*TN zv~D2j%v018$BO+;K2Ey+<^VCeT*u2w5uv8%vTEv&70GjY=( zA=#)ncgqbpRc2dh9m&;SM_Dr_0&SklycU1UW~B4uhkp+8jNi???^prl7LmiXl&Z&|vW&rx|BL5*iv(9;AO^;{G$0 z+`AFu`I8KSQ4Nz$XTitRtRoU~XjR=N_VPQwCV!8XR9>a zVLryyYA0eLRopBR#vD(g68bl>XogsrjvTElnU7}*$jAr+XrUB$XKM^p9}|bLuxhFf z=(V4*yK^m2RQAz8M2V#bOPY*ZzwWxbyQQsdcLw8yf_%IcJ}Va!)P8EdrVE4&fV*%6 zbV&!$&i85;pg+L2W=TUw_YM#h?UUuxMgcbd_0jcdW#J(!QPOy63JG~}gwIml_*xMX zOq>(ixYGPqs=0r3*O1Ncc{ryrkxzA#MFmuZe-X z&gQU-1gKSQZ@FFZPSc^Hk`SStvjX$;^ZN8A@$zewMM`Ett~KP!BRQS>vfJ%+KQ*pa4uv*2+q8KDP@J!2K49>h%fT{^KMx zurxT>v=JU$t5G^zX4)4QxJB5uzJ;125)j}&`?6=DZO^8g@BJX8-VivJ#5_^^rGB3J zIj2W)x%hNP41{DMjxrRo*TRw8E^KaInV|Rmq*c@tb+9g=R_nDRYQxK-2fb;ed>hNz zZOV@5mkC2UkC8WfNzugp#5QR~vw)TE_BO`lkzBt2JIbQkolWvujF3^NDgH7ZMF!kVkb zc=FG#kFKzh6=lOsG({}(X%UNgMC^W0ozb0bIxlGfaSGJbO^L}Fwse{xgFo#=ahHC7 z9IdUQ1-It2`h;ybxh!;CUGGD27|OIIjjI=czmVqZ!3F&Es3+8( zHB|`B_GGd41BO9Ycel^uLFRxPWqR>dtV|BIPntW0xWe0O9r1U!*BTw!7>C9-8;+8U zd|inWTSOw3;aX8?yV=HS)l*fouY2{~-7sG1RvekGDc-L?E`QmxQaC!foJIFFDo<=v zO<#{CFN~5wukKY6Vn(X094;rp_$re`qF1e>+QHbIzJS(vR@Z;qZTZ37Rue`!ctl6=GS$!gfD*S%4W2j%b|2&y#FB+aeZgME#_eJ7cAcIo{#D8d2bFcnti-O z;p%KOlu?85I&MzVcBY2k*}7YbG;lv;a8Czg_jGyPk1ZpR8X|s?^-9BE z92n-r7^SVba32);Q4*r#YAM@q6%EAx#ds9%{qczE>}Ps+_C-fR3}fKu5DB;Yaf99( zFLqT+*mU)tB3Q)^mXySs^AD5cA@}f-NJ_jdYoGh{yB?s1e9HjuFTwBe?+_lLt~CyhwntBwyKG+R4~p2Y)+*m@bQ6ll z3SB5YRKC2PD<99^`{_hi_n~gv{eg=NCj2AzV*S$VlSYe}>2LrcjYr$&W7hnh$}ju*NzK-h8-@(7D;!+kYt0)^37^5# zmAqts@gzK&GOe3Zu~&Z7#&a?3)^%L@%ZlAgL?)dWDDr3zSwVlB_nvMMe`Tcw0D7>l zo733a6ZLEG)qFtoa_$-H>rHbmXiR?V{rS?3hhVyI&m;=|cX$8+5pltt5G8fQBrz^- zduL~7cNe-N6laHmg5o$3au7-nVo7LKRDIy|_vQ}N(samhO7V+4^y|aC)A+Kg7kE(g2I3TIU1SO zwpYZ3qqA;8bn8_a6~CIH$1YmJ7ca|bTkyGEDe@ZEeVEDmM% zX^qw`f~~Oiwt#P6pNw?#s|X-#DB|->@4HHW6=1}{l&z{NF>OETVNgNB>vv0vvMp#( zfMLU!3_^yV<;haWaeb^PtWqg!05g2p!NlZMR4KZ&4=K_9Q_5akz;Us*4OF=L@$%3V zH~#Xt*{;AGNnC9zf8$@x@%QKIK|tQtzPED-kiP!c1M}VE48mzJw6rvq;fFt$DkWxN3Ik??&TCYOv!EvWU7(|Dv3JyN|CvPyS#prM zbiT^tLt^zR0gHw3r}3UUYw~p2A)kYK3uW&v1~yoEL%x?IwVvv10#%F{;zC#FXI>#} zOMI*O!s}pb?-&tfms|JiLrLXb9%XoFt@uV9>kx_fKi4%{;7`bfX30N{PmH=TmTZ-b z?E~He{6$gXSRyZhgO3J>-U^_@1lx4zg0W|C7Ic6TwW^veKMLK(cNu$ zu_^radexl?AQ3ybE;Tgz!ys)kTQ?!c;DzW+WeDv9uj*>miA)IwAeEQYtsHB}!u6pV_Zi zH6ezkBZfZ}V6-NCd!chn+oU44#xFj42^Sc&5ByQOZ298Dls>n4i*Iaf{8A0fY3L!x zD_=^yD~KeDM6{;>`T}iARFtSIdS^$>enNacV0~jFIXZg6h(`d;>4rT(u+xC0FwfQ1 z74TJ~op64bNl0Il?c1EcK}3lRi3ovgVF>tUYoLbW@6=ef?(9u>B<#mlHl%wasaFLG z)bX?#;qUmZ>~>Ei?&LoTd0cdKahTnLXNRxrNr^;9bT7=qB9U?8Y7z1ziWKlq3^r}! zV{o*}Gxhi=+iY#m8C8jKfa7P=(nq5vFd2^0%nU-FLeX?Ti7Brf3hqrrBw1q55wm;e z8n4G(iRdpfDi&&rp~*o(3OY}dor5joKB$c9X2dlWJ*%l9_5K(>nnXS~wc#6Ltlfb< z`v;w$yf5xG8&fhl3LxL^ck~eb`HSr3UMseiE}?dA9@Zx&Yo~sOr{2Y~ z=$P6~keG{%-L6$KqwBPuM{FBu^1rzNDCb(g$h_DClJ5qa^~mv^$io96ri~Y9XlklR z{q(yjB=yJV=;Otkgu;GVC8K+`%m&=iw23rc9<9%(n+7hzM>AamdsYQa;F2ovs`(B} zafY!@;|?c>?Qsp%q@Sqqm@<#X>MM&}2s4HQPyV0BwNBHvm%Ca+(FVglx`#D4Pnn(W zX^cJ+C8j}w;N+Kl*(VQ7)VL~-L>Y<@Oo_4yda68R5Yymf>cxj&IIT}Xd`~#bVq-K> z9#H%bzy40Yp2@yVweP~j;ZUT7jg`O}8&Ce!{cPAv|GFn=;rhCMc`)rqUGa@qX{MU8 zM!H?6C6^0F<~oIYV~bB(WdOmRnEyP;hoI=*!6yMKuI9zvnq7ky?<@JS+F_0H>+iEKAU9V^B2Q7ITC8SB05MqXeAQ&qYJFU0%&OZVa@2|Z>F zqn@7L9^cuPJeH_Sy61dC=|^awBCla6!5fp9*n9KuKuwL(ma=3b{S2t~<>`)rp1!TY z^Wl7{zLnKxHjHHKwU2Ke>{GVdqTXtW_f%3oe>B^Q!PBaAnJYA?c+DO;Vge;U_echD z(r@rhP{<4+ijdVtiV5^oRJ=okI)4U6RmwGlw|9#89{l@9P5OGBy^^`^KozGYJ0+cq!rjx<#h5p5REKcs zT)X$m6blOvwB;WZzsP2wvG8(&JagMaZy$awQ#K(^e4n{o%=p#U&SxxP)DcBYf#bvL zAgwjEDDV;FTUP9?Lrwh6;BS9483~U&dE(H5C3a}f;X-VAbJyG9Ov!br<|uk_cPyp9 zQgJf7JhM6a^sIef1jXPWur4~*sqMysTc;y^)G|vWxGUf?_hscos!Y4t`PFmf+3)<^ z*6$O&ZW}ivgN>QVu5S=3WR`CS&rU@54{k=_LTS(ej&?^_M~?YU2I(lI)pB`q*f^mX zze7aAa5pwLHxCaF*VpyT&65)oDWk<6@6O&5^72ZQ2I;=aG3b^hbK@14WZCFONvN|~ z1G5LKXplg(kWQni`7xQf!lS+}lmB7K#{WlGGNXXo)_b$>T0TCT7lwqPgYPZ z955b88R7<#OUqmd!?UOe(9oFeV2JO8|AB)wgW^a0Ya8qe^u@ef^!;#RvAg;LRMcC~ zyZ}7zj8ciKBWRlC`3G$a_$5y8PAH{P8pR6EQuDQWH;=p8<~JsCRCviXar&U}b!rMldK#~lJO~2?_%lW7KTHBaKKh^aC;y)}R~;H?Y#VY7%UAKk zRdOa;POCm$b5^R+Vuyb9Qt@+9Wn&|xH$Pkd{?y%=m6bzFN2lvzhvuFR0)b*;A#1e$ z%q_WrraQvQ*m&h!QorV(=)53vNJ8({zx8P}nTvrMgcS4(OtXI%(Y*ly1lAUNA18A9 zE(b4BI86RQjD!LCJ1ad1)`NDxqNgpLGZ7dV;SPE6ePncePKlg@Nt>1K+#^T)@b?hD z4_`=gLTuPatiuo<<49s*?C-MQsir{NwTg$Z!R=&ZF z)oATR1Q3#BDF-b9+?mzswXV(Y5ElH#N0tOM(rKtBgFw|%_9vZ6X!f`gm-}XvgdkKM z>N0b$^0|N-`w-+ydy_a;@>=H_E=*(Ep^~`vQw380#hm{Y7iU9Ds>XN2*?71wP~z~_ z6QJW`vCnIFz|2$+OTM)_?x}|hGraw-%z{ZfrcJucKHiobNLIj_;d2#i05@mCDFu_5 zkY`B7vi}%KX8vJkzLr%Jk|99iiEFIu>U=gZoYE%mz>AwNv2ZnYgFmPLQQ(!hl@^q- zGp9^Sx16&ldsjZduzu~dx6D^_7t-Qmc3nv7MMp!tO21o4kdRn}ZPQOh;F@BTCJ3^F1vU3#V+XJdryUlH%Sn$H5sQ<&F#v(FGDE_8gY^vj_ z#4TcTr+`hf9JZHGjvqH21slD#82x4g9MaWE9vuz(6rliEhlicV{ic$#Gh3C!gSfBE zMORbWO+Gq%?wsl@!oFlcy3O&VZII`ACAix?x(R3SVCOGYlQ-3kzEyi+G6po!;F(rq zn$X?laksfEikM`Ov3D73e7u5Nqt<%kWI)yFHM8QC?ORtNlnRVrzPFDMpG8aldbQ$XlkdHV zz5QbPHO+uJ`u@q;iP;AT;DeP1ZjN!@8m%TKK6NhMBv6f+I(>GgdWe90#@Is`U%K@FEA?kHXZW=UJN2DU0oF zWiki^b$F0oqIyaFkDvJL|Ft{=U~#1Ta1PCX^Xxo{wU>0BIe_Q~fWZ0bJPteZ@|QcQ zC|f}yvP9^}#d~h5GSBvk3KaBl)9wgqPn>sEv_6hPhn&Ne9)#A14ziVvkkFAMS6sNeIxtmLe7@I@#`K5r z)b=POf45njLf!P9)*S(o%v4+CxUCtV9-l!Da#yZZ4$~(b|IM<=CTa{YWMVNoO5f?9juNslOKLT0()8>%0F6+T__!sE~mxGZ3zHC zsJ|Vf9Nh}VeyXVAN%(m`4d(Ooh*Qt+A0wfDeUaoMZaE56${m{sX*>(l9i*d#g+RqV z#^Y+x&ji%in5q&0Q2Jh)*+Mz|Kr*&hPoIZVq(loCV~rI0QsD&e%L|;gZZ8zY>?s^E zg;t*OB>7z%;A66IxPVdcSZKK$o>dyXGQo1n_7nv*@6*BubI@>t4@hC0mgbQ!>frX1 z2&ij*<((~qC-+{bZIhqkM(`9_*_JEr?&q(n<^zW;MgMR{RTsyi{+7GDUajjg)Z+XE9qwBY5Y zwCe^0Kxqt&sRT?*AY>>PQHNZCi};mQ)M!l7)gtvB zljClQvO0}qb%;rfq~mSh>oc)!B7&nD$zX3~G)gC46l?HnHMSPVj#v@~hS?FGh}j+Q zof4t5*oOY#k;F-4IQD0S5bsqk(023e!vlVL8!r$Xx}ED^34{MFcku6KdPI-G%ydg9 zn~gx>(9HC7g_<@#Z;~UV`!mG!>9|m#x&I=kq^wl?akS(__}n%pQy{0r=Xu}1*t@u} zfY*MB^)d>%OsmoUq0j4SP@)H4x^H>Aj&AKPggK}gI!Kc9_ONuqtoc554b6S3;(wal zYz3xLn-x~8WR5W9u(C`ACJI?ZUC6T@U%I^iPgd_=nUViy3;&Z*3y++?kVjc_2e=rTIa~puWtWv-xof ze93i8K*NPXg_PE8{}8qz*uUIpbW+LPBY8TuD_Z)%>x|UCp`iY}mS8QFP}?v!t1Pp} ztL=532mY8fI@c&ZCphdjZ{MS}bQqO9@`VTh#yR@x-aKVdax=%)eO>a)Ua~2x?LD%$ zk)@K@V0WHQpYdQ0Lb+JUSNo|w1IFd2@c3-h5`REx-X%HWf3uh{6QS*Kh-j)Dm2TQK zkkK1>jcrfYD0z{j*0+a&$-mYY0fi(%`3~XOh_^3(Hy<81G{5-lxs=@d*}J z;bqBeDXzM=ib30Jbm>|Zhv4JxZD@Hbb+{atqxEF+-!FO(XavMT>%Xx~-#he6yIMKgh91!N_jkCef zC6l7e^B%uv1d>=Hm0jv?G*!`Y$2XzEW-gU-UWK*Gyg>TZ3pc&nB3c30AHFcLiMD!o zSjO|m$%cXOR2p%vo=dynDkBgOLvnuFg4&0A=d{P%!(omSamp8CpkXJ09d6-4L0;z%`yXVlq(e)!f$L31)}bkwhb8;3%}R;q>%=A6K%{n=1} zSoy(GdouEqN*R|H|2rw&mh%;U64D-}-%O*YD^+}X2U8GIdb$1&gC@!y>u$P2C1SVx zm>JlZY_US+aejoHgfeaB1l{YS)o{(bF9}VPR+5G-1;b(I*_o})FhJ^MNb)OLfN_y5zsr@S52NlVJh9*k{t8v&nE<25iSNAO*^uMp#t+&33JegIi4d>&sF* z#nM<-2K5^2J0hn2R`VQfF<_F;Wag0~3mbLRX07IATzb*8cadePl`6+@JlVrkw3Wm~ zUT|%tSx;-aM*5(pw2ZszuMij8X;YdhP9?3gTaqNfV2+U6U#V-dv-F7exox(dtE8$g z&I@L*-PK%d)_m#G!o%>pN7)gV;G2iTA{k-Pxsl}RbsN)dpG@B8jX{*{TW;Ick&z|tLlB#nDp6|BVaZ2!O zosUisYHul=$sbm~F;hMWzLsgC+Y+<&lF(ZPFGYRQ2R@M&I8Qx(+gN>0JpelT?PUlh z`?0?*^hLf8yF_Vldpy4JOxCD*+dlVo`?Yd!M9fve(`?H84{f_u*OEiA5YQ{w?##a- zBlAKv-a!sV+BCM(#c;{?h4Q`ERCvu-&j~umzyN@Kh(8ty9%9Bj1Bo9N3CcWBy47N3jSv7;z0EW~6iIo8Jr zjnC&TW#q}h3}|p|?ROu#Op2xt?n_n~gaH5d0WB1 z06&Pg(dh-Zkc$~S%7K8zlmZo%7BLp4wsWA5s*Wd~Jh|Rc@e_`zY%0z}FACA&?&%u+ zot%=aMeY9P!c{QG_igKjRC3r4&VRE}W)H;oIt<&5WnUkEKmpK_w|#AOKm|?nrEqCJ zUGtS_IS;41)e4OIXzLZWFzR}Vz;{6SBNA7Vqo3Pzm)*=vTo*?A55w`IWeO{C*heHnTsIb zE3{xHuyO_LysW16Xa1y*qg#^}`^G{3;Zw59D{++dlU*83mdra~V(V)1v)Ce{)($yJHGlTSIi=Z{tp zD8USrqeNg^7atTlZ?)|6z{|9n67=^Fnv`q>Tfb3QlXRcu-YJAbK4_IGYNI2Q1?@ZH z;xQDUupLTMoGbh}C3ib=3d8B;sg775LM9FibfzRyB&Cn}i6Lsw#-W}69+)&Y%S_c0 zsgHvTs}bu8yE(VZXQDO(K0Oe(d1Y?nh)XJS5M>PQy9gZ%@3F0ZUx_l32;DyB2mcgg z68#JKmWujPg5#i~Jje9CXCFl8dWeS=fhc;#-lM1GX~)yr?XHXH98oH5A|rFY?S4nf zv_2P%7>Y)!AO{7MvrC-@<0*fAV!zPOgmL^tg;Sd4I6r3aOPz0lqu07+h}Kz3M~L4w ze%!k@a*-jCKv91>{qs%Sj`EV>?2U!Vd15g>Vs9#A79mhd7uHTueIAxZKu62bd{q_}O+mu*Ds!A&TLMd2zDGsd(mR^^O|HgA`t|jB z7Ih&cBy9^WR`ib=aS11pk0`XvPi7NQ4N9APM@hu)I4S+hpIc{tz!!Tv=MMH+DI|>E zD!!YSfrt;^;WfQkLAKX%olzHFf;!CUzK#NqJ#bSABbv2g4xB=u4J=-kd2Igonfc~a z(kRH`c;27T z#3^i{i>-fAgv(;xU(O}NjMvB*$)iNbd%ufXwVJH0o-fC?m|5CqKpxBoj=3#ZX zuBkA2YdJrxQw!RF%)?CK;3qcAwyy@6Rzl7o^=4-{fW_;s>)$SFM z8TPMB)XLRsZKYv>@y5M6pGG$M$wfqpfE>yn*YwTL<e?ZL|2`V`%7FtiW?cE5O>8ig-nSXE!m#;L&`FP$O zzBJBr3KQk*+YzUZKxbQz5<>B#(VE%HLr)Blp>qV|8i@sMd%)g5g}bexo*qm5y?8%e ztJBSbzV8z3NYL*2B51>THNUIbiZX@j&c6ytM_JK9NL%3#b#oaSt2wmrTukCU|xSJCk_w1ciNPB%(Z5*6GN;* zn)+Uu>fkZJakFfO8H-Jgm<;?WL1|wh}AhXe9FvC!IxZE$F#c^ zfpES?6chSiJ@|Ey6Z8q5+Hqtwd?7+Cyta#7j{Rnctj%DJql0(Wz;qwnKuHvX5SqnWc+c=(~+ebp$P|rHeRC?%{P%QIs8jN_t5h-XIA}iE6(beZ8a%G!tTyW1$;%t9TnroEXjMNz|ebwQcQt zpzcU5%VOm*a0@ML^KvgTBEJ|)jkR^-A%3%!t&t@>;%oWi&CuW;Qfl#x(r&_Ala9`y z>x+JIp0EW)(MUY;Yg$%mqos&;jE3{QiPMeC%UfUK%C~ZiWIjR<;gPM|-S+0(0y49V zw2v<@>BBu|%WHKyG0`PTDT@u48Vfs0ENV+qPZKUv1D&bf7w!cvQ{U93$IOXSy>Axc z7+nXHBmt+>cnPYh!0i5o*SGVBM;!1-yX%jqD&L~P78S*nQ=H%4Y>-iwocAhdDt%cn zd@bl&&mvdViS-g`IqE)U;7v^U9zp43ZXJ)AXscYsMp_`DW6<>L2cTA=w)mwS^+iYT2&=YtaKzm|_2##_40B${ zv&H1S&EO>iCYTV^YG&}OJp)vP54hf1dMU;kn~&{4eK+ff(KEBH^6jx+2CNbN-obkF z=Op_3FcqrfAB`q=0KQ^+^8=;44yg ze}WXQZN5L^TdF!uo3O>caZjThq&S57mi3IorX!13OP*NtuHxJ&jM!qpYW}(g?*Q$i zmiPDC%YBkl)5scy-9d^DlUBhi%&Z6qfT6*E+4Z(D^CYEy$nvn&9FnbJy_V%n-03%g zVdX#jg*DDE+T;&8y#C;<$eo3q~HI_$GR7Aled($eWK zpT@goQt>v0hb3(QpkZsSppcp9)FQkSEGPIvbjXfv?To>;DDAn@!(SMj53chME=3dE z51*B=)o+IWugW%sTsz2l$~@kxkrWkDw_5P4JF9m6aG6sZq8ZcOJt0ifnfMKUN9W0b za^g-9q_*~BX-u>(&t&{qe_d78`*C~*2sPK4=N*x~Bl1f&D_^YDO03lpf5pY2sK57F zjYK}{o65t0mBhtX1n0l989hujdLID4PhpGIuoLf?gW@;)s)Pu*p48C<3*R-((u`t@ zz$YVP{0yv?`cLDaHg7KspPc%ovKM5+0V&kw8xc+(n5EhMR~ax*l@tZhdx&HN=0Zjr zzhVEK1WGRJ>I2=QLFlh6UE4zcks?COq`#?9fYcw){(k>9r}yq3n&s{o`$2`uKYc04 z-&7=NJ_;5#e?$1*bIGzVIpYAYv;{?)FBzI9*`v6M^b;z zUn(WaMR7CAmpGG?)J=kkV*b2~hXsSl5}Yr0t>};C^s^o4;sI;7!Kb@=VB`RvQ%M=F!+Bzg7BS%NQ1@fFO$RV9{VpcxUl#j5kdq=of&F zuP%;G0;TUe&=NC^AsisHD2MT{$6+W>2pW zpNi$UGEdxYPnVaM^c~oT1A>y%J4y1m_|!fcUdqvo(URdg)$&m(B1!iH_faF=Mn4J- zYZH`D_Ip5;ize=h+2)*R$giqvLUYH}_L%}PctP(&KYf=Yac(-P$&?k$F`Z>yRQCDv zSTcm)-j#?_kT6PnOZ53#gqa0p{FX(8k0psi6c^-#>}ftHR`{Tm{eAxAGjOA?>H=HZrYO@MEkA-HrE;2?mj<#>#3smtcQPDN8cA8HcsNO&p8Xw|ifd37 zV7iOTP-xS0*d?P|I>iPGKQavrEGrr$s)=(6*Qu^6jSh6;_Fsi1QVzAKBVFadSCn7;$ZO0K()`avChx*XSBHNeN)pHAWkRUHy6j09wmCqbDYsS&tAh sPi^R?tAE_MVrTUKgsF;No0_lxA6K{}FtK|a_W%F@07*qoM6N<$f|S<{{r~^~ literal 0 HcmV?d00001 diff --git a/docs/src/content/docs/features/Multi-User Mode/specification.mdx b/docs/src/content/docs/features/Multi-User Mode/specification.mdx new file mode 100644 index 00000000000..1e137464495 --- /dev/null +++ b/docs/src/content/docs/features/Multi-User Mode/specification.mdx @@ -0,0 +1,959 @@ +--- +title: Multi-User Specification +description: Detailed technical specification for InvokeAI's multi-user support. +sidebar: + order: 6 +--- + +## 1. Executive Summary + +This document provides a comprehensive specification for adding multi-user support to InvokeAI. The feature will enable a single InvokeAI instance to support multiple isolated users, each with their own generation settings, image boards, and workflows, while maintaining administrative controls for model management and system configuration. + +## 2. Overview + +### 2.1 Goals + +- Enable multiple users to share a single InvokeAI instance +- Provide user isolation for personal content (boards, images, workflows, settings) +- Maintain centralized model management by administrators +- Support shared boards for collaboration +- Provide secure authentication and authorization +- Minimize impact on existing single-user installations + +### 2.2 Non-Goals + +- Real-time collaboration features (multiple users editing same workflow simultaneously) +- Advanced team management features (in initial release) +- Migration of existing multi-user enterprise edition data +- Support for external identity providers (in initial release, can be added later) + +## 3. User Roles and Permissions + +### 3.1 Administrator Role + +**Capabilities:** + +- Full access to all InvokeAI features +- Model management (add, delete, configure models) +- User management (create, edit, delete users) +- View and manage all users' queue sessions +- Access system configuration +- Create and manage shared boards +- Grant/revoke administrative privileges to other users + +**Restrictions:** + +- Cannot delete their own account if they are the last administrator +- Cannot revoke their own admin privileges if they are the last administrator + +### 3.2 Regular User Role + +**Capabilities:** + +- Create, edit, and delete their own image boards +- Upload and manage their own assets +- Use all image generation tools (linear, canvas, upscale, workflow tabs) +- Create, edit, save, and load workflows +- Access public/shared workflows +- View and manage their own queue sessions +- Adjust personal UI preferences (theme, hotkeys, etc.) +- Access shared boards (read/write based on permissions) +- **View model configurations** (read-only access to model manager) +- **View model details, default settings, and metadata** + +**Restrictions:** + +- Cannot add, delete, or edit models +- **Can view but cannot modify model manager settings** (read-only access) +- Cannot reidentify, convert, or update model paths +- Cannot upload or change model thumbnail images +- Cannot save changes to model default settings +- Cannot perform bulk delete operations on models +- Cannot view or modify other users' boards, images, or workflows +- Cannot cancel or modify other users' queue sessions +- Cannot access system configuration +- Cannot manage users or permissions + +### 3.3 Future Role Considerations + +- **Viewer Role**: Read-only access (future enhancement) +- **Team/Group-based Permissions**: Organizational hierarchy (future enhancement) + +## 4. Authentication System + +### 4.1 Authentication Method + +- **Primary Method**: Username and password authentication with secure password hashing +- **Password Hashing**: Use bcrypt or Argon2 for password storage +- **Session Management**: JWT tokens or secure session cookies +- **Token Expiration**: Configurable session timeout (default: 7 days for "remember me", 24 hours otherwise) + +### 4.2 Initial Administrator Setup + +**First-time Launch Flow:** + +1. Application detects no administrator account exists +2. Displays mandatory setup dialog (cannot be skipped) +3. Prompts for: + - Administrator username (email format recommended) + - Administrator display name + - Strong password (minimum requirements enforced) + - Password confirmation +4. Stores hashed credentials in configuration +5. Creates administrator account in database +6. Proceeds to normal login screen + +**Reset Capability:** + +- Administrators can be reset by manually editing the config file +- Requires access to server filesystem (intentional security measure) +- Database maintains user records; config file contains root admin credentials + +### 4.3 Password Requirements + +- Minimum 8 characters +- At least one uppercase letter +- At least one lowercase letter +- At least one number +- At least one special character (optional but recommended) +- Not in common password list + +### 4.4 Login Flow + +1. User navigates to InvokeAI URL +2. If not authenticated, redirect to login page +3. User enters username/email and password +4. Optional "Remember me" checkbox for extended session +5. Backend validates credentials +6. On success: Generate session token, redirect to application +7. On failure: Display error, allow retry with rate limiting (prevent brute force) + +### 4.5 Logout Flow + +- User clicks logout button +- Frontend clears session token +- Backend invalidates session (if using server-side sessions) +- Redirect to login page + +### 4.6 Future Authentication Enhancements + +- OAuth2/OpenID Connect support +- Two-factor authentication (2FA) +- SSO integration +- API key authentication for programmatic access + +## 5. User Management + +### 5.1 User Creation (Administrator) + +**Flow:** + +1. Administrator navigates to user management interface +2. Clicks "Add User" button +3. Enters user information: + - Email address (required, used as username) + - Display name (optional, defaults to email) + - Role (User or Administrator) + - Initial password or "Send invitation email" +4. System validates email uniqueness +5. System creates user account +6. If invitation mode: + - Generate one-time secure token + - Send email with setup link + - Link expires after 7 days +7. If direct password mode: + - Administrator provides initial password + - User must change on first login + +**Invitation Email Flow:** + +1. User receives email with unique link +2. Link contains secure token +3. User clicks link, redirected to setup page +4. User enters desired password +5. Token validated and consumed (single-use) +6. Account activated +7. User redirected to login page + +### 5.2 User Profile Management + +**User Self-Service:** + +- Update display name +- Change password (requires current password) +- Update email address (requires verification) +- Manage UI preferences +- View account creation date and last login + +**Administrator Actions:** + +- Edit user information (name, email) +- Reset user password (generates reset link) +- Toggle administrator privileges +- Assign to groups (future feature) +- Suspend/unsuspend account +- Delete account (with data retention options) + +### 5.3 Password Reset Flow + +**User-Initiated (Future Enhancement):** + +1. User clicks "Forgot Password" on login page +2. Enters email address +3. System sends password reset link (if email exists) +4. User clicks link, enters new password +5. Password updated, user can login + +**Administrator-Initiated:** + +1. Administrator selects user +2. Clicks "Send Password Reset" +3. System generates reset token and link +4. Email sent to user +5. User follows same flow as user-initiated reset + +## 6. Data Model and Database Schema + +### 6.1 New Tables + +#### 6.1.1 users + +```sql +CREATE TABLE users ( + user_id TEXT NOT NULL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + display_name TEXT, + password_hash TEXT NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + last_login_at DATETIME +); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_is_admin ON users(is_admin); +CREATE INDEX idx_users_is_active ON users(is_active); +``` + +#### 6.1.2 user_sessions + +```sql +CREATE TABLE user_sessions ( + session_id TEXT NOT NULL PRIMARY KEY, + user_id TEXT NOT NULL, + token_hash TEXT NOT NULL, + expires_at DATETIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + last_activity_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + user_agent TEXT, + ip_address TEXT, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +); +CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id); +CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at); +CREATE INDEX idx_user_sessions_token_hash ON user_sessions(token_hash); +``` + +#### 6.1.3 user_invitations + +```sql +CREATE TABLE user_invitations ( + invitation_id TEXT NOT NULL PRIMARY KEY, + email TEXT NOT NULL, + token_hash TEXT NOT NULL, + invited_by_user_id TEXT NOT NULL, + expires_at DATETIME NOT NULL, + used_at DATETIME, + created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + FOREIGN KEY (invited_by_user_id) REFERENCES users(user_id) ON DELETE CASCADE +); +CREATE INDEX idx_user_invitations_email ON user_invitations(email); +CREATE INDEX idx_user_invitations_token_hash ON user_invitations(token_hash); +CREATE INDEX idx_user_invitations_expires_at ON user_invitations(expires_at); +``` + +#### 6.1.4 shared_boards + +```sql +CREATE TABLE shared_boards ( + board_id TEXT NOT NULL, + user_id TEXT NOT NULL, + permission TEXT NOT NULL CHECK(permission IN ('read', 'write', 'admin')), + created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + PRIMARY KEY (board_id, user_id), + FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +); +CREATE INDEX idx_shared_boards_user_id ON shared_boards(user_id); +CREATE INDEX idx_shared_boards_board_id ON shared_boards(board_id); +``` + +### 6.2 Modified Tables + +#### 6.2.1 boards + +```sql +-- Add columns: +ALTER TABLE boards ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; +ALTER TABLE boards ADD COLUMN is_shared BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE boards ADD COLUMN created_by_user_id TEXT; + +-- Add foreign key (requires recreation in SQLite): +FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +FOREIGN KEY (created_by_user_id) REFERENCES users(user_id) ON DELETE SET NULL + +-- Add indices: +CREATE INDEX idx_boards_user_id ON boards(user_id); +CREATE INDEX idx_boards_is_shared ON boards(is_shared); +``` + +#### 6.2.2 images + +```sql +-- Add column: +ALTER TABLE images ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; + +-- Add foreign key: +FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE + +-- Add index: +CREATE INDEX idx_images_user_id ON images(user_id); +``` + +#### 6.2.3 workflows + +```sql +-- Add columns: +ALTER TABLE workflows ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; +ALTER TABLE workflows ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE; + +-- Add foreign key: +FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE + +-- Add indices: +CREATE INDEX idx_workflows_user_id ON workflows(user_id); +CREATE INDEX idx_workflows_is_public ON workflows(is_public); +``` + +#### 6.2.4 session_queue + +```sql +-- Add column: +ALTER TABLE session_queue ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; + +-- Add foreign key: +FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE + +-- Add index: +CREATE INDEX idx_session_queue_user_id ON session_queue(user_id); +``` + +#### 6.2.5 style_presets + +```sql +-- Add columns: +ALTER TABLE style_presets ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; +ALTER TABLE style_presets ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE; + +-- Add foreign key: +FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE + +-- Add indices: +CREATE INDEX idx_style_presets_user_id ON style_presets(user_id); +CREATE INDEX idx_style_presets_is_public ON style_presets(is_public); +``` + +### 6.3 Migration Strategy + +1. Create new user tables (users, user_sessions, user_invitations, shared_boards) +2. Create default 'system' user for backward compatibility +3. Update existing data to reference 'system' user +4. Add foreign key constraints +5. Version as database migration (e.g., migration_25.py) + +### 6.4 Migration for Existing Installations + +- Single-user installations: Prompt to create admin account on first launch after update +- Existing data migration: Administrator can specify an arbitrary user account to hold legacy data (can be the admin account or a separate user) +- System provides UI during migration to choose destination user for existing data + +## 7. API Endpoints + +### 7.1 Authentication Endpoints + +#### POST /api/v1/auth/setup + +- Initialize first administrator account +- Only works if no admin exists +- Body: `{ email, display_name, password }` +- Response: `{ success, user }` + +#### POST /api/v1/auth/login + +- Authenticate user +- Body: `{ email, password, remember_me? }` +- Response: `{ token, user, expires_at }` + +#### POST /api/v1/auth/logout + +- Invalidate current session +- Headers: `Authorization: Bearer ` +- Response: `{ success }` + +#### GET /api/v1/auth/me + +- Get current user information +- Headers: `Authorization: Bearer ` +- Response: `{ user }` + +#### POST /api/v1/auth/change-password + +- Change current user's password +- Body: `{ current_password, new_password }` +- Headers: `Authorization: Bearer ` +- Response: `{ success }` + +### 7.2 User Management Endpoints (Admin Only) + +#### GET /api/v1/users + +- List all users (paginated) +- Query params: `offset`, `limit`, `search`, `role_filter` +- Response: `{ users[], total, offset, limit }` + +#### POST /api/v1/users + +- Create new user +- Body: `{ email, display_name, is_admin, send_invitation?, initial_password? }` +- Response: `{ user, invitation_link? }` + +#### GET `/api/v1/users/{user_id}` + +- Get user details +- Response: `{ user }` + +#### PATCH `/api/v1/users/{user_id}` + +- Update user +- Body: `{ display_name?, is_admin?, is_active? }` +- Response: `{ user }` + +#### DELETE `/api/v1/users/{user_id}` + +- Delete user +- Query params: `delete_data` (true/false) +- Response: `{ success }` + +#### POST `/api/v1/users/{user_id}/reset-password` + +- Send password reset email +- Response: `{ success, reset_link }` + +### 7.3 Shared Boards Endpoints + +#### POST `/api/v1/boards/{board_id}/share` + +- Share board with users +- Body: `{ user_ids[], permission: 'read' | 'write' | 'admin' }` +- Response: `{ success, shared_with[] }` + +#### GET `/api/v1/boards/{board_id}/shares` + +- Get board sharing information +- Response: `{ shares[] }` + +#### DELETE `/api/v1/boards/{board_id}/share/{user_id}` + +- Remove board sharing +- Response: `{ success }` + +### 7.4 Modified Endpoints + +All existing endpoints will be modified to: + +1. Require authentication (except setup/login) +2. Filter data by current user (unless admin viewing all) +3. Enforce permissions (e.g., model management requires admin) +4. Include user context in operations + +Example modifications: + +- `GET /api/v1/boards` → Returns only user's boards + shared boards +- `POST /api/v1/session/queue` → Associates queue item with current user +- `GET /api/v1/queue` → Returns all items for admin, only user's items for regular users + +## 8. Frontend Changes + +### 8.1 New Components + +#### LoginPage + +- Email/password form +- "Remember me" checkbox +- Login button +- Forgot password link (future) +- Branding and welcome message + +#### AdministratorSetup + +- Modal dialog (cannot be dismissed) +- Administrator account creation form +- Password strength indicator +- Terms/welcome message + +#### UserManagementPage (Admin only) + +- User list table +- Add user button +- User actions (edit, delete, reset password) +- Search and filter +- Role toggle + +#### UserProfilePage + +- Display user information +- Change password form +- UI preferences +- Account details + +#### BoardSharingDialog + +- User picker/search +- Permission selector +- Share button +- Current shares list + +### 8.2 Modified Components + +#### App Root + +- Add authentication check +- Redirect to login if not authenticated +- Handle session expiration +- Add global error boundary for auth errors + +#### Navigation/Header + +- Add user menu with logout +- Display current user name +- Admin indicator badge + +#### ModelManagerTab + +- Hide/disable for non-admin users +- Show "Admin only" message + +#### QueuePanel + +- Filter by current user (for non-admin) +- Show all with user indicators (for admin) +- Disable actions on other users' items (for non-admin) + +#### BoardsPanel + +- Show personal boards section +- Show shared boards section +- Add sharing controls to board actions + +### 8.3 State Management + +New Redux slices/zustand stores: + +- `authSlice`: Current user, authentication status, token +- `usersSlice`: User list for admin interface +- `sharingSlice`: Board sharing state + +Updated slices: + +- `boardsSlice`: Include shared boards, ownership info +- `queueSlice`: Include user filtering +- `workflowsSlice`: Include public/private status + +## 9. Configuration + +### 9.1 New Config Options + +Add to `InvokeAIAppConfig`: + +```python +# Authentication +auth_enabled: bool = True # Enable/disable multi-user auth +session_expiry_hours: int = 24 # Default session expiration +session_expiry_hours_remember: int = 168 # "Remember me" expiration (7 days) +password_min_length: int = 8 # Minimum password length +require_strong_passwords: bool = True # Enforce password complexity + +# Session tracking +enable_server_side_sessions: bool = False # Optional server-side session tracking + +# Audit logging +audit_log_auth_events: bool = True # Log authentication events +audit_log_admin_actions: bool = True # Log administrative actions + +# Email (optional - for invitations and password reset) +email_enabled: bool = False +smtp_host: str = "" +smtp_port: int = 587 +smtp_username: str = "" +smtp_password: str = "" +smtp_from_address: str = "" +smtp_from_name: str = "InvokeAI" + +# Initial admin (stored as hash) +admin_email: Optional[str] = None +admin_password_hash: Optional[str] = None +``` + +### 9.2 Backward Compatibility + +- If `auth_enabled = False`, system runs in legacy single-user mode +- All data belongs to implicit "system" user +- No authentication required +- Smooth upgrade path for existing installations + +## 10. Security Considerations + +### 10.1 Password Security + +- Never store passwords in plain text +- Use bcrypt or Argon2id for password hashing +- Implement proper salt generation +- Enforce password complexity requirements +- Implement rate limiting on login attempts +- Consider password breach checking (Have I Been Pwned API) + +### 10.2 Session Security + +- Use cryptographically secure random tokens +- Implement token rotation +- Set appropriate cookie flags (HttpOnly, Secure, SameSite) +- Implement session timeout and renewal +- Invalidate sessions on logout +- Clean up expired sessions periodically + +### 10.3 Authorization + +- Always verify user identity from session token (never trust client) +- Check permissions on every API call +- Implement principle of least privilege +- Validate user ownership of resources before operations +- Implement proper error messages (avoid information leakage) + +### 10.4 Data Isolation + +- Strict separation of user data in database queries +- Prevent SQL injection via parameterized queries +- Validate all user inputs +- Implement proper access control checks +- Audit trail for sensitive operations + +### 10.5 API Security + +- Implement rate limiting on sensitive endpoints +- Use HTTPS in production (enforce via config) +- Implement CSRF protection +- Validate and sanitize all inputs +- Implement proper CORS configuration +- Add security headers (CSP, X-Frame-Options, etc.) + +### 10.6 Deployment Security + +- Document secure deployment practices +- Recommend reverse proxy configuration (nginx, Apache) +- Provide example configurations for HTTPS +- Document firewall requirements +- Recommend network isolation strategies + +## 11. Email Integration (Optional) + +:::note +Email/SMTP configuration is optional. Many administrators will not have ready access to an outgoing SMTP server. When email is not configured, the system provides fallback mechanisms by displaying setup links directly in the admin UI. +::: + +### 11.1 Email Templates + +#### User Invitation + +```text +Subject: You've been invited to InvokeAI + +Hello, + +You've been invited to join InvokeAI by [Administrator Name]. + +Click the link below to set up your account: +[Setup Link] + +This link expires in 7 days. + +--- +InvokeAI +``` + +#### Password Reset + +```text +Subject: Reset your InvokeAI password + +Hello [User Name], + +A password reset was requested for your account. + +Click the link below to reset your password: +[Reset Link] + +This link expires in 24 hours. + +If you didn't request this, please ignore this email. + +--- +InvokeAI +``` + +### 11.2 Email Service + +- Support SMTP configuration +- Use secure connection (TLS) +- Handle email failures gracefully +- Implement email queue for reliability +- Log email activities (without sensitive data) +- Provide fallback for no-email deployments (show links in admin UI) + +## 12. Testing Requirements + +### 12.1 Unit Tests + +- Authentication service (password hashing, validation) +- Authorization checks +- Token generation and validation +- User management operations +- Shared board permissions +- Data isolation queries + +### 12.2 Integration Tests + +- Complete authentication flows +- User creation and invitation +- Password reset flow +- Multi-user data isolation +- Shared board access +- Session management +- Admin operations + +### 12.3 Security Tests + +- SQL injection prevention +- XSS prevention +- CSRF protection +- Session hijacking prevention +- Brute force protection +- Authorization bypass attempts + +### 12.4 Performance Tests + +- Authentication overhead +- Query performance with user filters +- Concurrent user sessions +- Database scalability with many users + +## 13. Documentation Requirements + +### 13.1 User Documentation + +- Getting started with multi-user InvokeAI +- Login and account management +- Using shared boards +- Understanding permissions +- Troubleshooting authentication issues + +### 13.2 Administrator Documentation + +- Setting up multi-user InvokeAI +- User management guide +- Creating and managing shared boards +- Email configuration +- Security best practices +- Backup and restore with user data + +### 13.3 Developer Documentation + +- Authentication architecture +- API authentication requirements +- Adding new multi-user features +- Database schema changes +- Testing multi-user features + +### 13.4 Migration Documentation + +- Upgrading from single-user to multi-user +- Data migration strategies +- Rollback procedures +- Common issues and solutions + +## 14. Future Enhancements + +### 14.1 Phase 2 Features + +- **OAuth2/OpenID Connect integration** (deferred from initial release to keep scope manageable) +- Two-factor authentication +- API keys for programmatic access +- Enhanced team/group management +- Advanced permission system (roles and capabilities) + +### 14.2 Phase 3 Features + +- SSO integration (SAML, LDAP) +- User quotas and limits +- Resource usage tracking +- Advanced collaboration features +- Workflow template library with permissions +- Model access controls per user/group + +## 15. Success Metrics + +### 15.1 Functionality Metrics + +- Successful user authentication rate +- Zero unauthorized data access incidents +- All tests passing (unit, integration, security) +- API response time within acceptable limits + +### 15.2 Usability Metrics + +- User setup completion time < 2 minutes +- Login time < 2 seconds +- Clear error messages for all auth failures +- Positive user feedback on multi-user features + +### 15.3 Security Metrics + +- No critical security vulnerabilities identified +- CodeQL scan passes +- Penetration testing completed +- Security best practices followed + +## 16. Risks and Mitigations + +### 16.1 Technical Risks + +| Risk | Impact | Probability | Mitigation | +|------|--------|-------------|------------| +| Performance degradation with user filtering | Medium | Low | Index optimization, query caching | +| Database migration failures | High | Low | Thorough testing, rollback procedures | +| Session management complexity | Medium | Medium | Use proven libraries (PyJWT), extensive testing | +| Auth bypass vulnerabilities | High | Low | Security review, penetration testing | + +### 16.2 UX Risks + +| Risk | Impact | Probability | Mitigation | +|------|--------|-------------|------------| +| Confusion in migration for existing users | Medium | High | Clear documentation, migration wizard | +| Friction from additional login step | Low | High | Remember me option, long session timeout | +| Complexity of admin interface | Medium | Medium | Intuitive UI design, user testing | + +### 16.3 Operational Risks + +| Risk | Impact | Probability | Mitigation | +|------|--------|-------------|------------| +| Email delivery failures | Low | Medium | Show links in UI, document manual methods | +| Lost admin password | High | Low | Document recovery procedure, config reset | +| User data conflicts in migration | Medium | Low | Data validation, backup requirements | + +## 17. Implementation Phases + +### Phase 1: Foundation (Weeks 1-2) + +- Database schema design and migration +- Basic authentication service +- Password hashing and validation +- Session management + +### Phase 2: Backend API (Weeks 3-4) + +- Authentication endpoints +- User management endpoints +- Authorization middleware +- Update existing endpoints with auth + +### Phase 3: Frontend Auth (Weeks 5-6) + +- Login page and flow +- Administrator setup +- Session management +- Auth state management + +### Phase 4: Multi-tenancy (Weeks 7-9) + +- User isolation in all services +- Shared boards implementation +- Queue permission filtering +- Workflow public/private + +### Phase 5: Admin Interface (Weeks 10-11) + +- User management UI +- Board sharing UI +- Admin-specific features +- User profile page + +### Phase 6: Testing & Polish (Weeks 12-13) + +- Comprehensive testing +- Security audit +- Performance optimization +- Documentation +- Bug fixes + +### Phase 7: Beta & Release (Week 14+) + +- Beta testing with selected users +- Feedback incorporation +- Final testing +- Release preparation +- Documentation finalization + +## 18. Acceptance Criteria + +- [ ] Administrator can set up initial account on first launch +- [ ] Users can log in with email and password +- [ ] Users can change their password +- [ ] Administrators can create, edit, and delete users +- [ ] User data is properly isolated (boards, images, workflows) +- [ ] Shared boards work correctly with permissions +- [ ] Non-admin users cannot access model management +- [ ] Queue filtering works correctly for users and admins +- [ ] Session management works correctly (expiry, renewal, logout) +- [ ] All security tests pass +- [ ] API documentation is updated +- [ ] User and admin documentation is complete +- [ ] Migration from single-user works smoothly +- [ ] Performance is acceptable with multiple concurrent users +- [ ] Backward compatibility mode works (auth disabled) + +## 19. Design Decisions + +The following design decisions have been approved for implementation: + +1. **OAuth2 Priority**: OAuth2/OpenID Connect integration will be a **future enhancement**. The initial release will focus on username/password authentication to keep scope manageable. + +2. **Email Requirement**: Email/SMTP configuration is **optional**. Many administrators will not have ready access to an outgoing SMTP server. The system will provide fallback mechanisms (showing setup links directly in the admin UI) when email is not configured. + +3. **Data Migration**: During migration from single-user to multi-user mode, the administrator will be given the **option to specify an arbitrary user account** to hold legacy data. The admin account can be used for this purpose if the administrator wishes. + +4. **API Compatibility**: Authentication will be **required on all APIs**, but authentication will not be required if multi-user support is disabled (backward compatibility mode with `auth_enabled: false`). + +5. **Session Storage**: The system will use **JWT tokens with optional server-side session tracking**. This provides scalability while allowing administrators to enable server-side tracking if needed. + +6. **Audit Logging**: The system will **log authentication events and admin actions**. This provides accountability and security monitoring for critical operations. + +## 20. Conclusion + +This specification provides a comprehensive blueprint for implementing multi-user support in InvokeAI. The design prioritizes: + +- **Security**: Proper authentication, authorization, and data isolation +- **Usability**: Intuitive UI, smooth migration, minimal friction +- **Scalability**: Efficient database design, performant queries +- **Maintainability**: Clean architecture, comprehensive testing +- **Flexibility**: Future enhancement paths, optional features + +The phased implementation approach allows for iterative development and testing, while the detailed specifications ensure all stakeholders have clear expectations of the final system. diff --git a/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx new file mode 100644 index 00000000000..6d33414ab38 --- /dev/null +++ b/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx @@ -0,0 +1,390 @@ +--- +title: Multi-User Guide +description: How to use InvokeAI in multi-user mode as an end user. +sidebar: + order: 3 +--- + +## Overview + +Multi-User mode is a recent feature (introduced in version 6.12), which allows multiple individuals to share a single InvokeAI server while keeping their work separate and organized. Each user has their own username and login password, images, assets, image boards, customization settings and workflows. + +Two types of users are recognized: + +- A user with **Administrator** status can add, remove and modify other users, and can install models. They also have the ability to view the full session queue and pause or kill other users' jobs. +- **Non-administrator** users can modify their own profile but not others. They also do not have the ability to install or configure models, but must ask an Administrator to do this task. + +Multiple users can be granted Administrator status. + +--- + +## Getting Started + +To activate Multi-User mode, open the `INVOKEAI_ROOT/invokeai.yaml` configuration file in a text editor. Add this line anywhere in the file: + +```yaml +multiuser: true +``` + +You may also wish to make InvokeAI available to other machines on your local LAN. Add an additional line to `invokeai.yaml`: + +```yaml +host: 0.0.0.0 +``` + +Restart the server. It will now be in multi-user mode. If you enabled the `host` option, other users on your home or office LAN will be able to reach it by browsing to the IP address of the machine the backend is running on (`http://host-ip-address:9090`). + +:::tip[Do not expose InvokeAI to the internet] +It is not recommended to expose the InvokeAI host to the internet due to security concerns. +::: + +### Initial Setup (First Time in Multi-User Mode) + +If you're the first person to access a fresh InvokeAI installation in multi-user mode, you'll see the **Administrator Setup** dialog: + +![Administrator Setup Screen](./assets/admin-setup.png) + +Now: + +1. Enter your email address (this will be your login name) +2. Create a display name (this will be the name other users see) +3. Choose a strong password that meets the requirements: + - At least 8 characters long + - Contains uppercase letters + - Contains lowercase letters + - Contains numbers +4. Confirm your password +5. Click **Create Administrator Account** + +You'll now be taken to a login screen and can enter the credentials you just created. + +### Adding and Modifying Users + +If you are logged in as Administrator, you can add additional users. Click on the small "person silhouette" icon at the bottom left of the main Invoke screen and select "User Management:" + +![Administrator Menu](./assets/admin-add-user-1.png) + +This will take you to the User Management screen... + +![User Management screen](./assets/admin-add-user-2.png) + +...where you can click "Create User" to add a new user. + +![Add User Screen](./assets/admin-add-user-3.png) + +The User Management screen also allows you to: + +1. Temporarily change a user's status to Inactive, preventing them from logging in to Invoke. +2. Edit a user (by clicking on the pencil icon) to change the user's display name or password. +3. Permanently delete a user. +4. Grant a user Administrator privileges. + +### Command-line User Management Scripts + +Administrators can also use a series of command-line scripts to add, modify, or delete users. If you use the launcher, click the ">" icon to enter the command-line interface. Otherwise, if you are a native command-line user, activate the InvokeAI environment from your terminal. + +The commands are named: + +- **invoke-useradd** — add a user +- **invoke-usermod** — modify a user +- **invoke-userdel** — delete a user +- **invoke-userlist** — list all users + +Pass the `--help` argument to get the usage of each script. For example: + +```bash +> invoke-useradd --help +usage: invoke-useradd [-h] [--root ROOT] [--email EMAIL] [--password PASSWORD] [--name NAME] [--admin] + +Add a user to the InvokeAI database + +options: + -h, --help show this help message and exit + --root ROOT, -r ROOT Path to the InvokeAI root directory. If omitted, the root is resolved in this order: the $INVOKEAI_ROOT environment + variable, the active virtual environment's parent directory, or $HOME/invokeai. + --email EMAIL, -e EMAIL + User email address + --password PASSWORD, -p PASSWORD + User password + --name NAME, -n NAME User display name (optional) + --admin, -a Make user an administrator + +If no arguments are provided, the script will run in interactive mode. +``` + +--- + +## Logging in as a Non-Administrative User + +If you are a registered user on the system, enter your email address and password to log in. The Administrator will be able to provide you with the values to use: + +![Login Screen](./assets/user-login-1.png) + +As an unprivileged user you can do pretty much anything that's allowed under single-user mode — generating images, using LoRAs, creating and running workflows, creating image boards — but you are restricted against installing new models, changing low-level server settings, or interfering with other users. More information on user roles is given below. + +### Changing your Profile + +To change your display name or profile, click on the person silhouette icon at the bottom left of the screen and choose "My Profile". This will take you to a screen that lets you change these values. At this time you can change your display name but not your login ID (ordinarily your contact email address). + +--- + +## Understanding User Roles + +In single-user mode, you have access to all features without restrictions. In multi-user mode, InvokeAI has two user roles: + +### Regular User + +As a regular user, you can: + +- Create and manage your own image boards +- Generate images using all AI tools (Linear, Canvas, Upscale, Workflows) +- Create, save, and load your own workflows +- View your own generation queue +- Customize your UI preferences (theme, hotkeys, etc.) +- View available models (read-only access to Model Manager) +- View shared and public boards created by other users +- View and use workflows marked as shared by other users + +You cannot: + +- Add, delete, or modify models +- View or modify other users' private boards, images, or workflows +- Manage user accounts +- Access system configuration +- View or cancel other users' generation tasks + +:::tip[The generation queue] +When two or more users are accessing InvokeAI at the same time, their image generation jobs will be placed on the session queue on a first-come, first-serve basis. This means that you will have to wait for other users' image rendering jobs to complete before yours will start. + +When another user's job is running, you will see the image generation progress bar and a queue badge that reads `X/Y`, where "X" is the number of jobs you have queued and "Y" is the total number of jobs queued, including your own and others. + +You can also pull up the Queue tab in order to see where your job is in relationship to other queued tasks. +::: + +### Administrator + +Administrators have all regular user capabilities, plus: + +- Full model management (add, delete, configure models) +- Create and manage user accounts +- View and manage all users' generation queues +- View and manage all users' boards, images, and workflows (including system-owned legacy content) +- Access system configuration +- Grant or revoke admin privileges + +--- + +## Working with Your Content in Multi-User Mode + +### Image Boards + +In multi-user mode, each user can create an unlimited number of boards and organize their images and assets as they see fit. Boards have three visibility levels: + +- **Private** (default): Only you (and administrators) can see and modify the board. +- **Shared**: All users can view the board and its contents, but only you (and administrators) can modify it (rename, archive, delete, or add/remove images). +- **Public**: All users can view the board. Only you (and administrators) can modify the board's structure (rename, archive, delete). + +To change a board's visibility, right-click on the board and select the desired visibility option. + +Administrators can see and manage all users' image boards and their contents regardless of visibility settings. + +### Going From Multi-User to Single-User Mode + +If an InvokeAI instance was in multiuser mode and then restarted in single user mode (by setting `multiuser: false` in the configuration file), all users' boards will be consolidated in one place. Any images that were in "Uncategorized" will be merged together into a single Uncategorized board. If, at a later date, the server is restarted in multi-user mode, the boards and images will be separated and restored to their owners. + +### Workflows + +Each user has their own private workflow library. Workflows you create are visible only to you by default. + +You can share a workflow with other users by marking it as **shared** (public). Shared workflows appear in all users' workflow libraries and can be opened by anyone, but only the owner (or an administrator) can modify or delete them. + +To share a workflow, open it and use the sharing controls to toggle its public/shared status. + +:::caution[Preexisting workflows after enabling multi-user mode] +When you enable multi-user mode for the first time on an existing InvokeAI installation, all workflows that were created before multi-user mode was activated will appear in the **shared workflows** section. These preexisting workflows are owned by the internal "system" account and are visible to all users. Administrators can edit or delete these shared legacy workflows. Regular users can view and use them but cannot modify them. +::: + +### The Generation Queue + +The queue shows your pending and running generation tasks. + +**Queue Features:** + +- View your current and completed generations +- Cancel pending tasks +- Re-run previous generations +- Monitor progress in real-time + +**Queue Isolation:** + +- You will see your own queue items, as well as the items generated by other users, but the generation parameters (e.g. prompts) for other users' jobs are hidden for privacy reasons. +- Administrators can view all queues for troubleshooting. +- Your generations won't interfere with other users' tasks. + +--- + +## Customizing Your Experience + +### Personal Preferences + +Your UI preferences are saved to your account and are restored when you log in: + +- **Theme**: Choose between light and dark modes +- **Hotkeys**: Customize keyboard shortcuts +- **Canvas Settings**: Default zoom, grid visibility, etc. +- **Generation Defaults**: Default values for width, height, steps, etc. + +These settings are stored per-user and won't affect other users. + +--- + +## Troubleshooting + +### Cannot Log In + +**Issue:** Login fails with "Incorrect email or password" + +**Solutions:** + +- Verify you're entering the correct email address +- Check that Caps Lock is off +- Try typing the password slowly to avoid mistakes +- Contact your administrator if you've forgotten your password + +**Issue:** Login fails with "Account is disabled" + +**Solution:** Contact your administrator to reactivate your account + +### Session Expired + +**Issue:** You're suddenly logged out and see "Session expired" + +**Explanation:** Sessions expire after 24 hours (or 7 days with "remember me") + +**Solution:** Simply log in again with your credentials + +### Cannot Access Features + +**Issue:** Features like Model Manager show "Admin privileges required" + +**Explanation:** Some features are restricted to administrators + +**Solution:** + +- For model viewing: You can view but not modify models +- For user management: Contact an administrator +- For system configuration: Contact an administrator + +### Missing Boards or Images + +**Issue:** Boards or images you created are not visible + +**Possible Causes:** + +1. **Filter Applied:** Check if a filter is hiding content +2. **Wrong User:** Ensure you're logged in with the correct account +3. **Archived Board:** Check the "Show Archived" option + +**Solution:** + +- Clear any active filters +- Verify you're logged in as the right user +- Check archived items + +### Slow Performance + +**Issue:** Generation or UI feels slower than expected + +**Possible Causes:** + +- Other users generating images simultaneously +- Server resource limits +- Network latency + +**Solutions:** + +- Check the queue to see if others are generating +- Wait for current generations to complete +- Contact administrator if persistent + +### Generation Stuck in Queue + +**Issue:** Your generation is queued but not starting + +**Possible Causes:** + +- Server is processing other users' generations +- Server resources are fully utilized +- Technical issue with the server + +**Solutions:** + +- Wait for your turn in the queue +- Check if your generation is paused +- Contact administrator if stuck for extended period + +--- + +## Frequently Asked Questions + +### Can other users see my images? + +Not unless you change your board's visibility to "shared" or "public". All personal boards and images are private by default. + +### Can I share my workflows with others? + +Yes. You can mark any workflow as shared (public), which makes it visible to all users. Other users can view and use shared workflows, but only you or an administrator can modify or delete them. + +### How long do sessions last? + +- 24 hours by default +- 7 days if you check "Remember me" during login + +### Can I use the API with multi-user mode? + +Yes, but you'll need to authenticate with a JWT token. See the [API Guide](/features/multi-user/api-guide/) for details. + +### What happens if I forget my password? + +Contact your administrator. They can reset your password for you. + +### Can I have multiple sessions? + +Yes, you can log in from multiple devices or browsers simultaneously. All sessions will use the same account and see the same content. + +### Why can't I see the Model Manager "Add Models" tab? + +Regular users can see the Models tab but with read-only access. Check that you're logged in and try refreshing the page. + +### How do I know if I'm an administrator? + +Administrators see an "Admin" badge next to their name in the top-right corner and have access to additional features like User Management. + +### Can I request admin privileges? + +Yes, ask your current administrator to grant you admin privileges. Admin privileges will give you the ability to see all other users' boards and images, as well as to add models and change various server-wide settings. + +## Getting Help + +### Support Channels + +- **Administrator:** Contact your system administrator for account issues +- **Documentation:** Check the [FAQ](/troubleshooting/faq/) for common issues +- **Community:** Join the [Discord](https://discord.gg/ZmtBAhwWhy) for help +- **Bug Reports:** File issues on [GitHub](https://github.com/invoke-ai/InvokeAI/issues) + +### Reporting Issues + +When reporting an issue, include: + +- Your role (regular user or administrator) +- What you were trying to do +- What happened instead +- Any error messages you saw +- Your browser and operating system + +## Additional Resources + +- [Administrator Guide](/features/multi-user/admin-guide/) — For administrators managing users and the system +- [API Guide](/features/multi-user/api-guide/) — For developers using the InvokeAI API +- [Multi-User Specification](/features/multi-user/specification/) — Technical details about the feature diff --git a/docs/src/content/docs/workflows/adding-nodes.mdx b/docs/src/content/docs/features/Workflows/adding-nodes.mdx similarity index 100% rename from docs/src/content/docs/workflows/adding-nodes.mdx rename to docs/src/content/docs/features/Workflows/adding-nodes.mdx diff --git a/docs/src/content/docs/workflows/assets/groupsallscale.png b/docs/src/content/docs/features/Workflows/assets/groupsallscale.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupsallscale.png rename to docs/src/content/docs/features/Workflows/assets/groupsallscale.png diff --git a/docs/src/content/docs/workflows/assets/groupsconditioning.png b/docs/src/content/docs/features/Workflows/assets/groupsconditioning.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupsconditioning.png rename to docs/src/content/docs/features/Workflows/assets/groupsconditioning.png diff --git a/docs/src/content/docs/workflows/assets/groupscontrol.png b/docs/src/content/docs/features/Workflows/assets/groupscontrol.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupscontrol.png rename to docs/src/content/docs/features/Workflows/assets/groupscontrol.png diff --git a/docs/src/content/docs/workflows/assets/groupsimgvae.png b/docs/src/content/docs/features/Workflows/assets/groupsimgvae.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupsimgvae.png rename to docs/src/content/docs/features/Workflows/assets/groupsimgvae.png diff --git a/docs/src/content/docs/workflows/assets/groupsiterate.png b/docs/src/content/docs/features/Workflows/assets/groupsiterate.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupsiterate.png rename to docs/src/content/docs/features/Workflows/assets/groupsiterate.png diff --git a/docs/src/content/docs/workflows/assets/groupslora.png b/docs/src/content/docs/features/Workflows/assets/groupslora.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupslora.png rename to docs/src/content/docs/features/Workflows/assets/groupslora.png diff --git a/docs/src/content/docs/workflows/assets/groupsmultigenseeding.png b/docs/src/content/docs/features/Workflows/assets/groupsmultigenseeding.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupsmultigenseeding.png rename to docs/src/content/docs/features/Workflows/assets/groupsmultigenseeding.png diff --git a/docs/src/content/docs/workflows/assets/groupsnoise.png b/docs/src/content/docs/features/Workflows/assets/groupsnoise.png similarity index 100% rename from docs/src/content/docs/workflows/assets/groupsnoise.png rename to docs/src/content/docs/features/Workflows/assets/groupsnoise.png diff --git a/docs/src/content/docs/workflows/assets/linearview.png b/docs/src/content/docs/features/Workflows/assets/linearview.png similarity index 100% rename from docs/src/content/docs/workflows/assets/linearview.png rename to docs/src/content/docs/features/Workflows/assets/linearview.png diff --git a/docs/src/content/docs/workflows/assets/nodescontrol.png b/docs/src/content/docs/features/Workflows/assets/nodescontrol.png similarity index 100% rename from docs/src/content/docs/workflows/assets/nodescontrol.png rename to docs/src/content/docs/features/Workflows/assets/nodescontrol.png diff --git a/docs/src/content/docs/workflows/assets/nodesi2i.png b/docs/src/content/docs/features/Workflows/assets/nodesi2i.png similarity index 100% rename from docs/src/content/docs/workflows/assets/nodesi2i.png rename to docs/src/content/docs/features/Workflows/assets/nodesi2i.png diff --git a/docs/src/content/docs/workflows/assets/nodest2i.png b/docs/src/content/docs/features/Workflows/assets/nodest2i.png similarity index 100% rename from docs/src/content/docs/workflows/assets/nodest2i.png rename to docs/src/content/docs/features/Workflows/assets/nodest2i.png diff --git a/docs/src/content/docs/workflows/assets/workflow_library.png b/docs/src/content/docs/features/Workflows/assets/workflow_library.png similarity index 100% rename from docs/src/content/docs/workflows/assets/workflow_library.png rename to docs/src/content/docs/features/Workflows/assets/workflow_library.png diff --git a/docs/src/content/docs/workflows/comfyui-migration.mdx b/docs/src/content/docs/features/Workflows/comfyui-migration.mdx similarity index 100% rename from docs/src/content/docs/workflows/comfyui-migration.mdx rename to docs/src/content/docs/features/Workflows/comfyui-migration.mdx diff --git a/docs/src/content/docs/workflows/community-nodes.mdx b/docs/src/content/docs/features/Workflows/community-nodes.mdx similarity index 100% rename from docs/src/content/docs/workflows/community-nodes.mdx rename to docs/src/content/docs/features/Workflows/community-nodes.mdx diff --git a/docs/src/content/docs/workflows/editor-interface.mdx b/docs/src/content/docs/features/Workflows/editor-interface.mdx similarity index 100% rename from docs/src/content/docs/workflows/editor-interface.mdx rename to docs/src/content/docs/features/Workflows/editor-interface.mdx diff --git a/docs/src/content/docs/workflows/index.mdx b/docs/src/content/docs/features/Workflows/index.mdx similarity index 100% rename from docs/src/content/docs/workflows/index.mdx rename to docs/src/content/docs/features/Workflows/index.mdx diff --git a/docs/src/content/docs/features/gallery.mdx b/docs/src/content/docs/features/gallery.mdx index fec8c918a3e..d5aad28188b 100644 --- a/docs/src/content/docs/features/gallery.mdx +++ b/docs/src/content/docs/features/gallery.mdx @@ -2,6 +2,8 @@ title: Gallery Panel description: Learn how to manage, organize, and use your generated images and assets with the Gallery Panel in InvokeAI. lastUpdated: 2026-02-19 +sidebar: + order: 1 --- import { Card, CardGrid, Steps } from '@astrojs/starlight/components'; diff --git a/docs/src/content/docs/features/hotkeys.mdx b/docs/src/content/docs/features/hotkeys.mdx index 0c74b556b78..a20aa05288c 100644 --- a/docs/src/content/docs/features/hotkeys.mdx +++ b/docs/src/content/docs/features/hotkeys.mdx @@ -2,6 +2,8 @@ title: Hotkeys description: Learn how to use and customize hotkeys in InvokeAI, and how developers can interact with the hotkey system. lastUpdated: 2026-02-19 +sidebar: + order: 7 --- import { Tabs, TabItem, Steps, Card, CardGrid, Icon } from '@astrojs/starlight/components'; From 8e293ee8b0e90303015d31d8b9287e00ea84b075 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 30 Apr 2026 15:07:15 -0400 Subject: [PATCH 2/5] docs: finish moving workflows under features/ --- docs/astro.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 265a28482b5..8f1d861a5b6 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -82,7 +82,7 @@ export default defineConfig({ label: 'Features', autogenerate: { directory: 'features' }, }, - { + { label: 'Development', autogenerate: { directory: 'development', collapsed: true }, collapsed: true, From 74c1e475fb962e40c0d17189a0ddbb34b606c3bc Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 30 Apr 2026 17:16:47 -0400 Subject: [PATCH 3/5] Update docs/src/content/docs/features/Canvas/text-tool.mdx Co-authored-by: Josh Corbett --- docs/src/content/docs/features/Canvas/text-tool.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/features/Canvas/text-tool.mdx b/docs/src/content/docs/features/Canvas/text-tool.mdx index f131895e26f..6e223767d8e 100644 --- a/docs/src/content/docs/features/Canvas/text-tool.mdx +++ b/docs/src/content/docs/features/Canvas/text-tool.mdx @@ -1,7 +1,7 @@ --- title: Text Tool sidebar: - order: 2 + order: 3 --- import { LinkCard } from '@astrojs/starlight/components'; From 5f2f56d251055f71aae9fc7de1dd7c529c901e3a Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 30 Apr 2026 17:17:00 -0400 Subject: [PATCH 4/5] Update docs/src/content/docs/features/hotkeys.mdx Co-authored-by: Josh Corbett --- docs/src/content/docs/features/hotkeys.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/features/hotkeys.mdx b/docs/src/content/docs/features/hotkeys.mdx index a20aa05288c..a4b99fca16d 100644 --- a/docs/src/content/docs/features/hotkeys.mdx +++ b/docs/src/content/docs/features/hotkeys.mdx @@ -3,7 +3,7 @@ title: Hotkeys description: Learn how to use and customize hotkeys in InvokeAI, and how developers can interact with the hotkey system. lastUpdated: 2026-02-19 sidebar: - order: 7 + order: 2 --- import { Tabs, TabItem, Steps, Card, CardGrid, Icon } from '@astrojs/starlight/components'; From 04bfde99cf37089a1cbea0db4146c12a02b6c1a5 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 30 Apr 2026 19:29:24 -0400 Subject: [PATCH 5/5] docs(multiuser): fix links, remove redundant sections --- .../development/Architecture/invocations.mdx | 2 +- .../features/Multi-User Mode/admin-guide.mdx | 367 ++----- .../features/Multi-User Mode/api-guide.mdx | 8 +- .../Multi-User Mode/specification.mdx | 959 ------------------ .../features/Multi-User Mode/user-guide.mdx | 47 +- 5 files changed, 89 insertions(+), 1294 deletions(-) delete mode 100644 docs/src/content/docs/features/Multi-User Mode/specification.mdx diff --git a/docs/src/content/docs/development/Architecture/invocations.mdx b/docs/src/content/docs/development/Architecture/invocations.mdx index d582d73ba92..08ebdda6a93 100644 --- a/docs/src/content/docs/development/Architecture/invocations.mdx +++ b/docs/src/content/docs/development/Architecture/invocations.mdx @@ -316,7 +316,7 @@ new Invocation ready to be used. Once you've created a Node, the next step is to share it with the community! The best way to do this is to submit a Pull Request to add the Node to the -[Community Nodes](/workflows/community-nodes/) list. If you're not sure how to do that, +[Community Nodes](/features/workflows/community-nodes) list. If you're not sure how to do that, take a look a at our [contributing nodes overview](/development/guides/creating-nodes/). ## Advanced diff --git a/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx index 5027a79a03a..0de968d2e29 100644 --- a/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx +++ b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx @@ -5,9 +5,13 @@ sidebar: order: 4 --- +import { Steps } from '@astrojs/starlight/components' + ## Overview -This guide is for administrators managing a multi-user InvokeAI installation. It covers initial setup, user management, security best practices, and troubleshooting. +This guide is for administrators managing a multi-user InvokeAI +installation. It covers initial setup, user management, security best +practices, and troubleshooting. ## Prerequisites @@ -44,6 +48,7 @@ When InvokeAI starts for the first time in multi-user mode, you'll see the **Adm **Setup Steps:** + 1. **Email Address**: Enter a valid email address (this becomes your username) - Example: `admin@example.com` or `admin@localhost` for testing @@ -75,6 +80,7 @@ When InvokeAI starts for the first time in multi-user mode, you'll see the **Adm 4. **Confirm Password**: Re-enter the password 5. Click **Create Administrator Account** + :::caution[Important] Store these credentials securely! The first administrator account can reset the password to something new, but cannot retrieve a lost one. @@ -100,20 +106,6 @@ jwt_token_expiry_hours: 24 # Default session timeout jwt_remember_me_days: 7 # "Remember me" duration ``` -**Single-User Mode** (`multiuser: false` or option absent): - -- No authentication required -- All functionality enabled by default -- All boards and images visible in unified view -- Ideal for personal use or trusted environments - -**Multi-User Mode** (`multiuser: true`): - -- Authentication required for access -- User isolation for boards, images, and workflows -- Role-based permissions enforced -- Ideal for shared servers or team environments - :::caution[Mode Switching Behavior] **Switching to Single-User Mode:** If boards or images were created in multi-user mode, they will all be combined into a single unified view when switching to single-user mode. @@ -124,10 +116,12 @@ jwt_remember_me_days: 7 # "Remember me" duration When upgrading from a single-user installation or switching modes: + 1. **Automatic Migration**: The database will automatically migrate to multi-user schema when multi-user mode is first enabled 2. **Legacy Data Ownership**: Existing data (boards, images, workflows) created in single-user mode is assigned to an internal user named "system" 3. **Administrator Access**: Only administrators will have access to legacy "system"-owned assets when in multi-user mode 4. **No Data Loss**: All existing content is preserved + **Migration Process:** @@ -153,168 +147,72 @@ A utility to migrate legacy "system"-owned assets to specific user accounts will ### Creating Users -**Via Web Interface (Coming Soon):** - -:::note[Web UI for User Management] -A web-based user interface that allows administrators to manage users is coming in a future release. Until then, use the command-line scripts described below. -::: - -**Via Command Line Scripts:** - -InvokeAI provides several command-line scripts in the `scripts/` directory for user management: - -**useradd.py** — Add a new user: - -```bash -# Interactive mode (prompts for details) -python scripts/useradd.py - -# Create a regular user -python scripts/useradd.py \ - --email user@example.com \ - --password TempPass123 \ - --name "User Name" - -# Create an administrator -python scripts/useradd.py \ - --email admin@example.com \ - --password AdminPass123 \ - --name "Admin Name" \ - --admin -``` - -**userlist.py** — List all users: - -```bash -# List all users -python scripts/userlist.py - -# Show detailed information -python scripts/userlist.py --verbose -``` - -**usermod.py** — Modify an existing user: - -```bash -# Change display name -python scripts/usermod.py --email user@example.com --name "New Name" - -# Promote to administrator -python scripts/usermod.py --email user@example.com --admin - -# Demote from administrator -python scripts/usermod.py --email user@example.com --no-admin - -# Deactivate account -python scripts/usermod.py --email user@example.com --deactivate +Administrators can create and modify users (including other +administrators) via a built-in web interface or using command-line +scripts. -# Reactivate account -python scripts/usermod.py --email user@example.com --activate +#### **Via the Web Frontend:** -# Change password -python scripts/usermod.py --email user@example.com --password NewPassword123 -``` - -**userdel.py** — Delete a user: - -```bash -# Delete a user (prompts for confirmation) -python scripts/userdel.py --email user@example.com - -# Delete without confirmation -python scripts/userdel.py --email user@example.com --force -``` - -:::tip[Script Usage] -Run any script with `--help` to see all available options: - -```bash -python scripts/useradd.py --help -``` -::: - -:::caution[Command Line Management] -- These scripts directly modify the database -- Always backup your database before making changes -- Changes take effect immediately (users may need to log in again) -- Deleting a user permanently removes all their content -::: - -### Editing Users - -**Via Command Line:** - -Use `usermod.py` as described above to modify user properties. - -:::caution[Last Administrator] -You cannot remove admin privileges from the last remaining administrator account. -::: +Please see the Multi-User Guide's section on [Adding and Modifying Users](../user-guide#adding-and-modifying-users) +for a walk-through. -### Resetting User Passwords +#### **Via Command Line Scripts:** -**Via Web Interface (Coming Soon):** +##### Command-line User Management Scripts -Web-based password reset functionality for administrators is coming in a future release. +Administrators can also use a series of command-line scripts to add, modify, or delete users. If you use the launcher, click the ">" icon to enter the command-line interface. Otherwise, if you are a native command-line user, activate the InvokeAI environment from your terminal. -**Via Command Line:** +All command-line arguments are optional. The scripts will prompt you to provide any missing arguments. -```bash -# Reset a user's password -python scripts/usermod.py --email user@example.com --password NewTempPassword123 -``` - -**Security Note:** Never send passwords via email or unsecured channels. Use secure communication methods. +The commands are: -### Deactivating Users +| Name | Function | Example CLI Usage | +|--------------------|---------------|--------------------| +|**invoke-useradd** | add a user | `invoke-useradd --email user@example.com --name "Example User" --password "badpassword"` | +|**invoke-usermod** | modify a user | `invoke-usermod --email user@example.com --name "Mr. Example User" --password "8adsf2**%"` | +|**invoke-userdel** | delete a user | `invoke-userdel --email user@example.com --force` | +|**invoke-userlist** | list all users| `invoke-userlist` | -**Via Command Line:** +Pass the `--help` argument to get the usage of each script. For example: ```bash -# Deactivate a user account -python scripts/usermod.py --email user@example.com --deactivate - -# Reactivate a user account -python scripts/usermod.py --email user@example.com --activate +> invoke-useradd --help +usage: invoke-useradd [-h] [--root ROOT] [--email EMAIL] [--password PASSWORD] [--name NAME] [--admin] + +Add a user to the InvokeAI database + +options: + -h, --help show this help message and exit + --root ROOT, -r ROOT Path to the InvokeAI root directory. If omitted, the root is resolved in this order: the $INVOKEAI_ROOT environment + variable, the active virtual environment's parent directory, or $HOME/invokeai. + --email EMAIL, -e EMAIL + User email address + --password PASSWORD, -p PASSWORD + User password + --name NAME, -n NAME User display name (optional) + --admin, -a Make user an administrator + +If no arguments are provided, the script will run in interactive mode. ``` -**Effects:** - -- User cannot log in when deactivated -- Existing sessions are immediately invalidated -- User's data is preserved -- Can be reactivated at any time - -### Deleting Users - -**Via Command Line:** - -```bash -# Delete a user (prompts for confirmation) -python scripts/userdel.py --email user@example.com - -# Delete without confirmation prompt -python scripts/userdel.py --email user@example.com --force -``` - -**Important:** - -- This action is **permanent** -- User's boards, images, and workflows are deleted -- Cannot be undone -- Consider deactivating instead of deleting - :::danger[Data Loss] -Deleting a user permanently removes all their content. Back up the database first if recovery might be needed. +Be aware that deleting a user permanently removes all their +content and settings, and will remove their access to the boards and images they have created. +However, their physical image files will continue to reside in `outputs/images` until a gallery maintenance script is +run to remove orphan images. +Back up the database first if recovery might be needed. ::: ### Viewing User Activity **Queue Management:** + 1. Navigate to **Admin** → **Queue Overview** 2. View all users' active and pending generations 3. Filter by user 4. Cancel stuck or problematic tasks + **User Statistics:** @@ -325,123 +223,10 @@ Deleting a user permanently removes all their content. Back up the database firs ## Model Management -As an administrator, you have full access to model management. - -### Adding Models - -**Via Model Manager UI:** - -1. Go to **Models** tab -2. Click **Add Model** -3. Choose installation method: - - **From URL**: Provide HuggingFace repo or download URL - - **From Local Path**: Scan local directories - - **Import**: Import model from filesystem - -**Supported Model Types:** - -- Main models (Stable Diffusion, SDXL, FLUX) -- LoRA models -- ControlNet models -- VAE models -- Textual Inversions -- IP-Adapters - -### Configuring Models - -**Model Settings:** - -- Display name -- Description -- Default generation settings (CFG, steps, scheduler) -- Variant selection (fp16/fp32) -- Model thumbnail image - -**Default Settings:** - -Set default parameters that users will start with: - -1. Select a model -2. Go to **Default Settings** tab -3. Configure: - - CFG Scale - - Steps - - Scheduler - - VAE selection -4. Save settings - -### Removing Models - -1. Go to **Models** tab -2. Select model(s) to remove -3. Click **Delete** -4. Confirm deletion - -:::caution[Impact] -Removing a model affects all users who may be using it in workflows or saved settings. -::: - -## Shared Boards - -Shared boards enable collaboration between users while maintaining control. - -:::note[Future Feature] -Board sharing will be implemented in a future release. -::: - -### Creating Shared Boards - -1. Log in as administrator -2. Create a new board (or use existing board) -3. Right-click the board → **Share Board** -4. Add users and set permissions -5. Click **Save Sharing Settings** - -### Permission Levels - -| Level | View | Add Images | Edit/Delete | Manage Sharing | -|-------|------|------------|-------------|----------------| -| **Read** | Yes | No | No | No | -| **Write** | Yes | Yes | Yes | No | -| **Admin** | Yes | Yes | Yes | Yes | - -**Permission Recommendations:** - -- **Read**: For viewers who should see but not modify content -- **Write**: For active collaborators who add and organize images -- **Admin**: For trusted users who help manage the shared board - -### Managing Shared Boards - -**Add Users to Shared Board:** - -1. Right-click shared board → **Manage Sharing** -2. Click **Add User** -3. Select user from dropdown -4. Choose permission level -5. Save changes - -**Remove Users from Shared Board:** - -1. Right-click shared board → **Manage Sharing** -2. Find user in list -3. Click **Remove** -4. Confirm removal - -**Change User Permissions:** - -1. Right-click shared board → **Manage Sharing** -2. Find user in list -3. Change permission dropdown -4. Save changes - -### Shared Board Best Practices - -- Give meaningful names to shared boards -- Document the board's purpose in the description -- Assign minimum necessary permissions -- Regularly audit access lists -- Remove users who no longer need access +As an administrator, you have full access to the [Model +Manager](/concepts/models) and can install, edit and delete +models just as in single-user mode. Unprivileged users, however, can +view the models previously installed, but cannot add or modify them. ## Security @@ -517,7 +302,7 @@ host: 0.0.0.0 After relaunching the backend you will be able to reach the server from other machines on the LAN using the server machine's IP address or hostname and port 9090. -#### Connecting to the Internet +#### Making InvokeAI Accessible to the Internet :::danger[Use at your own risk] The InvokeAI team has done its best to make the software free of exploitable bugs, but the software has not undergone a rigorous security audit or intrusion testing. Use at your own risk. @@ -563,7 +348,8 @@ It is best to restrict access to trusted networks and remote IP addresses, or us **Backup and Recovery:** -It is a good idea to periodically backup your InvokeAI database, images, and possibly models in the event of unauthorized use of a publicly-accessible server. +It is always a good idea to periodically backup your InvokeAI database and images, but especially +so if the server is publicly accessible to the Internet. **Manual Backup:** @@ -635,12 +421,14 @@ Include these directories/files: **Recovery Process:** + 1. Install InvokeAI on new system 2. Restore configuration file 3. Restore database directory 4. Restore models and outputs 5. Verify file permissions 6. Start InvokeAI and test + ## Troubleshooting @@ -693,6 +481,7 @@ sqlite3 databases/invokeai.db "PRAGMA journal_mode=WAL;" **Recovery Process:** + 1. Stop InvokeAI 2. Direct database access: @@ -712,6 +501,7 @@ sqlite3 databases/invokeai.db "PRAGMA journal_mode=WAL;" SET password_hash = '$2b$12$...' WHERE email = 'admin@example.com'; ``` + 4. Restart InvokeAI @@ -723,10 +513,12 @@ sqlite3 databases/invokeai.db "PRAGMA journal_mode=WAL;" **Diagnosis:** + 1. Check active generation count 2. Review resource usage (CPU/GPU/RAM) 3. Check database size and performance 4. Review network latency + **Solutions:** @@ -802,16 +594,20 @@ Full audit logging is planned for a future release. Currently, you can: ### Can users have different model access? -Not in the current release. All users can view and use all installed models. Per-user model access is a possible enhancement. +Currently all users can view and use all installed models. Per-user +model access is a possible enhancement. Please let the development +team know if you want this feature. ### How do I handle user data when they leave? Best practice: + 1. Deactivate the account first 2. Transfer ownership of shared boards 3. After transition period, delete the account 4. Or keep the account deactivated for audit purposes + ### What's the licensing impact of multi-user mode? @@ -819,27 +615,10 @@ InvokeAI remains under its existing license. Multi-user mode does not change lic ## Getting Help -### Support Resources +### Support -- **Documentation**: [InvokeAI Docs](https://invoke.ai/) +- **General Documentation**: [InvokeAI Docs](https://invoke.ai/) +- **User Guide**: [For Users](/features/multi-user-mode/user-guide/) +- **API Guide**: [For Developers](/features/multi-user-mode/api-guide/) - **Discord**: [Join Community](https://discord.gg/ZmtBAhwWhy) - **GitHub Issues**: [Report Problems](https://github.com/invoke-ai/InvokeAI/issues) -- **User Guide**: [For Users](/features/multi-user/user-guide/) -- **API Guide**: [For Developers](/features/multi-user/api-guide/) - -### Reporting Issues - -When reporting administrator issues, include: - -- InvokeAI version -- Operating system and version -- Database size and user count -- Relevant log excerpts -- Steps to reproduce -- Expected vs actual behavior - -## Additional Resources - -- [User Guide](/features/multi-user/user-guide/) — For end users -- [API Guide](/features/multi-user/api-guide/) — For API consumers -- [Multi-User Specification](/features/multi-user/specification/) — Technical details diff --git a/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx index fa5077261b2..459a0f504bf 100644 --- a/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx +++ b/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx @@ -4,6 +4,7 @@ description: How to authenticate and interact with the InvokeAI API in multi-use sidebar: order: 5 --- +import { Steps } from '@astrojs/starlight/components' ## Overview @@ -33,10 +34,12 @@ When multi-user mode is enabled, all API endpoints (except `/api/v1/auth/setup` **Authentication Process:** + 1. **Obtain Token**: POST credentials to `/api/v1/auth/login` 2. **Store Token**: Save the JWT token securely 3. **Use Token**: Include token in `Authorization` header for all requests 4. **Refresh**: Re-authenticate when token expires + :::note[Single-User Mode] When running in single-user mode (`multiuser: false`), authentication endpoints are not available and authentication headers are not required. @@ -1221,9 +1224,8 @@ cors_origins: ## Additional Resources -- [User Guide](/features/multi-user/user-guide/) — For end users -- [Administrator Guide](/features/multi-user/admin-guide/) — For administrators -- [Multi-User Specification](/features/multi-user/specification/) — Technical details +- [User Guide](./user-guide/) — For end users +- [Administrator Guide](./admin-guide/) — For administrators - [GitHub Repository](https://github.com/invoke-ai/InvokeAI) — Source code --- diff --git a/docs/src/content/docs/features/Multi-User Mode/specification.mdx b/docs/src/content/docs/features/Multi-User Mode/specification.mdx deleted file mode 100644 index 1e137464495..00000000000 --- a/docs/src/content/docs/features/Multi-User Mode/specification.mdx +++ /dev/null @@ -1,959 +0,0 @@ ---- -title: Multi-User Specification -description: Detailed technical specification for InvokeAI's multi-user support. -sidebar: - order: 6 ---- - -## 1. Executive Summary - -This document provides a comprehensive specification for adding multi-user support to InvokeAI. The feature will enable a single InvokeAI instance to support multiple isolated users, each with their own generation settings, image boards, and workflows, while maintaining administrative controls for model management and system configuration. - -## 2. Overview - -### 2.1 Goals - -- Enable multiple users to share a single InvokeAI instance -- Provide user isolation for personal content (boards, images, workflows, settings) -- Maintain centralized model management by administrators -- Support shared boards for collaboration -- Provide secure authentication and authorization -- Minimize impact on existing single-user installations - -### 2.2 Non-Goals - -- Real-time collaboration features (multiple users editing same workflow simultaneously) -- Advanced team management features (in initial release) -- Migration of existing multi-user enterprise edition data -- Support for external identity providers (in initial release, can be added later) - -## 3. User Roles and Permissions - -### 3.1 Administrator Role - -**Capabilities:** - -- Full access to all InvokeAI features -- Model management (add, delete, configure models) -- User management (create, edit, delete users) -- View and manage all users' queue sessions -- Access system configuration -- Create and manage shared boards -- Grant/revoke administrative privileges to other users - -**Restrictions:** - -- Cannot delete their own account if they are the last administrator -- Cannot revoke their own admin privileges if they are the last administrator - -### 3.2 Regular User Role - -**Capabilities:** - -- Create, edit, and delete their own image boards -- Upload and manage their own assets -- Use all image generation tools (linear, canvas, upscale, workflow tabs) -- Create, edit, save, and load workflows -- Access public/shared workflows -- View and manage their own queue sessions -- Adjust personal UI preferences (theme, hotkeys, etc.) -- Access shared boards (read/write based on permissions) -- **View model configurations** (read-only access to model manager) -- **View model details, default settings, and metadata** - -**Restrictions:** - -- Cannot add, delete, or edit models -- **Can view but cannot modify model manager settings** (read-only access) -- Cannot reidentify, convert, or update model paths -- Cannot upload or change model thumbnail images -- Cannot save changes to model default settings -- Cannot perform bulk delete operations on models -- Cannot view or modify other users' boards, images, or workflows -- Cannot cancel or modify other users' queue sessions -- Cannot access system configuration -- Cannot manage users or permissions - -### 3.3 Future Role Considerations - -- **Viewer Role**: Read-only access (future enhancement) -- **Team/Group-based Permissions**: Organizational hierarchy (future enhancement) - -## 4. Authentication System - -### 4.1 Authentication Method - -- **Primary Method**: Username and password authentication with secure password hashing -- **Password Hashing**: Use bcrypt or Argon2 for password storage -- **Session Management**: JWT tokens or secure session cookies -- **Token Expiration**: Configurable session timeout (default: 7 days for "remember me", 24 hours otherwise) - -### 4.2 Initial Administrator Setup - -**First-time Launch Flow:** - -1. Application detects no administrator account exists -2. Displays mandatory setup dialog (cannot be skipped) -3. Prompts for: - - Administrator username (email format recommended) - - Administrator display name - - Strong password (minimum requirements enforced) - - Password confirmation -4. Stores hashed credentials in configuration -5. Creates administrator account in database -6. Proceeds to normal login screen - -**Reset Capability:** - -- Administrators can be reset by manually editing the config file -- Requires access to server filesystem (intentional security measure) -- Database maintains user records; config file contains root admin credentials - -### 4.3 Password Requirements - -- Minimum 8 characters -- At least one uppercase letter -- At least one lowercase letter -- At least one number -- At least one special character (optional but recommended) -- Not in common password list - -### 4.4 Login Flow - -1. User navigates to InvokeAI URL -2. If not authenticated, redirect to login page -3. User enters username/email and password -4. Optional "Remember me" checkbox for extended session -5. Backend validates credentials -6. On success: Generate session token, redirect to application -7. On failure: Display error, allow retry with rate limiting (prevent brute force) - -### 4.5 Logout Flow - -- User clicks logout button -- Frontend clears session token -- Backend invalidates session (if using server-side sessions) -- Redirect to login page - -### 4.6 Future Authentication Enhancements - -- OAuth2/OpenID Connect support -- Two-factor authentication (2FA) -- SSO integration -- API key authentication for programmatic access - -## 5. User Management - -### 5.1 User Creation (Administrator) - -**Flow:** - -1. Administrator navigates to user management interface -2. Clicks "Add User" button -3. Enters user information: - - Email address (required, used as username) - - Display name (optional, defaults to email) - - Role (User or Administrator) - - Initial password or "Send invitation email" -4. System validates email uniqueness -5. System creates user account -6. If invitation mode: - - Generate one-time secure token - - Send email with setup link - - Link expires after 7 days -7. If direct password mode: - - Administrator provides initial password - - User must change on first login - -**Invitation Email Flow:** - -1. User receives email with unique link -2. Link contains secure token -3. User clicks link, redirected to setup page -4. User enters desired password -5. Token validated and consumed (single-use) -6. Account activated -7. User redirected to login page - -### 5.2 User Profile Management - -**User Self-Service:** - -- Update display name -- Change password (requires current password) -- Update email address (requires verification) -- Manage UI preferences -- View account creation date and last login - -**Administrator Actions:** - -- Edit user information (name, email) -- Reset user password (generates reset link) -- Toggle administrator privileges -- Assign to groups (future feature) -- Suspend/unsuspend account -- Delete account (with data retention options) - -### 5.3 Password Reset Flow - -**User-Initiated (Future Enhancement):** - -1. User clicks "Forgot Password" on login page -2. Enters email address -3. System sends password reset link (if email exists) -4. User clicks link, enters new password -5. Password updated, user can login - -**Administrator-Initiated:** - -1. Administrator selects user -2. Clicks "Send Password Reset" -3. System generates reset token and link -4. Email sent to user -5. User follows same flow as user-initiated reset - -## 6. Data Model and Database Schema - -### 6.1 New Tables - -#### 6.1.1 users - -```sql -CREATE TABLE users ( - user_id TEXT NOT NULL PRIMARY KEY, - email TEXT NOT NULL UNIQUE, - display_name TEXT, - password_hash TEXT NOT NULL, - is_admin BOOLEAN NOT NULL DEFAULT FALSE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - last_login_at DATETIME -); -CREATE INDEX idx_users_email ON users(email); -CREATE INDEX idx_users_is_admin ON users(is_admin); -CREATE INDEX idx_users_is_active ON users(is_active); -``` - -#### 6.1.2 user_sessions - -```sql -CREATE TABLE user_sessions ( - session_id TEXT NOT NULL PRIMARY KEY, - user_id TEXT NOT NULL, - token_hash TEXT NOT NULL, - expires_at DATETIME NOT NULL, - created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - last_activity_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - user_agent TEXT, - ip_address TEXT, - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE -); -CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id); -CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at); -CREATE INDEX idx_user_sessions_token_hash ON user_sessions(token_hash); -``` - -#### 6.1.3 user_invitations - -```sql -CREATE TABLE user_invitations ( - invitation_id TEXT NOT NULL PRIMARY KEY, - email TEXT NOT NULL, - token_hash TEXT NOT NULL, - invited_by_user_id TEXT NOT NULL, - expires_at DATETIME NOT NULL, - used_at DATETIME, - created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - FOREIGN KEY (invited_by_user_id) REFERENCES users(user_id) ON DELETE CASCADE -); -CREATE INDEX idx_user_invitations_email ON user_invitations(email); -CREATE INDEX idx_user_invitations_token_hash ON user_invitations(token_hash); -CREATE INDEX idx_user_invitations_expires_at ON user_invitations(expires_at); -``` - -#### 6.1.4 shared_boards - -```sql -CREATE TABLE shared_boards ( - board_id TEXT NOT NULL, - user_id TEXT NOT NULL, - permission TEXT NOT NULL CHECK(permission IN ('read', 'write', 'admin')), - created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - PRIMARY KEY (board_id, user_id), - FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE -); -CREATE INDEX idx_shared_boards_user_id ON shared_boards(user_id); -CREATE INDEX idx_shared_boards_board_id ON shared_boards(board_id); -``` - -### 6.2 Modified Tables - -#### 6.2.1 boards - -```sql --- Add columns: -ALTER TABLE boards ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; -ALTER TABLE boards ADD COLUMN is_shared BOOLEAN NOT NULL DEFAULT FALSE; -ALTER TABLE boards ADD COLUMN created_by_user_id TEXT; - --- Add foreign key (requires recreation in SQLite): -FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE -FOREIGN KEY (created_by_user_id) REFERENCES users(user_id) ON DELETE SET NULL - --- Add indices: -CREATE INDEX idx_boards_user_id ON boards(user_id); -CREATE INDEX idx_boards_is_shared ON boards(is_shared); -``` - -#### 6.2.2 images - -```sql --- Add column: -ALTER TABLE images ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; - --- Add foreign key: -FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE - --- Add index: -CREATE INDEX idx_images_user_id ON images(user_id); -``` - -#### 6.2.3 workflows - -```sql --- Add columns: -ALTER TABLE workflows ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; -ALTER TABLE workflows ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE; - --- Add foreign key: -FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE - --- Add indices: -CREATE INDEX idx_workflows_user_id ON workflows(user_id); -CREATE INDEX idx_workflows_is_public ON workflows(is_public); -``` - -#### 6.2.4 session_queue - -```sql --- Add column: -ALTER TABLE session_queue ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; - --- Add foreign key: -FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE - --- Add index: -CREATE INDEX idx_session_queue_user_id ON session_queue(user_id); -``` - -#### 6.2.5 style_presets - -```sql --- Add columns: -ALTER TABLE style_presets ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system'; -ALTER TABLE style_presets ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE; - --- Add foreign key: -FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE - --- Add indices: -CREATE INDEX idx_style_presets_user_id ON style_presets(user_id); -CREATE INDEX idx_style_presets_is_public ON style_presets(is_public); -``` - -### 6.3 Migration Strategy - -1. Create new user tables (users, user_sessions, user_invitations, shared_boards) -2. Create default 'system' user for backward compatibility -3. Update existing data to reference 'system' user -4. Add foreign key constraints -5. Version as database migration (e.g., migration_25.py) - -### 6.4 Migration for Existing Installations - -- Single-user installations: Prompt to create admin account on first launch after update -- Existing data migration: Administrator can specify an arbitrary user account to hold legacy data (can be the admin account or a separate user) -- System provides UI during migration to choose destination user for existing data - -## 7. API Endpoints - -### 7.1 Authentication Endpoints - -#### POST /api/v1/auth/setup - -- Initialize first administrator account -- Only works if no admin exists -- Body: `{ email, display_name, password }` -- Response: `{ success, user }` - -#### POST /api/v1/auth/login - -- Authenticate user -- Body: `{ email, password, remember_me? }` -- Response: `{ token, user, expires_at }` - -#### POST /api/v1/auth/logout - -- Invalidate current session -- Headers: `Authorization: Bearer ` -- Response: `{ success }` - -#### GET /api/v1/auth/me - -- Get current user information -- Headers: `Authorization: Bearer ` -- Response: `{ user }` - -#### POST /api/v1/auth/change-password - -- Change current user's password -- Body: `{ current_password, new_password }` -- Headers: `Authorization: Bearer ` -- Response: `{ success }` - -### 7.2 User Management Endpoints (Admin Only) - -#### GET /api/v1/users - -- List all users (paginated) -- Query params: `offset`, `limit`, `search`, `role_filter` -- Response: `{ users[], total, offset, limit }` - -#### POST /api/v1/users - -- Create new user -- Body: `{ email, display_name, is_admin, send_invitation?, initial_password? }` -- Response: `{ user, invitation_link? }` - -#### GET `/api/v1/users/{user_id}` - -- Get user details -- Response: `{ user }` - -#### PATCH `/api/v1/users/{user_id}` - -- Update user -- Body: `{ display_name?, is_admin?, is_active? }` -- Response: `{ user }` - -#### DELETE `/api/v1/users/{user_id}` - -- Delete user -- Query params: `delete_data` (true/false) -- Response: `{ success }` - -#### POST `/api/v1/users/{user_id}/reset-password` - -- Send password reset email -- Response: `{ success, reset_link }` - -### 7.3 Shared Boards Endpoints - -#### POST `/api/v1/boards/{board_id}/share` - -- Share board with users -- Body: `{ user_ids[], permission: 'read' | 'write' | 'admin' }` -- Response: `{ success, shared_with[] }` - -#### GET `/api/v1/boards/{board_id}/shares` - -- Get board sharing information -- Response: `{ shares[] }` - -#### DELETE `/api/v1/boards/{board_id}/share/{user_id}` - -- Remove board sharing -- Response: `{ success }` - -### 7.4 Modified Endpoints - -All existing endpoints will be modified to: - -1. Require authentication (except setup/login) -2. Filter data by current user (unless admin viewing all) -3. Enforce permissions (e.g., model management requires admin) -4. Include user context in operations - -Example modifications: - -- `GET /api/v1/boards` → Returns only user's boards + shared boards -- `POST /api/v1/session/queue` → Associates queue item with current user -- `GET /api/v1/queue` → Returns all items for admin, only user's items for regular users - -## 8. Frontend Changes - -### 8.1 New Components - -#### LoginPage - -- Email/password form -- "Remember me" checkbox -- Login button -- Forgot password link (future) -- Branding and welcome message - -#### AdministratorSetup - -- Modal dialog (cannot be dismissed) -- Administrator account creation form -- Password strength indicator -- Terms/welcome message - -#### UserManagementPage (Admin only) - -- User list table -- Add user button -- User actions (edit, delete, reset password) -- Search and filter -- Role toggle - -#### UserProfilePage - -- Display user information -- Change password form -- UI preferences -- Account details - -#### BoardSharingDialog - -- User picker/search -- Permission selector -- Share button -- Current shares list - -### 8.2 Modified Components - -#### App Root - -- Add authentication check -- Redirect to login if not authenticated -- Handle session expiration -- Add global error boundary for auth errors - -#### Navigation/Header - -- Add user menu with logout -- Display current user name -- Admin indicator badge - -#### ModelManagerTab - -- Hide/disable for non-admin users -- Show "Admin only" message - -#### QueuePanel - -- Filter by current user (for non-admin) -- Show all with user indicators (for admin) -- Disable actions on other users' items (for non-admin) - -#### BoardsPanel - -- Show personal boards section -- Show shared boards section -- Add sharing controls to board actions - -### 8.3 State Management - -New Redux slices/zustand stores: - -- `authSlice`: Current user, authentication status, token -- `usersSlice`: User list for admin interface -- `sharingSlice`: Board sharing state - -Updated slices: - -- `boardsSlice`: Include shared boards, ownership info -- `queueSlice`: Include user filtering -- `workflowsSlice`: Include public/private status - -## 9. Configuration - -### 9.1 New Config Options - -Add to `InvokeAIAppConfig`: - -```python -# Authentication -auth_enabled: bool = True # Enable/disable multi-user auth -session_expiry_hours: int = 24 # Default session expiration -session_expiry_hours_remember: int = 168 # "Remember me" expiration (7 days) -password_min_length: int = 8 # Minimum password length -require_strong_passwords: bool = True # Enforce password complexity - -# Session tracking -enable_server_side_sessions: bool = False # Optional server-side session tracking - -# Audit logging -audit_log_auth_events: bool = True # Log authentication events -audit_log_admin_actions: bool = True # Log administrative actions - -# Email (optional - for invitations and password reset) -email_enabled: bool = False -smtp_host: str = "" -smtp_port: int = 587 -smtp_username: str = "" -smtp_password: str = "" -smtp_from_address: str = "" -smtp_from_name: str = "InvokeAI" - -# Initial admin (stored as hash) -admin_email: Optional[str] = None -admin_password_hash: Optional[str] = None -``` - -### 9.2 Backward Compatibility - -- If `auth_enabled = False`, system runs in legacy single-user mode -- All data belongs to implicit "system" user -- No authentication required -- Smooth upgrade path for existing installations - -## 10. Security Considerations - -### 10.1 Password Security - -- Never store passwords in plain text -- Use bcrypt or Argon2id for password hashing -- Implement proper salt generation -- Enforce password complexity requirements -- Implement rate limiting on login attempts -- Consider password breach checking (Have I Been Pwned API) - -### 10.2 Session Security - -- Use cryptographically secure random tokens -- Implement token rotation -- Set appropriate cookie flags (HttpOnly, Secure, SameSite) -- Implement session timeout and renewal -- Invalidate sessions on logout -- Clean up expired sessions periodically - -### 10.3 Authorization - -- Always verify user identity from session token (never trust client) -- Check permissions on every API call -- Implement principle of least privilege -- Validate user ownership of resources before operations -- Implement proper error messages (avoid information leakage) - -### 10.4 Data Isolation - -- Strict separation of user data in database queries -- Prevent SQL injection via parameterized queries -- Validate all user inputs -- Implement proper access control checks -- Audit trail for sensitive operations - -### 10.5 API Security - -- Implement rate limiting on sensitive endpoints -- Use HTTPS in production (enforce via config) -- Implement CSRF protection -- Validate and sanitize all inputs -- Implement proper CORS configuration -- Add security headers (CSP, X-Frame-Options, etc.) - -### 10.6 Deployment Security - -- Document secure deployment practices -- Recommend reverse proxy configuration (nginx, Apache) -- Provide example configurations for HTTPS -- Document firewall requirements -- Recommend network isolation strategies - -## 11. Email Integration (Optional) - -:::note -Email/SMTP configuration is optional. Many administrators will not have ready access to an outgoing SMTP server. When email is not configured, the system provides fallback mechanisms by displaying setup links directly in the admin UI. -::: - -### 11.1 Email Templates - -#### User Invitation - -```text -Subject: You've been invited to InvokeAI - -Hello, - -You've been invited to join InvokeAI by [Administrator Name]. - -Click the link below to set up your account: -[Setup Link] - -This link expires in 7 days. - ---- -InvokeAI -``` - -#### Password Reset - -```text -Subject: Reset your InvokeAI password - -Hello [User Name], - -A password reset was requested for your account. - -Click the link below to reset your password: -[Reset Link] - -This link expires in 24 hours. - -If you didn't request this, please ignore this email. - ---- -InvokeAI -``` - -### 11.2 Email Service - -- Support SMTP configuration -- Use secure connection (TLS) -- Handle email failures gracefully -- Implement email queue for reliability -- Log email activities (without sensitive data) -- Provide fallback for no-email deployments (show links in admin UI) - -## 12. Testing Requirements - -### 12.1 Unit Tests - -- Authentication service (password hashing, validation) -- Authorization checks -- Token generation and validation -- User management operations -- Shared board permissions -- Data isolation queries - -### 12.2 Integration Tests - -- Complete authentication flows -- User creation and invitation -- Password reset flow -- Multi-user data isolation -- Shared board access -- Session management -- Admin operations - -### 12.3 Security Tests - -- SQL injection prevention -- XSS prevention -- CSRF protection -- Session hijacking prevention -- Brute force protection -- Authorization bypass attempts - -### 12.4 Performance Tests - -- Authentication overhead -- Query performance with user filters -- Concurrent user sessions -- Database scalability with many users - -## 13. Documentation Requirements - -### 13.1 User Documentation - -- Getting started with multi-user InvokeAI -- Login and account management -- Using shared boards -- Understanding permissions -- Troubleshooting authentication issues - -### 13.2 Administrator Documentation - -- Setting up multi-user InvokeAI -- User management guide -- Creating and managing shared boards -- Email configuration -- Security best practices -- Backup and restore with user data - -### 13.3 Developer Documentation - -- Authentication architecture -- API authentication requirements -- Adding new multi-user features -- Database schema changes -- Testing multi-user features - -### 13.4 Migration Documentation - -- Upgrading from single-user to multi-user -- Data migration strategies -- Rollback procedures -- Common issues and solutions - -## 14. Future Enhancements - -### 14.1 Phase 2 Features - -- **OAuth2/OpenID Connect integration** (deferred from initial release to keep scope manageable) -- Two-factor authentication -- API keys for programmatic access -- Enhanced team/group management -- Advanced permission system (roles and capabilities) - -### 14.2 Phase 3 Features - -- SSO integration (SAML, LDAP) -- User quotas and limits -- Resource usage tracking -- Advanced collaboration features -- Workflow template library with permissions -- Model access controls per user/group - -## 15. Success Metrics - -### 15.1 Functionality Metrics - -- Successful user authentication rate -- Zero unauthorized data access incidents -- All tests passing (unit, integration, security) -- API response time within acceptable limits - -### 15.2 Usability Metrics - -- User setup completion time < 2 minutes -- Login time < 2 seconds -- Clear error messages for all auth failures -- Positive user feedback on multi-user features - -### 15.3 Security Metrics - -- No critical security vulnerabilities identified -- CodeQL scan passes -- Penetration testing completed -- Security best practices followed - -## 16. Risks and Mitigations - -### 16.1 Technical Risks - -| Risk | Impact | Probability | Mitigation | -|------|--------|-------------|------------| -| Performance degradation with user filtering | Medium | Low | Index optimization, query caching | -| Database migration failures | High | Low | Thorough testing, rollback procedures | -| Session management complexity | Medium | Medium | Use proven libraries (PyJWT), extensive testing | -| Auth bypass vulnerabilities | High | Low | Security review, penetration testing | - -### 16.2 UX Risks - -| Risk | Impact | Probability | Mitigation | -|------|--------|-------------|------------| -| Confusion in migration for existing users | Medium | High | Clear documentation, migration wizard | -| Friction from additional login step | Low | High | Remember me option, long session timeout | -| Complexity of admin interface | Medium | Medium | Intuitive UI design, user testing | - -### 16.3 Operational Risks - -| Risk | Impact | Probability | Mitigation | -|------|--------|-------------|------------| -| Email delivery failures | Low | Medium | Show links in UI, document manual methods | -| Lost admin password | High | Low | Document recovery procedure, config reset | -| User data conflicts in migration | Medium | Low | Data validation, backup requirements | - -## 17. Implementation Phases - -### Phase 1: Foundation (Weeks 1-2) - -- Database schema design and migration -- Basic authentication service -- Password hashing and validation -- Session management - -### Phase 2: Backend API (Weeks 3-4) - -- Authentication endpoints -- User management endpoints -- Authorization middleware -- Update existing endpoints with auth - -### Phase 3: Frontend Auth (Weeks 5-6) - -- Login page and flow -- Administrator setup -- Session management -- Auth state management - -### Phase 4: Multi-tenancy (Weeks 7-9) - -- User isolation in all services -- Shared boards implementation -- Queue permission filtering -- Workflow public/private - -### Phase 5: Admin Interface (Weeks 10-11) - -- User management UI -- Board sharing UI -- Admin-specific features -- User profile page - -### Phase 6: Testing & Polish (Weeks 12-13) - -- Comprehensive testing -- Security audit -- Performance optimization -- Documentation -- Bug fixes - -### Phase 7: Beta & Release (Week 14+) - -- Beta testing with selected users -- Feedback incorporation -- Final testing -- Release preparation -- Documentation finalization - -## 18. Acceptance Criteria - -- [ ] Administrator can set up initial account on first launch -- [ ] Users can log in with email and password -- [ ] Users can change their password -- [ ] Administrators can create, edit, and delete users -- [ ] User data is properly isolated (boards, images, workflows) -- [ ] Shared boards work correctly with permissions -- [ ] Non-admin users cannot access model management -- [ ] Queue filtering works correctly for users and admins -- [ ] Session management works correctly (expiry, renewal, logout) -- [ ] All security tests pass -- [ ] API documentation is updated -- [ ] User and admin documentation is complete -- [ ] Migration from single-user works smoothly -- [ ] Performance is acceptable with multiple concurrent users -- [ ] Backward compatibility mode works (auth disabled) - -## 19. Design Decisions - -The following design decisions have been approved for implementation: - -1. **OAuth2 Priority**: OAuth2/OpenID Connect integration will be a **future enhancement**. The initial release will focus on username/password authentication to keep scope manageable. - -2. **Email Requirement**: Email/SMTP configuration is **optional**. Many administrators will not have ready access to an outgoing SMTP server. The system will provide fallback mechanisms (showing setup links directly in the admin UI) when email is not configured. - -3. **Data Migration**: During migration from single-user to multi-user mode, the administrator will be given the **option to specify an arbitrary user account** to hold legacy data. The admin account can be used for this purpose if the administrator wishes. - -4. **API Compatibility**: Authentication will be **required on all APIs**, but authentication will not be required if multi-user support is disabled (backward compatibility mode with `auth_enabled: false`). - -5. **Session Storage**: The system will use **JWT tokens with optional server-side session tracking**. This provides scalability while allowing administrators to enable server-side tracking if needed. - -6. **Audit Logging**: The system will **log authentication events and admin actions**. This provides accountability and security monitoring for critical operations. - -## 20. Conclusion - -This specification provides a comprehensive blueprint for implementing multi-user support in InvokeAI. The design prioritizes: - -- **Security**: Proper authentication, authorization, and data isolation -- **Usability**: Intuitive UI, smooth migration, minimal friction -- **Scalability**: Efficient database design, performant queries -- **Maintainability**: Clean architecture, comprehensive testing -- **Flexibility**: Future enhancement paths, optional features - -The phased implementation approach allows for iterative development and testing, while the detailed specifications ensure all stakeholders have clear expectations of the final system. diff --git a/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx index 6d33414ab38..6c24e2231d0 100644 --- a/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx +++ b/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx @@ -4,6 +4,7 @@ description: How to use InvokeAI in multi-user mode as an end user. sidebar: order: 3 --- +import { Steps } from '@astrojs/starlight/components' ## Overview @@ -46,6 +47,7 @@ If you're the first person to access a fresh InvokeAI installation in multi-user Now: + 1. Enter your email address (this will be your login name) 2. Create a display name (this will be the name other users see) 3. Choose a strong password that meets the requirements: @@ -55,6 +57,7 @@ Now: - Contains numbers 4. Confirm your password 5. Click **Create Administrator Account** + You'll now be taken to a login screen and can enter the credentials you just created. @@ -74,43 +77,12 @@ This will take you to the User Management screen... The User Management screen also allows you to: + 1. Temporarily change a user's status to Inactive, preventing them from logging in to Invoke. 2. Edit a user (by clicking on the pencil icon) to change the user's display name or password. 3. Permanently delete a user. 4. Grant a user Administrator privileges. - -### Command-line User Management Scripts - -Administrators can also use a series of command-line scripts to add, modify, or delete users. If you use the launcher, click the ">" icon to enter the command-line interface. Otherwise, if you are a native command-line user, activate the InvokeAI environment from your terminal. - -The commands are named: - -- **invoke-useradd** — add a user -- **invoke-usermod** — modify a user -- **invoke-userdel** — delete a user -- **invoke-userlist** — list all users - -Pass the `--help` argument to get the usage of each script. For example: - -```bash -> invoke-useradd --help -usage: invoke-useradd [-h] [--root ROOT] [--email EMAIL] [--password PASSWORD] [--name NAME] [--admin] - -Add a user to the InvokeAI database - -options: - -h, --help show this help message and exit - --root ROOT, -r ROOT Path to the InvokeAI root directory. If omitted, the root is resolved in this order: the $INVOKEAI_ROOT environment - variable, the active virtual environment's parent directory, or $HOME/invokeai. - --email EMAIL, -e EMAIL - User email address - --password PASSWORD, -p PASSWORD - User password - --name NAME, -n NAME User display name (optional) - --admin, -a Make user an administrator - -If no arguments are provided, the script will run in interactive mode. -``` + --- @@ -281,9 +253,11 @@ These settings are stored per-user and won't affect other users. **Possible Causes:** + 1. **Filter Applied:** Check if a filter is hiding content 2. **Wrong User:** Ensure you're logged in with the correct account 3. **Archived Board:** Check the "Show Archived" option + **Solution:** @@ -342,7 +316,7 @@ Yes. You can mark any workflow as shared (public), which makes it visible to all ### Can I use the API with multi-user mode? -Yes, but you'll need to authenticate with a JWT token. See the [API Guide](/features/multi-user/api-guide/) for details. +Yes, but you'll need to authenticate with a JWT token. See the [API Guide](./api-guide/) for details. ### What happens if I forget my password? @@ -385,6 +359,5 @@ When reporting an issue, include: ## Additional Resources -- [Administrator Guide](/features/multi-user/admin-guide/) — For administrators managing users and the system -- [API Guide](/features/multi-user/api-guide/) — For developers using the InvokeAI API -- [Multi-User Specification](/features/multi-user/specification/) — Technical details about the feature +- [Administrator Guide](./admin-guide/) — For administrators managing users and the system +- [API Guide](./api-guide/) — For developers using the InvokeAI API