mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
Fix translation keys, session persistence, and add admin tools
- Fix nav_menu translation key: 'Add Webpage(s)' -> 'Add Webpages' - Fix session persistence: sync baseApiService token on login/logout/load - Add admin user update script (scripts/update_admin_user.py) - Add Ollama memory optimization guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ae6455069b
commit
28f2076141
8 changed files with 1055 additions and 4 deletions
515
claude.md
515
claude.md
|
|
@ -1651,6 +1651,521 @@ For questions or issues related to authentication:
|
|||
|
||||
---
|
||||
|
||||
## Registration Toggle System
|
||||
|
||||
**Implementation Date:** 2025-11-18
|
||||
**Implemented By:** Claude AI Assistant (Anthropic Sonnet 4.5)
|
||||
**Feature:** Disable User Registration via Admin Panel
|
||||
|
||||
### Overview
|
||||
|
||||
Implemented a 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 for private deployments.
|
||||
|
||||
### Implementation Summary
|
||||
|
||||
The registration toggle system operates at multiple layers:
|
||||
|
||||
1. **Database Layer**: Added `disable_registration` boolean field to `site_configuration` table (migration #39)
|
||||
2. **Backend API Layer**: Updated `registration_allowed()` dependency to check database toggle and return 403 when disabled
|
||||
3. **Frontend UI Layer**: Conditionally hide "Sign Up" link on login page based on configuration
|
||||
4. **Frontend Route Layer**: RouteGuard component protects `/register` route, showing 404 when disabled
|
||||
5. **Admin Panel Layer**: Added "Registration Control" section to site-settings page for easy toggling
|
||||
|
||||
### Backend Changes
|
||||
|
||||
#### Database Migration (#39)
|
||||
|
||||
**File:** `surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py`
|
||||
|
||||
```python
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
'site_configuration',
|
||||
sa.Column('disable_registration', sa.Boolean(), nullable=False, server_default='false')
|
||||
)
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('site_configuration', 'disable_registration')
|
||||
```
|
||||
|
||||
**Revision ID:** `'39'` (shortened to fit varchar(32) constraint)
|
||||
**Down Revision:** `'38_add_site_configuration_table'`
|
||||
**Default Value:** `false` (registration enabled by default)
|
||||
|
||||
#### Database Model
|
||||
|
||||
**File:** `surfsense_backend/app/db.py`
|
||||
|
||||
```python
|
||||
class SiteConfiguration(SQLModel, table=True):
|
||||
# ... existing fields ...
|
||||
|
||||
# Registration control
|
||||
disable_registration = Column(Boolean, nullable=False, default=False)
|
||||
```
|
||||
|
||||
#### API Schemas
|
||||
|
||||
**File:** `surfsense_backend/app/schemas/site_configuration.py`
|
||||
|
||||
```python
|
||||
class SiteConfigurationBase(BaseModel):
|
||||
# ... existing fields ...
|
||||
|
||||
# Registration control
|
||||
disable_registration: bool = False
|
||||
|
||||
class SiteConfigurationUpdate(SiteConfigurationBase):
|
||||
# ... existing fields ...
|
||||
disable_registration: bool | None = None
|
||||
```
|
||||
|
||||
#### Registration Endpoint Protection
|
||||
|
||||
**File:** `surfsense_backend/app/app.py`
|
||||
|
||||
```python
|
||||
async def registration_allowed(session: AsyncSession = Depends(get_async_session)):
|
||||
# Check environment variable first
|
||||
if not config.REGISTRATION_ENABLED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Registration is disabled by system configuration"
|
||||
)
|
||||
|
||||
# Check site configuration database toggle
|
||||
result = await session.execute(select(SiteConfiguration).where(SiteConfiguration.id == 1))
|
||||
site_config = result.scalar_one_or_none()
|
||||
|
||||
if site_config and site_config.disable_registration:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Registration is currently disabled. Please contact the administrator if you need access."
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
# Applied to registration routes
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(UserRead, UserCreate),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
dependencies=[Depends(registration_allowed)], # Enforces registration check
|
||||
)
|
||||
```
|
||||
|
||||
**Error Response** (when disabled):
|
||||
```json
|
||||
{
|
||||
"detail": "Registration is currently disabled. Please contact the administrator if you need access."
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend Changes
|
||||
|
||||
#### Context/State Management
|
||||
|
||||
**File:** `surfsense_web/contexts/SiteConfigContext.tsx`
|
||||
|
||||
```typescript
|
||||
export interface SiteConfig {
|
||||
// ... existing fields ...
|
||||
|
||||
// Registration control
|
||||
disable_registration: boolean;
|
||||
}
|
||||
|
||||
const defaultConfig: SiteConfig = {
|
||||
// ... existing fields ...
|
||||
disable_registration: false,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
#### Login Form
|
||||
|
||||
**File:** `surfsense_web/app/(home)/login/LocalLoginForm.tsx`
|
||||
|
||||
**Changes:**
|
||||
- Added `useSiteConfig()` hook to access site configuration
|
||||
- Conditionally render "Sign up" link only when `!config.disable_registration`
|
||||
|
||||
```typescript
|
||||
import { useSiteConfig } from "@/contexts/SiteConfigContext";
|
||||
|
||||
export function LocalLoginForm() {
|
||||
const { config } = useSiteConfig();
|
||||
|
||||
return (
|
||||
{/* ... login form ... */}
|
||||
|
||||
{!config.disable_registration && (
|
||||
<div className="mt-4 text-center text-sm">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t("dont_have_account")}{" "}
|
||||
<Link href="/register">
|
||||
{t("sign_up")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 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 `<RouteGuard routeKey="registration">`
|
||||
|
||||
```typescript
|
||||
import { RouteGuard } from "@/components/RouteGuard";
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<RouteGuard routeKey="registration">
|
||||
{/* ... registration form content ... */}
|
||||
</RouteGuard>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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 */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Registration Control
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
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.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<ToggleSwitch
|
||||
label="Disable Registration"
|
||||
description="Prevent new users from creating accounts"
|
||||
checked={formData.disable_registration}
|
||||
onChange={() => handleToggle("disable_registration")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### 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: <class 'asyncpg.exceptions.StringDataRightTruncationError'>:
|
||||
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 <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
### 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**
|
||||
|
|
|
|||
383
surfsense_backend/OLLAMA_MEMORY_OPTIMIZATION.md
Normal file
383
surfsense_backend/OLLAMA_MEMORY_OPTIMIZATION.md
Normal file
|
|
@ -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
|
||||
142
surfsense_backend/scripts/update_admin_user.py
Normal file
142
surfsense_backend/scripts/update_admin_user.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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`,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue