11from __future__ import annotations
2-
32import json
43import uuid
54from typing import Any , Dict , List , Optional
6-
75from fastapi import APIRouter , Depends , HTTPException
86from pydantic import BaseModel , Field , field_validator
9-
107from .auth import get_current_owner , require_api_key
118from .database import get_db
129
2118
2219
2320class FilterPreset (BaseModel ):
24- """Validated representation of the frontend filter state."""
2521 severity : str = "all"
2622 target : str = "all"
2723 scanner : str = "all"
@@ -46,9 +42,9 @@ def validate_severity(cls, v: str) -> str:
4642
4743
4844class SavedViewCreate (BaseModel ):
49- """Request body for POST /saved-views."""
5045 name : str = Field (..., min_length = 1 , max_length = 60 )
5146 filter_json : str
47+ shared : bool = False
5248
5349 @field_validator ("name" )
5450 @classmethod
@@ -70,9 +66,9 @@ def validate_filter_json(cls, v: str) -> str:
7066
7167
7268class SavedViewUpdate (BaseModel ):
73- """Request body for PUT /saved-views/{id}."""
7469 name : Optional [str ] = Field (None , min_length = 1 , max_length = 60 )
7570 filter_json : Optional [str ] = None
71+ shared : Optional [bool ] = None
7672
7773 @field_validator ("name" )
7874 @classmethod
@@ -97,12 +93,9 @@ def validate_filter_json(cls, v: Optional[str]) -> Optional[str]:
9793 return v
9894
9995
100-
101-
102-
10396async def require_owned_saved_view (db , view_id : str , owner : str ) -> Dict [str , Any ]:
10497 row = await db .fetchone (
105- "SELECT id, owner_id FROM saved_views WHERE id = ?" , (view_id ,)
98+ "SELECT id, owner_id, shared FROM saved_views WHERE id = ?" , (view_id ,)
10699 )
107100 if row is None :
108101 raise HTTPException (status_code = 404 , detail = "Saved view not found" )
@@ -115,10 +108,15 @@ async def require_owned_saved_view(db, view_id: str, owner: str) -> Dict[str, An
115108
116109@saved_views_router .get ("" )
117110async def list_saved_views (owner : str = Depends (get_current_owner )) -> Dict [str , Any ]:
111+ """Return views owned by the caller plus all shared views."""
118112 db = await get_db ()
119113 rows : List [Dict ] = await db .fetchall (
120- "SELECT id, name, filter_json, created_at, updated_at "
121- "FROM saved_views WHERE owner_id = ? ORDER BY created_at ASC" ,
114+ """
115+ SELECT id, name, filter_json, shared, owner_id, created_at, updated_at
116+ FROM saved_views
117+ WHERE owner_id = ? OR shared = 1
118+ ORDER BY created_at ASC
119+ """ ,
122120 (owner ,),
123121 )
124122 return {"views" : rows , "total" : len (rows )}
@@ -129,7 +127,6 @@ async def create_saved_view(
129127 body : SavedViewCreate , owner : str = Depends (get_current_owner )
130128) -> Dict [str , Any ]:
131129 db = await get_db ()
132-
133130 existing = await db .fetchone (
134131 "SELECT id FROM saved_views WHERE LOWER(name) = LOWER(?) AND owner_id = ?" ,
135132 (body .name , owner ),
@@ -140,10 +137,13 @@ async def create_saved_view(
140137 detail = f"A saved view named '{ body .name } ' already exists. "
141138 "Use PUT to overwrite it." ,
142139 )
143-
144140 view_id = str (uuid .uuid4 ())
145141 await db .execute (
146- (view_id , body .name , body .filter_json , owner ),
142+ """
143+ INSERT INTO saved_views (id, name, filter_json, shared, owner_id)
144+ VALUES (?, ?, ?, ?, ?)
145+ """ ,
146+ (view_id , body .name , body .filter_json , int (body .shared ), owner ),
147147 )
148148 return {"id" : view_id , "name" : body .name , "created" : True }
149149
@@ -155,14 +155,12 @@ async def update_saved_view(
155155 owner : str = Depends (get_current_owner ),
156156) -> Dict [str , Any ]:
157157 db = await get_db ()
158-
159158 await require_owned_saved_view (db , view_id , owner )
160159
161160 updates : List [str ] = []
162161 params : List [Any ] = []
163162
164163 if body .name is not None :
165- # Check for name collision with a *different* record owned by this caller
166164 collision = await db .fetchone (
167165 "SELECT id FROM saved_views WHERE LOWER(name) = LOWER(?) "
168166 "AND id != ? AND owner_id = ?" ,
@@ -180,6 +178,10 @@ async def update_saved_view(
180178 updates .append ("filter_json = ?" )
181179 params .append (body .filter_json )
182180
181+ if body .shared is not None :
182+ updates .append ("shared = ?" )
183+ params .append (int (body .shared ))
184+
183185 if not updates :
184186 raise HTTPException (status_code = 400 , detail = "No fields to update" )
185187
@@ -199,16 +201,14 @@ async def delete_saved_view(
199201 view_id : str , owner : str = Depends (get_current_owner )
200202) -> Dict [str , Any ]:
201203 db = await get_db ()
202-
203204 row = await db .fetchone (
204205 "SELECT owner_id FROM saved_views WHERE id = ?" , (view_id ,)
205206 )
206207 if row is not None and row ["owner_id" ] != owner :
207208 raise HTTPException (
208209 status_code = 403 , detail = "You do not have access to this saved view"
209210 )
210-
211211 await db .execute (
212212 "DELETE FROM saved_views WHERE id = ? AND owner_id = ?" , (view_id , owner )
213213 )
214- return {"id" : view_id , "deleted" : True }
214+ return {"id" : view_id , "deleted" : True }
0 commit comments