+ )}
+ );
+}
+```
+
+#### Route Guard
+
+**File:** `surfsense_web/components/RouteGuard.tsx`
+
+**Changes:**
+- Extended `routeKey` type to include `"registration"`
+- Added special case handling for registration toggle
+
+```typescript
+interface RouteGuardProps {
+ children: React.ReactNode;
+ routeKey: "pricing" | "docs" | "contact" | "terms" | "privacy" | "registration";
+}
+
+export function RouteGuard({ children, routeKey }: RouteGuardProps) {
+ const { config, isLoading } = useSiteConfig();
+ const router = useRouter();
+
+ // Special case: registration uses disable_registration instead of disable_registration_route
+ const disableKey = routeKey === "registration"
+ ? "disable_registration"
+ : (`disable_${routeKey}_route` as keyof typeof config);
+ const isDisabled = config[disableKey as keyof typeof config];
+
+ if (isDisabled) {
+ router.replace("/404");
+ return null;
+ }
+
+ return <>{children}>;
+}
+```
+
+#### Register Page
+
+**File:** `surfsense_web/app/(home)/register/page.tsx`
+
+**Changes:**
+- Imported `RouteGuard` component
+- Wrapped entire page content with ``
+
+```typescript
+import { RouteGuard } from "@/components/RouteGuard";
+
+export default function RegisterPage() {
+ return (
+
+ {/* ... registration form content ... */}
+
+ );
+}
+```
+
+**Behavior:** When `disable_registration` is true, accessing `/register` redirects to `/404`
+
+#### Admin Panel
+
+**File:** `surfsense_web/app/dashboard/site-settings/page.tsx`
+
+**Changes:**
+- Added `disable_registration: boolean` to `SiteConfigForm` interface
+- Added default value `disable_registration: false` to form state
+- Added field to `useEffect` that loads config from API
+- Added new "Registration Control" section in UI
+
+```typescript
+interface SiteConfigForm {
+ // ... existing fields ...
+ disable_registration: boolean;
+}
+
+{/* Registration Control Section */}
+
+
+ Registration Control
+
+
+ Control user registration availability. When disabled, the Sign Up link will be hidden
+ and the registration page will show a 404 error. The backend will also block registration
+ requests with a 403 error.
+
+
+ handleToggle("disable_registration")}
+ />
+
+
+```
+
+### Behavior Summary
+
+#### When Registration is ENABLED (default: `disable_registration = false`):
+
+1. **Login Page (`/login`):**
+ - Shows "Don't have an account? Sign up" link
+ - Link navigates to `/register`
+
+2. **Register Page (`/register`):**
+ - Page loads normally
+ - Registration form accepts submissions
+ - Backend POST `/auth/register` succeeds (200)
+
+3. **Admin Panel (`/dashboard/site-settings`):**
+ - "Disable Registration" toggle is OFF
+ - Description explains what happens when enabled
+
+#### When Registration is DISABLED (`disable_registration = true`):
+
+1. **Login Page (`/login`):**
+ - "Don't have an account? Sign up" link is HIDDEN
+ - No visual indication of registration functionality
+
+2. **Register Page (`/register`):**
+ - RouteGuard redirects to `/404` (route disabled)
+ - Page shows standard 404 error
+
+3. **Backend API (`POST /auth/register`):**
+ - Returns `403 Forbidden`
+ - Error message: "Registration is currently disabled. Please contact the administrator if you need access."
+
+4. **Admin Panel (`/dashboard/site-settings`):**
+ - "Disable Registration" toggle is ON (orange)
+ - Admins can toggle back to enable registration
+
+### Security Considerations
+
+1. **Layered Protection:**
+ - Frontend hides UI elements (Sign Up link)
+ - Frontend route guard blocks page access
+ - Backend API validates and blocks requests
+ - Defense in depth approach prevents bypass attempts
+
+2. **Database-Driven:**
+ - Configuration stored in PostgreSQL (singleton row with id=1)
+ - Atomic updates via SQLAlchemy transactions
+ - Consistent state across all application instances
+
+3. **Admin-Only Access:**
+ - Only superusers can access `/dashboard/site-settings`
+ - JWT authentication required for PUT `/api/v1/site-config`
+ - Public GET `/api/v1/site-config/public` is read-only
+
+4. **Graceful Error Messages:**
+ - User-friendly 403 message explains registration is disabled
+ - Directs users to contact administrator
+ - No technical details exposed
+
+### Deployment Steps
+
+1. ✅ **Pull latest code:** `git pull origin nightly`
+2. ✅ **Apply migration:** `alembic upgrade head` (migration #39)
+3. ✅ **Rebuild frontend:** `pnpm build`
+4. ✅ **Restart services:** `systemctl restart surfsense.service surfsense-frontend.service surfsense-celery.service surfsense-celery-beat.service`
+5. ✅ **Verify deployment:** All services active
+
+**Deployment Date:** 2025-11-18
+**Deployment Location:** https://ai.kapteinis.lv
+**VPS:** 46.62.230.195
+
+### Migration Notes
+
+#### Issue Encountered: Revision ID Too Long
+
+**Problem:** Alembic `alembic_version` table has `varchar(32)` constraint on `version_num` column. Original revision ID `'39_add_disable_registration_to_site_configuration'` (50 characters) exceeded limit.
+
+**Error:**
+```
+sqlalchemy.exc.DBAPIError: :
+value too long for type character varying(32)
+[SQL: UPDATE alembic_version SET version_num='39_add_disable_registration_to_site_configuration'
+WHERE alembic_version.version_num = '38_add_site_configuration_table']
+```
+
+**Solution:** Shortened revision ID using sed on VPS:
+```bash
+sed -i "s/revision: str = '39_add_disable_registration_to_site_configuration'/revision: str = '39'/g" \
+ alembic/versions/39_add_disable_registration_to_site_configuration.py
+```
+
+**Final Revision ID:** `'39'` (2 characters, fits varchar(32))
+
+**Note:** Migration #38 uses long ID `'38_add_site_configuration_table'` but succeeded before (possibly database was modified to allow longer IDs, or varchar constraint was later added).
+
+### Testing Checklist
+
+**Default State (Registration Enabled):**
+- [x] Login page shows "Sign up" link
+- [x] `/register` page loads successfully
+- [x] Registration form accepts submissions
+- [x] Backend POST `/auth/register` returns 200/201
+- [x] Admin panel toggle is OFF (not orange)
+
+**Toggled State (Registration Disabled):**
+- [ ] Admin can toggle "Disable Registration" ON in `/dashboard/site-settings`
+- [ ] Changes saved successfully (toast notification)
+- [ ] Login page "Sign up" link disappears after refetch
+- [ ] `/register` page shows 404 error
+- [ ] Backend POST `/auth/register` returns 403 with message
+- [ ] Admin panel toggle is ON (orange highlight)
+
+**Toggle Back (Re-enable Registration):**
+- [ ] Admin can toggle "Disable Registration" OFF
+- [ ] Login page "Sign up" link reappears
+- [ ] `/register` page loads normally
+- [ ] Backend POST `/auth/register` accepts submissions again
+
+### Files Modified
+
+**Backend (4 files + 1 migration):**
+```
+surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py # NEW
+surfsense_backend/app/db.py # +3 lines
+surfsense_backend/app/app.py # +20 lines
+surfsense_backend/app/schemas/site_configuration.py # +4 lines
+```
+
+**Frontend (5 files):**
+```
+surfsense_web/contexts/SiteConfigContext.tsx # +4 lines
+surfsense_web/app/(home)/login/LocalLoginForm.tsx # +11 lines, -15 lines (conditional)
+surfsense_web/app/(home)/register/page.tsx # +2 lines (import, wrapper)
+surfsense_web/components/RouteGuard.tsx # +10 lines (registration support)
+surfsense_web/app/dashboard/site-settings/page.tsx # +25 lines (new section)
+```
+
+**Total Changes:** 9 files modified, 1 file created, ~80 lines added
+
+### Commit Information
+
+**Commit Hash:** `b8bb74e`
+**Branch:** `nightly`
+**Message:**
+```
+Add disable_registration toggle to site configuration
+
+Implemented comprehensive registration control system that allows administrators
+to disable new user registrations through the admin panel. This provides better
+control over user access and supports closed registration scenarios.
+
+Backend changes:
+- Added disable_registration field to SiteConfiguration model (db.py)
+- Created migration #39 to add disable_registration column with default false
+- Updated SiteConfigurationBase, Update, and Read schemas
+- Enhanced registration_allowed() dependency to check database toggle
+- Returns 403 with clear message when registration is disabled
+
+Frontend changes:
+- Added disable_registration to SiteConfig interface and default config
+- Updated LocalLoginForm to conditionally hide "Sign up" link when disabled
+- Added RouteGuard support for "registration" route key
+- Protected /register page with RouteGuard (shows 404 when disabled)
+- Added "Registration Control" section to admin site-settings page
+
+🤖 Generated with [Claude Code](https://claude.com/claude-code)
+
+Co-Authored-By: Claude
+```
+
+### Use Cases
+
+1. **Private Deployments:**
+ - Organization wants to control who can create accounts
+ - Only invite approved users, disable public registration
+ - Admin manually creates accounts for new employees
+
+2. **Capacity Management:**
+ - Server approaching resource limits
+ - Temporarily disable registration until scaling completed
+ - Re-enable when infrastructure ready
+
+3. **Closed Beta:**
+ - Limited initial user base for testing
+ - Disable registration after target number reached
+ - Re-enable for public launch
+
+4. **Security Incident Response:**
+ - Spam/abuse accounts being created
+ - Quickly disable registration to stop attack
+ - Investigate and re-enable when resolved
+
+### Troubleshooting
+
+#### Issue: Toggle doesn't save in admin panel
+
+**Symptoms:** Clicking toggle works, but changes don't persist after refresh
+
+**Diagnosis:**
+1. Check browser console for API errors
+2. Verify JWT token is valid (`localStorage.getItem('access_token')`)
+3. Check backend logs for 401/403 errors
+4. Confirm user is superuser (`is_superuser = true`)
+
+**Solution:** Ensure logged-in user has superuser privileges
+
+#### Issue: Sign Up link still visible after disabling registration
+
+**Symptoms:** Login page shows Sign Up link despite toggle being ON
+
+**Diagnosis:**
+1. Check if frontend context refetched after save
+2. Inspect SiteConfigContext state in React DevTools
+3. Verify GET `/api/v1/site-config/public` returns `disable_registration: true`
+
+**Solution:** Hard refresh browser (Ctrl+Shift+R) or wait for automatic refetch
+
+#### Issue: /register page still loads instead of 404
+
+**Symptoms:** Accessing `/register` shows form instead of 404 error
+
+**Diagnosis:**
+1. Verify RouteGuard is wrapping RegisterPage component
+2. Check SiteConfigContext has correct `disable_registration` value
+3. Confirm RouteGuard logic handles "registration" route key
+
+**Solution:** Verify frontend build includes latest changes, restart frontend service
+
+#### Issue: Backend still accepts registrations despite toggle
+
+**Symptoms:** POST `/auth/register` returns 200 instead of 403
+
+**Diagnosis:**
+1. Check database: `SELECT disable_registration FROM site_configuration WHERE id = 1;`
+2. Verify `registration_allowed()` function checks database
+3. Confirm dependency is applied to register router
+
+**Solution:** Restart backend service to reload code changes
+
+### Future Enhancements
+
+**Potential additions (not yet implemented):**
+- [ ] Registration whitelist (allow specific email domains)
+- [ ] Invitation-based registration (one-time tokens)
+- [ ] Registration queue/approval workflow
+- [ ] Auto-disable after N registrations
+- [ ] Custom messaging when registration disabled
+- [ ] Email notification to admins when disabled
+- [ ] Audit log of registration toggle changes
+- [ ] Rate limiting for registration attempts
+
+### Support
+
+For questions or issues related to registration control:
+- Review admin panel documentation: `/dashboard/site-settings`
+- Check backend registration logic: `surfsense_backend/app/app.py:registration_allowed()`
+- Examine frontend RouteGuard: `surfsense_web/components/RouteGuard.tsx`
+- Verify SiteConfig context: `surfsense_web/contexts/SiteConfigContext.tsx`
+- Migration file: `surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py`
+
+---
+
## Conclusion
**Overall Security Posture:** ✅ **STRONG**
diff --git a/surfsense_backend/OLLAMA_MEMORY_OPTIMIZATION.md b/surfsense_backend/OLLAMA_MEMORY_OPTIMIZATION.md
new file mode 100644
index 000000000..2818ac9b0
--- /dev/null
+++ b/surfsense_backend/OLLAMA_MEMORY_OPTIMIZATION.md
@@ -0,0 +1,383 @@
+# Ollama Memory Optimization Guide for SurfSense
+
+This guide helps you optimize Ollama configuration for memory-constrained VPS environments.
+
+## Quick Recommendations
+
+### For VPS with 4GB RAM or less
+
+Use **highly quantized small models**:
+
+```yaml
+# global_llm_config.yaml
+global_llm_configs:
+ - id: -1
+ name: "Ollama Qwen2.5 3B (Q4)"
+ provider: "OLLAMA"
+ model_name: "qwen2.5:3b-instruct-q4_K_M"
+ api_key: ""
+ api_base: "http://localhost:11434"
+ language: "English"
+ litellm_params:
+ temperature: 0.7
+ max_tokens: 2048
+ num_ctx: 2048 # Reduced context window
+```
+
+**Recommended models:**
+- `qwen2.5:3b-instruct-q4_K_M` (~2GB RAM)
+- `gemma2:2b-instruct-q4_K_M` (~1.5GB RAM)
+- `phi3:mini-4k-instruct-q4_K_M` (~2GB RAM)
+
+### For VPS with 8GB RAM
+
+Use **medium-sized quantized models**:
+
+```yaml
+global_llm_configs:
+ - id: -1
+ name: "Ollama Llama 3.1 8B (Q4)"
+ provider: "OLLAMA"
+ model_name: "llama3.1:8b-instruct-q4_K_M"
+ api_key: ""
+ api_base: "http://localhost:11434"
+ language: "English"
+ litellm_params:
+ temperature: 0.7
+ max_tokens: 4096
+ num_ctx: 4096 # Moderate context window
+```
+
+**Recommended models:**
+- `llama3.1:8b-instruct-q4_K_M` (~5GB RAM)
+- `mistral:7b-instruct-q4_K_M` (~4GB RAM)
+- `qwen2.5:7b-instruct-q4_K_M` (~4.5GB RAM)
+
+### For VPS with 16GB+ RAM
+
+Use **larger models with better quantization**:
+
+```yaml
+global_llm_configs:
+ - id: -1
+ name: "Ollama Llama 3.1 8B (Q8)"
+ provider: "OLLAMA"
+ model_name: "llama3.1:8b-instruct-q8_0"
+ api_key: ""
+ api_base: "http://localhost:11434"
+ language: "English"
+ litellm_params:
+ temperature: 0.7
+ max_tokens: 8192
+ num_ctx: 8192
+```
+
+---
+
+## Memory Optimization Techniques
+
+### 1. Reduce Context Window Size
+
+The context window (`num_ctx`) directly impacts memory usage. Reduce it for memory savings:
+
+```yaml
+litellm_params:
+ num_ctx: 2048 # Default is often 4096 or 8192
+```
+
+**Memory impact:**
+- 4096 context: ~2x memory of 2048
+- 8192 context: ~4x memory of 2048
+
+### 2. Use Quantized Models
+
+Quantization reduces model precision to save memory:
+
+| Quantization | Quality | Memory | Recommendation |
+|-------------|---------|--------|----------------|
+| Q8_0 | Highest | 8-bit | Best quality if RAM allows |
+| Q5_K_M | High | 5-bit | Good balance |
+| Q4_K_M | Medium | 4-bit | **Recommended for VPS** |
+| Q3_K_M | Lower | 3-bit | Maximum memory savings |
+| Q2_K | Lowest | 2-bit | Not recommended |
+
+### 3. Configure Ollama Environment Variables
+
+Create or edit `/etc/systemd/system/ollama.service.d/override.conf`:
+
+```ini
+[Service]
+Environment="OLLAMA_NUM_PARALLEL=1"
+Environment="OLLAMA_MAX_LOADED_MODELS=1"
+Environment="OLLAMA_KEEP_ALIVE=5m"
+```
+
+Then reload:
+```bash
+sudo systemctl daemon-reload
+sudo systemctl restart ollama
+```
+
+**Environment variables explained:**
+- `OLLAMA_NUM_PARALLEL=1`: Process one request at a time (saves memory)
+- `OLLAMA_MAX_LOADED_MODELS=1`: Keep only one model in memory
+- `OLLAMA_KEEP_ALIVE=5m`: Unload model after 5 minutes of inactivity
+
+### 4. Add Swap Space
+
+If you're running low on memory, add swap:
+
+```bash
+# Create 4GB swap file
+sudo fallocate -l 4G /swapfile
+sudo chmod 600 /swapfile
+sudo mkswap /swapfile
+sudo swapon /swapfile
+
+# Make permanent
+echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
+
+# Optimize swappiness
+echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
+sudo sysctl -p
+```
+
+---
+
+## Model Selection Guide
+
+### Best Models for Memory-Constrained Environments
+
+#### Lightweight (1-3GB RAM)
+```bash
+ollama pull qwen2.5:3b-instruct-q4_K_M
+ollama pull gemma2:2b-instruct-q4_K_M
+ollama pull phi3:mini-4k-instruct-q4_K_M
+```
+
+#### Medium (4-6GB RAM)
+```bash
+ollama pull llama3.1:8b-instruct-q4_K_M
+ollama pull mistral:7b-instruct-q4_K_M
+ollama pull qwen2.5:7b-instruct-q4_K_M
+```
+
+#### Quality (8-12GB RAM)
+```bash
+ollama pull llama3.1:8b-instruct-q8_0
+ollama pull qwen2.5:14b-instruct-q4_K_M
+```
+
+### Check Available Models
+
+```bash
+# List available models
+ollama list
+
+# Check model info (including memory requirements)
+ollama show llama3.1:8b-instruct-q4_K_M
+```
+
+---
+
+## SurfSense Configuration
+
+### Example: Memory-Optimized Setup
+
+Create `surfsense_backend/app/config/global_llm_config.yaml`:
+
+```yaml
+# Memory-optimized Ollama configuration for VPS
+global_llm_configs:
+ # Long Context LLM - larger context, fewer tokens
+ - id: -1
+ name: "Ollama Long Context (Local)"
+ provider: "OLLAMA"
+ model_name: "qwen2.5:7b-instruct-q4_K_M"
+ api_key: ""
+ api_base: "http://localhost:11434"
+ language: "English"
+ litellm_params:
+ temperature: 0.7
+ max_tokens: 2048
+ num_ctx: 4096
+
+ # Fast LLM - smaller model, lower latency
+ - id: -2
+ name: "Ollama Fast (Local)"
+ provider: "OLLAMA"
+ model_name: "qwen2.5:3b-instruct-q4_K_M"
+ api_key: ""
+ api_base: "http://localhost:11434"
+ language: "English"
+ litellm_params:
+ temperature: 0.5
+ max_tokens: 1024
+ num_ctx: 2048
+
+ # Strategic LLM - same as long context
+ - id: -3
+ name: "Ollama Strategic (Local)"
+ provider: "OLLAMA"
+ model_name: "qwen2.5:7b-instruct-q4_K_M"
+ api_key: ""
+ api_base: "http://localhost:11434"
+ language: "English"
+ litellm_params:
+ temperature: 0.3
+ max_tokens: 2048
+ num_ctx: 4096
+```
+
+### Hybrid Configuration (Ollama + Cloud)
+
+For best results with limited RAM, use Ollama for fast queries and cloud APIs for complex tasks:
+
+```yaml
+global_llm_configs:
+ # Complex tasks - Cloud API (no local memory needed)
+ - id: -1
+ name: "GPT-4 Turbo (Long Context)"
+ provider: "OPENAI"
+ model_name: "gpt-4-turbo"
+ api_key: "sk-your-api-key"
+ api_base: ""
+ language: "English"
+ litellm_params:
+ temperature: 0.7
+ max_tokens: 4000
+
+ # Quick queries - Local Ollama (fast, private)
+ - id: -2
+ name: "Ollama Fast (Local)"
+ provider: "OLLAMA"
+ model_name: "qwen2.5:3b-instruct-q4_K_M"
+ api_key: ""
+ api_base: "http://localhost:11434"
+ language: "English"
+ litellm_params:
+ temperature: 0.5
+ max_tokens: 1024
+ num_ctx: 2048
+
+ # Strategic - Cloud API
+ - id: -3
+ name: "GPT-4 (Strategic)"
+ provider: "OPENAI"
+ model_name: "gpt-4"
+ api_key: "sk-your-api-key"
+ api_base: ""
+ language: "English"
+ litellm_params:
+ temperature: 0.3
+ max_tokens: 2000
+```
+
+---
+
+## Troubleshooting
+
+### Error: "Out of memory"
+
+1. **Switch to a smaller model**:
+ ```bash
+ ollama rm llama3.1:8b
+ ollama pull qwen2.5:3b-instruct-q4_K_M
+ ```
+
+2. **Reduce context window** in your config:
+ ```yaml
+ litellm_params:
+ num_ctx: 2048
+ ```
+
+3. **Free up memory**:
+ ```bash
+ # Stop unnecessary services
+ sudo systemctl stop ollama
+ free -h
+ sudo systemctl start ollama
+ ```
+
+4. **Check what's using memory**:
+ ```bash
+ htop
+ # or
+ ps aux --sort=-%mem | head -20
+ ```
+
+### Error: "Model not found"
+
+Pull the model first:
+```bash
+ollama pull qwen2.5:3b-instruct-q4_K_M
+```
+
+### Slow responses
+
+1. Ensure only one model is loaded:
+ ```bash
+ export OLLAMA_MAX_LOADED_MODELS=1
+ ```
+
+2. Add more swap space
+
+3. Use a faster/smaller model
+
+### Connection refused
+
+Ensure Ollama is running:
+```bash
+sudo systemctl status ollama
+sudo systemctl start ollama
+```
+
+Check it's listening:
+```bash
+curl http://localhost:11434/api/tags
+```
+
+---
+
+## Monitoring Memory Usage
+
+### Check Ollama memory usage
+
+```bash
+# Watch memory in real-time
+watch -n 1 'ps aux | grep ollama'
+
+# Check system memory
+free -h
+
+# Detailed memory info
+cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached"
+```
+
+### Log analysis
+
+```bash
+# Ollama logs
+sudo journalctl -u ollama -f
+
+# System memory pressure
+dmesg | grep -i "out of memory"
+```
+
+---
+
+## Summary
+
+For a typical VPS with 4-8GB RAM running SurfSense:
+
+1. **Use Q4_K_M quantized models** (best memory/quality balance)
+2. **Start with smaller models** (3B-7B parameters)
+3. **Reduce context window** to 2048-4096
+4. **Configure Ollama** to load only one model at a time
+5. **Add swap space** as a safety net
+6. **Consider hybrid setup** with cloud APIs for complex tasks
+
+**Recommended starting configuration:**
+- Model: `qwen2.5:3b-instruct-q4_K_M` or `qwen2.5:7b-instruct-q4_K_M`
+- Context: 2048-4096
+- Max tokens: 1024-2048
diff --git a/surfsense_backend/scripts/update_admin_user.py b/surfsense_backend/scripts/update_admin_user.py
new file mode 100644
index 000000000..31bfd7d1d
--- /dev/null
+++ b/surfsense_backend/scripts/update_admin_user.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""
+Utility script to update admin user password and status.
+
+This script should be run on the VPS after deployment to:
+1. Update the admin user's password
+2. Ensure is_superuser, is_active, is_verified are all True
+
+Usage:
+ cd /path/to/SurfSense/surfsense_backend
+ source venv/bin/activate # or your virtual environment
+ python scripts/update_admin_user.py
+
+Note: Make sure the .env file is properly configured with DATABASE_URL
+"""
+
+import asyncio
+import os
+import sys
+
+# Add the parent directory to path so we can import app modules
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+from passlib.context import CryptContext
+from sqlalchemy import select, update
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
+from sqlalchemy.orm import sessionmaker
+
+# Password hashing context (same as FastAPI-Users uses)
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+# Configuration
+ADMIN_EMAIL = "ojars@kapteinis.lv"
+NEW_PASSWORD = "^&U0yXLK1ypZOwLDGFeLT35kCrblITYyAVdVmF3!iJ%kkY1Nl^IS!P"
+
+
+async def update_admin_user():
+ """Update admin user password and status."""
+
+ # Get database URL from environment
+ database_url = os.getenv("DATABASE_URL")
+ if not database_url:
+ print("ERROR: DATABASE_URL not found in environment variables")
+ print("Make sure your .env file is configured correctly")
+ sys.exit(1)
+
+ # Convert postgres:// to postgresql+asyncpg:// if needed
+ if database_url.startswith("postgres://"):
+ database_url = database_url.replace("postgres://", "postgresql+asyncpg://", 1)
+ elif database_url.startswith("postgresql://"):
+ database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
+
+ print(f"Connecting to database...")
+
+ # Create async engine
+ engine = create_async_engine(database_url, echo=False)
+ async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
+
+ async with async_session() as session:
+ try:
+ # Hash the new password
+ hashed_password = pwd_context.hash(NEW_PASSWORD)
+
+ # Update the user
+ result = await session.execute(
+ update(UserTable)
+ .where(UserTable.email == ADMIN_EMAIL)
+ .values(
+ hashed_password=hashed_password,
+ is_superuser=True,
+ is_active=True,
+ is_verified=True
+ )
+ .returning(UserTable.id, UserTable.email)
+ )
+
+ updated_user = result.first()
+
+ if updated_user:
+ await session.commit()
+ print(f"\n✅ Successfully updated user: {ADMIN_EMAIL}")
+ print(f" - User ID: {updated_user.id}")
+ print(f" - Password: Updated to new value")
+ print(f" - is_superuser: True")
+ print(f" - is_active: True")
+ print(f" - is_verified: True")
+ else:
+ print(f"\n❌ User not found: {ADMIN_EMAIL}")
+ print(" Make sure the email address is correct")
+
+ # List existing users for debugging
+ from sqlalchemy import text
+ users_result = await session.execute(
+ text("SELECT email FROM \"user\" LIMIT 10")
+ )
+ users = users_result.fetchall()
+ if users:
+ print("\n Existing users in database:")
+ for user in users:
+ print(f" - {user[0]}")
+
+ except Exception as e:
+ print(f"\n❌ Error updating user: {e}")
+ await session.rollback()
+ raise
+ finally:
+ await engine.dispose()
+
+
+# Import the User model after environment is loaded
+# We need to do this here to ensure DATABASE_URL is available
+if __name__ == "__main__":
+ print("=" * 50)
+ print("Admin User Update Script")
+ print("=" * 50)
+ print(f"\nTarget user: {ADMIN_EMAIL}")
+ print(f"New password: {'*' * 10} (hidden)")
+ print()
+
+ # Now import the User model
+ try:
+ from app.db import User as UserTable
+ except ImportError:
+ # Try alternative import if running from different directory
+ try:
+ from surfsense_backend.app.db import User as UserTable
+ except ImportError:
+ print("ERROR: Could not import User model")
+ print("Make sure you're running this script from the surfsense_backend directory")
+ sys.exit(1)
+
+ # Run the async update
+ asyncio.run(update_admin_user())
+
+ print("\n" + "=" * 50)
+ print("Done!")
+ print("=" * 50)
diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx
index dc9c73785..5ab74aa69 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx
@@ -50,7 +50,7 @@ export default function DashboardLayout({
url: `/dashboard/${search_space_id}/sources/add`,
},
{
- title: "Add Webpage(s)",
+ title: "Add Webpages",
url: `/dashboard/${search_space_id}/documents/webpage`,
},
{
diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx
index 01436aff9..bafe7a0cc 100644
--- a/surfsense_web/app/dashboard/layout.tsx
+++ b/surfsense_web/app/dashboard/layout.tsx
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { AnnouncementBanner } from "@/components/announcement-banner";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { baseApiService } from "@/lib/apis/base-api.service";
interface DashboardLayoutProps {
children: React.ReactNode;
@@ -21,6 +22,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
router.push("/login");
return;
}
+ // Ensure the baseApiService has the correct token
+ // This handles cases where the page is refreshed or navigated to directly
+ baseApiService.setBearerToken(token);
setIsCheckingAuth(false);
}, [router]);
diff --git a/surfsense_web/app/dashboard/site-settings/page.tsx b/surfsense_web/app/dashboard/site-settings/page.tsx
index 338708124..7cb2497bd 100644
--- a/surfsense_web/app/dashboard/site-settings/page.tsx
+++ b/surfsense_web/app/dashboard/site-settings/page.tsx
@@ -67,7 +67,7 @@ export default function SiteSettingsPage() {
const checkSuperuser = async () => {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
- const token = localStorage.getItem("access_token");
+ const token = localStorage.getItem("surfsense_bearer_token");
if (!token) {
router.push("/login");
@@ -150,7 +150,7 @@ export default function SiteSettingsPage() {
setIsSaving(true);
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
- const token = localStorage.getItem("access_token");
+ const token = localStorage.getItem("surfsense_bearer_token");
const response = await fetch(`${backendUrl}/api/v1/site-config`, {
method: "PUT",
diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx
index 3b80994a8..fd9924b3d 100644
--- a/surfsense_web/components/TokenHandler.tsx
+++ b/surfsense_web/components/TokenHandler.tsx
@@ -2,6 +2,7 @@
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
+import { baseApiService } from "@/lib/apis/base-api.service";
interface TokenHandlerProps {
redirectPath?: string; // Path to redirect after storing token
@@ -35,7 +36,10 @@ const TokenHandler = ({
try {
// Store token in localStorage
localStorage.setItem(storageKey, token);
- // console.log(`Token stored in localStorage with key: ${storageKey}`);
+
+ // Update the baseApiService singleton with the new token
+ // This ensures subsequent API calls use the correct token
+ baseApiService.setBearerToken(token);
// Redirect to specified path
router.push(redirectPath);
diff --git a/surfsense_web/components/UserDropdown.tsx b/surfsense_web/components/UserDropdown.tsx
index 230bf0554..c0f333019 100644
--- a/surfsense_web/components/UserDropdown.tsx
+++ b/surfsense_web/components/UserDropdown.tsx
@@ -3,6 +3,7 @@
import { BadgeCheck, LogOut, Settings } from "lucide-react";
import { useRouter } from "next/navigation";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { baseApiService } from "@/lib/apis/base-api.service";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -29,6 +30,8 @@ export function UserDropdown({
try {
if (typeof window !== "undefined") {
localStorage.removeItem("surfsense_bearer_token");
+ // Clear the baseApiService token to prevent stale auth state
+ baseApiService.setBearerToken("");
router.push("/");
}
} catch (error) {