| import json |
| import logging |
| from typing import Optional |
|
|
| from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status |
| from open_webui.config import ( |
| BYPASS_ADMIN_ACCESS_CONTROL, |
| ENABLE_ADMIN_CHAT_ACCESS, |
| ENABLE_ADMIN_EXPORT, |
| ) |
| from open_webui.constants import ERROR_MESSAGES |
| from open_webui.internal.db import get_async_session |
| from open_webui.models.access_grants import AccessGrants |
| from open_webui.models.groups import Groups |
| from open_webui.models.notes import ( |
| NoteForm, |
| NoteListResponse, |
| NoteModel, |
| Notes, |
| NoteUserResponse, |
| ) |
| from open_webui.models.users import UserResponse, Users |
| from open_webui.socket.main import sio |
| from open_webui.utils.access_control import ( |
| filter_allowed_access_grants, |
| has_permission, |
| has_public_read_access_grant, |
| has_public_write_access_grant, |
| ) |
| from open_webui.utils.auth import get_admin_user, get_verified_user |
| from pydantic import BaseModel |
| from sqlalchemy.ext.asyncio import AsyncSession |
|
|
| log = logging.getLogger(__name__) |
|
|
| router = APIRouter() |
|
|
|
|
| def _truncate_note_data(data: Optional[dict], max_length: int = 1000) -> Optional[dict]: |
| if not data: |
| return data |
| md = (data.get('content') or {}).get('md') or '' |
| return {'content': {'md': md[:max_length]}} |
|
|
|
|
| |
| |
| |
|
|
|
|
| class NoteItemResponse(BaseModel): |
| id: str |
| title: str |
| data: Optional[dict] |
| is_pinned: Optional[bool] = False |
| updated_at: int |
| created_at: int |
| user: Optional[UserResponse] = None |
|
|
|
|
| @router.get('/', response_model=list[NoteItemResponse]) |
| async def get_notes( |
| request: Request, |
| page: Optional[int] = None, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| limit = None |
| skip = None |
| if page is not None: |
| limit = 60 |
| skip = (page - 1) * limit |
|
|
| notes = await Notes.get_notes_by_user_id(user.id, 'read', skip=skip, limit=limit, db=db) |
| if not notes: |
| return [] |
|
|
| user_ids = list(set(note.user_id for note in notes)) |
| users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} |
|
|
| pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) |
|
|
| return [ |
| NoteUserResponse( |
| **{ |
| **note.model_dump(), |
| 'is_pinned': note.id in pinned_note_ids, |
| 'data': _truncate_note_data(note.data), |
| 'user': UserResponse(**users[note.user_id].model_dump()), |
| } |
| ) |
| for note in notes |
| if note.user_id in users |
| ] |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get('/pinned', response_model=list[NoteItemResponse]) |
| async def get_pinned_notes( |
| request: Request, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| notes = await Notes.get_pinned_notes_by_user_id(user.id, 'read', db=db) |
| if not notes: |
| return [] |
|
|
| user_ids = list(set(note.user_id for note in notes)) |
| users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} |
|
|
| return [ |
| NoteUserResponse( |
| **{ |
| **note.model_dump(), |
| 'is_pinned': True, |
| 'data': _truncate_note_data(note.data), |
| 'user': UserResponse(**users[note.user_id].model_dump()), |
| } |
| ) |
| for note in notes |
| if note.user_id in users |
| ] |
|
|
|
|
| @router.get('/search', response_model=NoteListResponse) |
| async def search_notes( |
| request: Request, |
| query: Optional[str] = None, |
| view_option: Optional[str] = None, |
| permission: Optional[str] = None, |
| order_by: Optional[str] = None, |
| direction: Optional[str] = None, |
| page: Optional[int] = 1, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| limit = None |
| skip = None |
| if page is not None: |
| limit = 60 |
| skip = (page - 1) * limit |
|
|
| filter = {} |
| if query: |
| filter['query'] = query |
| if view_option: |
| filter['view_option'] = view_option |
| if permission: |
| filter['permission'] = permission |
| if order_by: |
| filter['order_by'] = order_by |
| if direction: |
| filter['direction'] = direction |
|
|
| if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: |
| groups = await Groups.get_groups_by_member_id(user.id, db=db) |
| if groups: |
| filter['group_ids'] = [group.id for group in groups] |
|
|
| filter['user_id'] = user.id |
|
|
| result = await Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db) |
| pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) |
| for note in result.items: |
| note.is_pinned = note.id in pinned_note_ids |
| note.data = _truncate_note_data(note.data) |
| return result |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.post('/create', response_model=Optional[NoteModel]) |
| async def create_new_note( |
| request: Request, |
| form_data: NoteForm, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| form_data.access_grants = await filter_allowed_access_grants( |
| request.app.state.config.USER_PERMISSIONS, |
| user.id, |
| user.role, |
| form_data.access_grants, |
| 'sharing.public_notes', |
| db=db, |
| ) |
|
|
| try: |
| note = await Notes.insert_new_note(user.id, form_data, db=db) |
| return note |
| except Exception as e: |
| log.exception(e) |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) |
|
|
|
|
| |
| |
| |
|
|
|
|
| class NoteResponse(NoteModel): |
| write_access: bool = False |
|
|
|
|
| @router.get('/{id}', response_model=Optional[NoteResponse]) |
| async def get_note_by_id( |
| request: Request, |
| id: str, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| note = await Notes.get_note_by_id(id, db=db) |
| if not note: |
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) |
|
|
| if user.role != 'admin' and ( |
| user.id != note.user_id |
| and ( |
| not await AccessGrants.has_access( |
| user_id=user.id, |
| resource_type='note', |
| resource_id=note.id, |
| permission='read', |
| db=db, |
| ) |
| ) |
| ): |
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) |
|
|
| write_access = ( |
| user.role == 'admin' |
| or (user.id == note.user_id) |
| or await AccessGrants.has_access( |
| user_id=user.id, |
| resource_type='note', |
| resource_id=note.id, |
| permission='write', |
| db=db, |
| ) |
| or has_public_write_access_grant(note.access_grants) |
| ) |
|
|
| pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) |
| return NoteResponse( |
| **{**note.model_dump(), 'is_pinned': note.id in pinned_note_ids}, |
| write_access=write_access, |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.post('/{id}/update', response_model=Optional[NoteModel]) |
| async def update_note_by_id( |
| request: Request, |
| id: str, |
| form_data: NoteForm, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| note = await Notes.get_note_by_id(id, db=db) |
| if not note: |
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) |
|
|
| if user.role != 'admin' and ( |
| user.id != note.user_id |
| and not await AccessGrants.has_access( |
| user_id=user.id, |
| resource_type='note', |
| resource_id=note.id, |
| permission='write', |
| db=db, |
| ) |
| ): |
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) |
|
|
| form_data.access_grants = await filter_allowed_access_grants( |
| request.app.state.config.USER_PERMISSIONS, |
| user.id, |
| user.role, |
| form_data.access_grants, |
| 'sharing.public_notes', |
| db=db, |
| ) |
|
|
| try: |
| note = await Notes.update_note_by_id(id, form_data, db=db) |
| pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) |
| note.is_pinned = note.id in pinned_note_ids |
|
|
| await sio.emit( |
| 'note-events', |
| note.model_dump(), |
| to=f'note:{note.id}', |
| ) |
|
|
| return note |
| except Exception as e: |
| log.exception(e) |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) |
|
|
|
|
| |
| |
| |
|
|
|
|
| class NoteAccessGrantsForm(BaseModel): |
| access_grants: list[dict] |
|
|
|
|
| @router.post('/{id}/access/update', response_model=Optional[NoteModel]) |
| async def update_note_access_by_id( |
| request: Request, |
| id: str, |
| form_data: NoteAccessGrantsForm, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| note = await Notes.get_note_by_id(id, db=db) |
| if not note: |
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) |
|
|
| if user.role != 'admin' and ( |
| user.id != note.user_id |
| and not await AccessGrants.has_access( |
| user_id=user.id, |
| resource_type='note', |
| resource_id=note.id, |
| permission='write', |
| db=db, |
| ) |
| ): |
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) |
|
|
| form_data.access_grants = await filter_allowed_access_grants( |
| request.app.state.config.USER_PERMISSIONS, |
| user.id, |
| user.role, |
| form_data.access_grants, |
| 'sharing.public_notes', |
| ) |
|
|
| await AccessGrants.set_access_grants('note', id, form_data.access_grants, db=db) |
|
|
| note = await Notes.get_note_by_id(id, db=db) |
| pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) |
| note.is_pinned = note.id in pinned_note_ids |
| return note |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.post('/{id}/pin', response_model=Optional[NoteModel]) |
| async def pin_note_by_id( |
| request: Request, |
| id: str, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| note = await Notes.get_note_by_id(id, db=db) |
| if not note: |
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) |
|
|
| if user.role != 'admin' and ( |
| user.id != note.user_id |
| and not await AccessGrants.has_access( |
| user_id=user.id, |
| resource_type='note', |
| resource_id=note.id, |
| permission='read', |
| db=db, |
| ) |
| ): |
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) |
|
|
| note = await Notes.toggle_note_pinned_by_id(id, user.id, db=db) |
| pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) |
| note.is_pinned = note.id in pinned_note_ids |
| return note |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.delete('/{id}/delete', response_model=bool) |
| async def delete_note_by_id( |
| request: Request, |
| id: str, |
| user=Depends(get_verified_user), |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| if user.role != 'admin' and not await has_permission( |
| user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail=ERROR_MESSAGES.UNAUTHORIZED, |
| ) |
|
|
| note = await Notes.get_note_by_id(id, db=db) |
| if not note: |
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) |
|
|
| if user.role != 'admin' and ( |
| user.id != note.user_id |
| and not await AccessGrants.has_access( |
| user_id=user.id, |
| resource_type='note', |
| resource_id=note.id, |
| permission='write', |
| db=db, |
| ) |
| ): |
| raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) |
|
|
| try: |
| note = await Notes.delete_note_by_id(id, db=db) |
| return True |
| except Exception as e: |
| log.exception(e) |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) |
|
|