diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py
index a68720a7c..1ee107a6a 100644
--- a/backend/open_webui/config.py
+++ b/backend/open_webui/config.py
@@ -1624,6 +1624,12 @@ ENABLE_CHANNELS = PersistentConfig(
os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true',
)
+ENABLE_CALENDAR = PersistentConfig(
+ 'ENABLE_CALENDAR',
+ 'calendar.enable',
+ os.environ.get('ENABLE_CALENDAR', 'True').lower() == 'true',
+)
+
AUTOMATION_MAX_COUNT = PersistentConfig(
'AUTOMATION_MAX_COUNT',
'automations.max_count',
diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py
index c13250c58..23379a775 100644
--- a/backend/open_webui/main.py
+++ b/backend/open_webui/main.py
@@ -396,6 +396,7 @@ from open_webui.config import (
AUTOMATION_MAX_COUNT,
AUTOMATION_MIN_INTERVAL,
ENABLE_CHANNELS,
+ ENABLE_CALENDAR,
ENABLE_NOTES,
ENABLE_USER_STATUS,
ENABLE_COMMUNITY_SHARING,
@@ -902,6 +903,7 @@ app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT
app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT
app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
+app.state.config.ENABLE_CALENDAR = ENABLE_CALENDAR
app.state.config.ENABLE_NOTES = ENABLE_NOTES
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
@@ -2218,6 +2220,7 @@ async def get_app_config(request: Request):
'enable_folders': app.state.config.ENABLE_FOLDERS,
'folder_max_file_count': app.state.config.FOLDER_MAX_FILE_COUNT,
'enable_channels': app.state.config.ENABLE_CHANNELS,
+ 'enable_calendar': app.state.config.ENABLE_CALENDAR,
'enable_notes': app.state.config.ENABLE_NOTES,
'enable_web_search': app.state.config.ENABLE_WEB_SEARCH,
'enable_code_execution': app.state.config.ENABLE_CODE_EXECUTION,
diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py
index 651e123b6..c8daa4957 100644
--- a/backend/open_webui/routers/auths.py
+++ b/backend/open_webui/routers/auths.py
@@ -972,6 +972,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
+ 'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
@@ -1000,6 +1001,7 @@ class AdminConfig(BaseModel):
AUTOMATION_MAX_COUNT: Optional[int | str] = None
AUTOMATION_MIN_INTERVAL: Optional[int | str] = None
ENABLE_CHANNELS: bool
+ ENABLE_CALENDAR: bool
ENABLE_MEMORIES: bool
ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool
@@ -1031,6 +1033,7 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else ''
)
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
+ request.app.state.config.ENABLE_CALENDAR = form_data.ENABLE_CALENDAR
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
@@ -1074,6 +1077,7 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
+ 'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py
index 220edf853..f92f5b894 100644
--- a/backend/open_webui/routers/calendar.py
+++ b/backend/open_webui/routers/calendar.py
@@ -2,7 +2,7 @@ import logging
import time
from typing import Optional
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from open_webui.models.calendar import (
@@ -24,12 +24,22 @@ from open_webui.models.groups import Groups
from open_webui.models.users import UserModel
from open_webui.utils.auth import get_verified_user
from open_webui.utils.calendar import expand_recurring_event
+from open_webui.constants import ERROR_MESSAGES
log = logging.getLogger(__name__)
router = APIRouter()
+async def check_calendar_enabled(request: Request):
+ """Dependency to ensure calendar feature is globally enabled."""
+ if not request.app.state.config.ENABLE_CALENDAR:
+ raise HTTPException(
+ status_code=403,
+ detail=ERROR_MESSAGES.FEATURE_DISABLED('Calendar'),
+ )
+
+
async def _check_calendar_access(
calendar_id: str, user: UserModel, permission: str = 'write'
) -> CalendarModel:
@@ -58,14 +68,16 @@ async def _check_calendar_access(
@router.get('/', response_model=list[CalendarModel])
-async def get_calendars(user: UserModel = Depends(get_verified_user)):
+async def get_calendars(request: Request, user: UserModel = Depends(get_verified_user)):
"""List user's calendars (owned + shared). Auto-creates defaults on first call."""
+ await check_calendar_enabled(request)
return await Calendars.get_calendars_by_user(user.id)
@router.post('/create', response_model=CalendarModel)
-async def create_calendar(form_data: CalendarForm, user: UserModel = Depends(get_verified_user)):
+async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)):
"""Create a new user calendar."""
+ await check_calendar_enabled(request)
return await Calendars.insert_new_calendar(user.id, form_data)
@@ -76,6 +88,7 @@ async def create_calendar(form_data: CalendarForm, user: UserModel = Depends(get
@router.get('/events')
async def get_events(
+ request: Request,
start: str,
end: str,
calendar_ids: Optional[str] = None,
@@ -92,6 +105,7 @@ async def get_events(
- Stored events from the database
- Virtual events computed from active automation RRULEs (Scheduled Tasks calendar)
"""
+ await check_calendar_enabled(request)
from datetime import datetime
try:
@@ -203,25 +217,29 @@ async def get_events(
@router.post('/events/create', response_model=CalendarEventModel)
-async def create_event(form_data: CalendarEventForm, user: UserModel = Depends(get_verified_user)):
+async def create_event(request: Request, form_data: CalendarEventForm, user: UserModel = Depends(get_verified_user)):
+ await check_calendar_enabled(request)
await _check_calendar_access(form_data.calendar_id, user, 'write')
return await CalendarEvents.insert_new_event(user.id, form_data)
@router.get('/events/search', response_model=CalendarEventListResponse)
async def search_events(
+ request: Request,
query: Optional[str] = None,
skip: int = 0,
limit: int = 30,
user: UserModel = Depends(get_verified_user),
):
+ await check_calendar_enabled(request)
return await CalendarEvents.search_events(
user_id=user.id, query=query, skip=skip, limit=limit
)
@router.get('/events/{event_id}', response_model=CalendarEventModel)
-async def get_event(event_id: str, user: UserModel = Depends(get_verified_user)):
+async def get_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)):
+ await check_calendar_enabled(request)
event = await CalendarEvents.get_event_by_id(event_id)
if not event:
raise HTTPException(status_code=404, detail='Event not found')
@@ -233,8 +251,9 @@ async def get_event(event_id: str, user: UserModel = Depends(get_verified_user))
@router.post('/events/{event_id}/update', response_model=CalendarEventModel)
async def update_event(
- event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user)
+ request: Request, event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user)
):
+ await check_calendar_enabled(request)
event = await CalendarEvents.get_event_by_id(event_id)
if not event:
raise HTTPException(status_code=404, detail='Event not found')
@@ -248,7 +267,8 @@ async def update_event(
@router.delete('/events/{event_id}/delete')
-async def delete_event(event_id: str, user: UserModel = Depends(get_verified_user)):
+async def delete_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)):
+ await check_calendar_enabled(request)
event = await CalendarEvents.get_event_by_id(event_id)
if not event:
raise HTTPException(status_code=404, detail='Event not found')
@@ -263,9 +283,10 @@ async def delete_event(event_id: str, user: UserModel = Depends(get_verified_use
@router.post('/events/{event_id}/rsvp', response_model=dict)
async def rsvp_event(
- event_id: str, form_data: RSVPForm, user: UserModel = Depends(get_verified_user)
+ request: Request, event_id: str, form_data: RSVPForm, user: UserModel = Depends(get_verified_user)
):
"""Update own RSVP status for an event."""
+ await check_calendar_enabled(request)
if form_data.status not in ('accepted', 'declined', 'tentative', 'pending'):
raise HTTPException(status_code=400, detail='Invalid status')
@@ -281,15 +302,17 @@ async def rsvp_event(
@router.get('/{calendar_id}', response_model=CalendarModel)
-async def get_calendar_by_id(calendar_id: str, user: UserModel = Depends(get_verified_user)):
+async def get_calendar_by_id(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)):
+ await check_calendar_enabled(request)
cal = await _check_calendar_access(calendar_id, user, 'read')
return cal
@router.post('/{calendar_id}/update', response_model=CalendarModel)
async def update_calendar(
- calendar_id: str, form_data: CalendarUpdateForm, user: UserModel = Depends(get_verified_user)
+ request: Request, calendar_id: str, form_data: CalendarUpdateForm, user: UserModel = Depends(get_verified_user)
):
+ await check_calendar_enabled(request)
cal = await _check_calendar_access(calendar_id, user, 'write')
# Only owner/admin can change access grants
@@ -303,7 +326,8 @@ async def update_calendar(
@router.delete('/{calendar_id}/delete')
-async def delete_calendar(calendar_id: str, user: UserModel = Depends(get_verified_user)):
+async def delete_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)):
+ await check_calendar_enabled(request)
cal = await _check_calendar_access(calendar_id, user, 'write')
# Only owner/admin can delete
@@ -318,7 +342,8 @@ async def delete_calendar(calendar_id: str, user: UserModel = Depends(get_verifi
@router.post('/{calendar_id}/default')
-async def set_default_calendar(calendar_id: str, user: UserModel = Depends(get_verified_user)):
+async def set_default_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)):
+ await check_calendar_enabled(request)
cal = await Calendars.set_default_calendar(user.id, calendar_id)
if not cal:
raise HTTPException(status_code=404, detail='Calendar not found')
diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py
index 52a886839..e0791a35f 100644
--- a/backend/open_webui/utils/tools.py
+++ b/backend/open_webui/utils/tools.py
@@ -557,7 +557,10 @@ async def get_builtin_tools(
)
# Calendar tools - search/create/update/delete events
- if is_builtin_tool_enabled('calendar'):
+ if (
+ is_builtin_tool_enabled('calendar')
+ and getattr(request.app.state.config, 'ENABLE_CALENDAR', False)
+ ):
builtin_functions.extend(
[search_calendar_events, create_calendar_event, update_calendar_event, delete_calendar_event]
)
diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte
index f2ba4a3ee..f535bd68e 100644
--- a/src/lib/components/admin/Settings/General.svelte
+++ b/src/lib/components/admin/Settings/General.svelte
@@ -756,6 +756,14 @@