mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-07 03:11:15 +03:00
UI viewer role (#16978)
* db migration * db model * assign admin role on password reset * add role to jwt and api responses * don't restrict api access for admins yet * use json response * frontend auth context * update auth form for profile endpoint * add access denied page * add protected routes * auth hook * dialogs * user settings view * restrict viewer access to settings * restrict camera functions for viewer role * add password dialog to account menu * spacing tweak * migrator default to admin * escape quotes in migrator * ui tweaks * tweaks * colors * colors * fix merge conflict * fix icons * add api layer enforcement * ui tweaks * fix error message * debug * clean up * remove print * guard apis for admin only * fix tests * fix review tests * use correct error responses from api in toasts * add role to account menu
This commit is contained in:
+4
-3
@@ -22,6 +22,7 @@ from markupsafe import escape
|
||||
from peewee import operator
|
||||
from pydantic import ValidationError
|
||||
|
||||
from frigate.api.auth import require_role
|
||||
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
|
||||
from frigate.api.defs.request.app_body import AppConfigSetBody
|
||||
from frigate.api.defs.tags import Tags
|
||||
@@ -201,7 +202,7 @@ def config_raw():
|
||||
)
|
||||
|
||||
|
||||
@router.post("/config/save")
|
||||
@router.post("/config/save", dependencies=[Depends(require_role(["admin"]))])
|
||||
def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
||||
new_config = body.decode()
|
||||
if not new_config:
|
||||
@@ -326,7 +327,7 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
||||
)
|
||||
|
||||
|
||||
@router.put("/config/set")
|
||||
@router.put("/config/set", dependencies=[Depends(require_role(["admin"]))])
|
||||
def config_set(request: Request, body: AppConfigSetBody):
|
||||
config_file = find_config_file()
|
||||
|
||||
@@ -542,7 +543,7 @@ async def logs(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/restart")
|
||||
@router.post("/restart", dependencies=[Depends(require_role(["admin"]))])
|
||||
def restart():
|
||||
try:
|
||||
restart_frigate()
|
||||
|
||||
+148
-29
@@ -11,8 +11,9 @@ import secrets
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from joserfc import jwt
|
||||
from peewee import DoesNotExist
|
||||
@@ -22,6 +23,7 @@ from frigate.api.defs.request.app_body import (
|
||||
AppPostLoginBody,
|
||||
AppPostUsersBody,
|
||||
AppPutPasswordBody,
|
||||
AppPutRoleBody,
|
||||
)
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.config import AuthConfig, ProxyConfig
|
||||
@@ -169,8 +171,10 @@ def verify_password(password, password_hash):
|
||||
return secrets.compare_digest(password_hash, compare_hash)
|
||||
|
||||
|
||||
def create_encoded_jwt(user, expiration, secret):
|
||||
return jwt.encode({"alg": "HS256"}, {"sub": user, "exp": expiration}, secret)
|
||||
def create_encoded_jwt(user, role, expiration, secret):
|
||||
return jwt.encode(
|
||||
{"alg": "HS256"}, {"sub": user, "role": role, "exp": expiration}, secret
|
||||
)
|
||||
|
||||
|
||||
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure):
|
||||
@@ -184,7 +188,48 @@ def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, sec
|
||||
)
|
||||
|
||||
|
||||
# Endpoint for use with nginx auth_request
|
||||
async def get_current_user(request: Request):
|
||||
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
||||
encoded_token = request.cookies.get(JWT_COOKIE_NAME)
|
||||
if not encoded_token:
|
||||
return JSONResponse(content={"message": "No JWT token found"}, status_code=401)
|
||||
|
||||
try:
|
||||
token = jwt.decode(encoded_token, request.app.jwt_token)
|
||||
if "sub" not in token.claims or "role" not in token.claims:
|
||||
return JSONResponse(
|
||||
content={"message": "Invalid JWT token"}, status_code=401
|
||||
)
|
||||
return {"username": token.claims["sub"], "role": token.claims["role"]}
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing JWT: {e}")
|
||||
return JSONResponse(content={"message": "Invalid JWT token"}, status_code=401)
|
||||
|
||||
|
||||
def require_role(required_roles: List[str]):
|
||||
async def role_checker(request: Request):
|
||||
# Get role from header (could be comma-separated)
|
||||
role_header = request.headers.get("remote-role")
|
||||
roles = [r.strip() for r in role_header.split(",")] if role_header else []
|
||||
|
||||
# Check if we have any roles
|
||||
if not roles:
|
||||
raise HTTPException(status_code=403, detail="Role not provided")
|
||||
|
||||
# Check if any role matches required_roles
|
||||
if not any(role in required_roles for role in roles):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Role {', '.join(roles)} not authorized. Required: {', '.join(required_roles)}",
|
||||
)
|
||||
|
||||
# Return the first matching role
|
||||
return next((role for role in roles if role in required_roles), roles[0])
|
||||
|
||||
return role_checker
|
||||
|
||||
|
||||
# Endpoints
|
||||
@router.get("/auth")
|
||||
def auth(request: Request):
|
||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||
@@ -195,6 +240,8 @@ def auth(request: Request):
|
||||
# dont require auth if the request is on the internal port
|
||||
# this header is set by Frigate's nginx proxy, so it cant be spoofed
|
||||
if int(request.headers.get("x-server-port", default=0)) == 5000:
|
||||
success_response.headers["remote-user"] = "anonymous"
|
||||
success_response.headers["remote-role"] = "admin"
|
||||
return success_response
|
||||
|
||||
fail_response = Response("", status_code=401)
|
||||
@@ -211,14 +258,18 @@ def auth(request: Request):
|
||||
if not auth_config.enabled:
|
||||
# pass the user header value from the upstream proxy if a mapping is specified
|
||||
# or use anonymous if none are specified
|
||||
if proxy_config.header_map.user is not None:
|
||||
upstream_user_header_value = request.headers.get(
|
||||
proxy_config.header_map.user,
|
||||
default="anonymous",
|
||||
)
|
||||
success_response.headers["remote-user"] = upstream_user_header_value
|
||||
else:
|
||||
success_response.headers["remote-user"] = "anonymous"
|
||||
user_header = proxy_config.header_map.user
|
||||
role_header = proxy_config.header_map.get("role", "Remote-Role")
|
||||
success_response.headers["remote-user"] = (
|
||||
request.headers.get(user_header, default="anonymous")
|
||||
if user_header
|
||||
else "anonymous"
|
||||
)
|
||||
success_response.headers["remote-role"] = (
|
||||
request.headers.get(role_header, default="viewer")
|
||||
if role_header
|
||||
else "viewer"
|
||||
)
|
||||
return success_response
|
||||
|
||||
# now apply authentication
|
||||
@@ -251,11 +302,15 @@ def auth(request: Request):
|
||||
if "sub" not in token.claims:
|
||||
logger.debug("user not set in jwt token")
|
||||
return fail_response
|
||||
if "role" not in token.claims:
|
||||
logger.debug("role not set in jwt token")
|
||||
return fail_response
|
||||
if "exp" not in token.claims:
|
||||
logger.debug("exp not set in jwt token")
|
||||
return fail_response
|
||||
|
||||
user = token.claims.get("sub")
|
||||
role = token.claims.get("role")
|
||||
current_time = int(time.time())
|
||||
|
||||
# if the jwt is expired
|
||||
@@ -283,7 +338,7 @@ def auth(request: Request):
|
||||
return fail_response
|
||||
new_expiration = current_time + JWT_SESSION_LENGTH
|
||||
new_encoded_jwt = create_encoded_jwt(
|
||||
user, new_expiration, request.app.jwt_token
|
||||
user, role, new_expiration, request.app.jwt_token
|
||||
)
|
||||
set_jwt_cookie(
|
||||
success_response,
|
||||
@@ -294,6 +349,7 @@ def auth(request: Request):
|
||||
)
|
||||
|
||||
success_response.headers["remote-user"] = user
|
||||
success_response.headers["remote-role"] = role
|
||||
return success_response
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing jwt: {e}")
|
||||
@@ -302,8 +358,16 @@ def auth(request: Request):
|
||||
|
||||
@router.get("/profile")
|
||||
def profile(request: Request):
|
||||
username = request.headers.get("remote-user")
|
||||
return JSONResponse(content={"username": username})
|
||||
username = request.headers.get("remote-user", "anonymous")
|
||||
if username != "anonymous":
|
||||
try:
|
||||
user = User.get_by_id(username)
|
||||
role = getattr(user, "role", "viewer")
|
||||
except DoesNotExist:
|
||||
role = "viewer" # Fallback if user deleted
|
||||
else:
|
||||
role = None
|
||||
return JSONResponse(content={"username": username, "role": role})
|
||||
|
||||
|
||||
@router.get("/logout")
|
||||
@@ -333,8 +397,11 @@ def login(request: Request, body: AppPostLoginBody):
|
||||
|
||||
password_hash = db_user.password_hash
|
||||
if verify_password(password, password_hash):
|
||||
role = getattr(db_user, "role", "viewer")
|
||||
if role not in ["admin", "viewer"]:
|
||||
role = "viewer" # Enforce valid roles
|
||||
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
||||
encoded_jwt = create_encoded_jwt(user, expiration, request.app.jwt_token)
|
||||
encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token)
|
||||
response = Response("", 200)
|
||||
set_jwt_cookie(
|
||||
response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE
|
||||
@@ -343,25 +410,31 @@ def login(request: Request, body: AppPostLoginBody):
|
||||
return JSONResponse(content={"message": "Login failed"}, status_code=401)
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
@router.get("/users", dependencies=[Depends(require_role(["admin"]))])
|
||||
def get_users():
|
||||
exports = User.select(User.username).order_by(User.username).dicts().iterator()
|
||||
exports = (
|
||||
User.select(User.username, User.role).order_by(User.username).dicts().iterator()
|
||||
)
|
||||
return JSONResponse([e for e in exports])
|
||||
|
||||
|
||||
@router.post("/users")
|
||||
def create_user(request: Request, body: AppPostUsersBody):
|
||||
@router.post("/users", dependencies=[Depends(require_role(["admin"]))])
|
||||
def create_user(
|
||||
request: Request,
|
||||
body: AppPostUsersBody,
|
||||
):
|
||||
HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations
|
||||
|
||||
if not re.match("^[A-Za-z0-9._]+$", body.username):
|
||||
JSONResponse(content={"message": "Invalid username"}, status_code=400)
|
||||
return JSONResponse(content={"message": "Invalid username"}, status_code=400)
|
||||
|
||||
role = body.role if body.role in ["admin", "viewer"] else "viewer"
|
||||
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
||||
|
||||
User.insert(
|
||||
{
|
||||
User.username: body.username,
|
||||
User.password_hash: password_hash,
|
||||
User.role: role,
|
||||
User.notification_tokens: [],
|
||||
}
|
||||
).execute()
|
||||
@@ -375,15 +448,61 @@ def delete_user(username: str):
|
||||
|
||||
|
||||
@router.put("/users/{username}/password")
|
||||
def update_password(request: Request, username: str, body: AppPutPasswordBody):
|
||||
async def update_password(
|
||||
request: Request,
|
||||
username: str,
|
||||
body: AppPutPasswordBody,
|
||||
):
|
||||
current_user = await get_current_user(request)
|
||||
if isinstance(current_user, JSONResponse):
|
||||
# auth failed
|
||||
return current_user
|
||||
|
||||
current_username = current_user.get("username")
|
||||
current_role = current_user.get("role")
|
||||
|
||||
# viewers can only change their own password
|
||||
if current_role == "viewer" and current_username != username:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Viewers can only update their own password"
|
||||
)
|
||||
|
||||
HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations
|
||||
|
||||
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
||||
User.set_by_id(username, {User.password_hash: password_hash})
|
||||
|
||||
User.set_by_id(
|
||||
username,
|
||||
{
|
||||
User.password_hash: password_hash,
|
||||
},
|
||||
)
|
||||
return JSONResponse(content={"success": True})
|
||||
|
||||
|
||||
@router.put(
|
||||
"/users/{username}/role",
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
async def update_role(
|
||||
request: Request,
|
||||
username: str,
|
||||
body: AppPutRoleBody,
|
||||
):
|
||||
current_user = await get_current_user(request)
|
||||
if isinstance(current_user, JSONResponse):
|
||||
# auth failed
|
||||
return current_user
|
||||
|
||||
current_role = current_user.get("role")
|
||||
# viewers can't change anyone's role
|
||||
if current_role == "viewer":
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Admin role is required to change user roles"
|
||||
)
|
||||
if username == "admin":
|
||||
return JSONResponse(
|
||||
content={"message": "Cannot modify admin user's role"}, status_code=403
|
||||
)
|
||||
if body.role not in ["admin", "viewer"]:
|
||||
return JSONResponse(
|
||||
content={"message": "Role must be 'admin' or 'viewer'"}, status_code=400
|
||||
)
|
||||
|
||||
User.set_by_id(username, {User.role: body.role})
|
||||
return JSONResponse(content={"success": True})
|
||||
|
||||
@@ -6,12 +6,13 @@ import random
|
||||
import shutil
|
||||
import string
|
||||
|
||||
from fastapi import APIRouter, Request, UploadFile
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from pathvalidate import sanitize_filename
|
||||
from peewee import DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.auth import require_role
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.const import FACE_DIR
|
||||
from frigate.embeddings import EmbeddingsContext
|
||||
@@ -44,7 +45,7 @@ def get_faces():
|
||||
return JSONResponse(status_code=200, content=face_dict)
|
||||
|
||||
|
||||
@router.post("/faces/reprocess")
|
||||
@router.post("/faces/reprocess", dependencies=[Depends(require_role(["admin"]))])
|
||||
def reclassify_face(request: Request, body: dict = None):
|
||||
if not request.app.frigate_config.face_recognition.enabled:
|
||||
return JSONResponse(
|
||||
@@ -121,7 +122,7 @@ def train_face(request: Request, name: str, body: dict = None):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/faces/{name}/create")
|
||||
@router.post("/faces/{name}/create", dependencies=[Depends(require_role(["admin"]))])
|
||||
async def create_face(request: Request, name: str):
|
||||
if not request.app.frigate_config.face_recognition.enabled:
|
||||
return JSONResponse(
|
||||
@@ -138,7 +139,7 @@ async def create_face(request: Request, name: str):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/faces/{name}/register")
|
||||
@router.post("/faces/{name}/register", dependencies=[Depends(require_role(["admin"]))])
|
||||
async def register_face(request: Request, name: str, file: UploadFile):
|
||||
if not request.app.frigate_config.face_recognition.enabled:
|
||||
return JSONResponse(
|
||||
@@ -154,7 +155,7 @@ async def register_face(request: Request, name: str, file: UploadFile):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/faces/{name}/delete")
|
||||
@router.post("/faces/{name}/delete", dependencies=[Depends(require_role(["admin"]))])
|
||||
def deregister_faces(request: Request, name: str, body: dict = None):
|
||||
if not request.app.frigate_config.face_recognition.enabled:
|
||||
return JSONResponse(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -12,8 +14,13 @@ class AppPutPasswordBody(BaseModel):
|
||||
class AppPostUsersBody(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
role: Optional[str] = "viewer"
|
||||
|
||||
|
||||
class AppPostLoginBody(BaseModel):
|
||||
user: str
|
||||
password: str
|
||||
|
||||
|
||||
class AppPutRoleBody(BaseModel):
|
||||
role: str
|
||||
|
||||
+46
-9
@@ -14,6 +14,7 @@ from fastapi.responses import JSONResponse
|
||||
from peewee import JOIN, DoesNotExist, fn, operator
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.auth import require_role
|
||||
from frigate.api.defs.query.events_query_parameters import (
|
||||
DEFAULT_TIME_RANGE,
|
||||
EventsQueryParams,
|
||||
@@ -708,7 +709,11 @@ def event(event_id: str):
|
||||
return JSONResponse(content="Event not found", status_code=404)
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/retain", response_model=GenericResponse)
|
||||
@router.post(
|
||||
"/events/{event_id}/retain",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def set_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
@@ -928,7 +933,11 @@ def false_positive(request: Request, event_id: str):
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/events/{event_id}/retain", response_model=GenericResponse)
|
||||
@router.delete(
|
||||
"/events/{event_id}/retain",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def delete_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == event_id)
|
||||
@@ -947,7 +956,11 @@ def delete_retain(event_id: str):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/sub_label", response_model=GenericResponse)
|
||||
@router.post(
|
||||
"/events/{event_id}/sub_label",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def set_sub_label(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
@@ -1022,7 +1035,11 @@ def set_sub_label(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/events/{event_id}/description", response_model=GenericResponse)
|
||||
@router.post(
|
||||
"/events/{event_id}/description",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def set_description(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
@@ -1069,7 +1086,11 @@ def set_description(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/events/{event_id}/description/regenerate", response_model=GenericResponse)
|
||||
@router.put(
|
||||
"/events/{event_id}/description/regenerate",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def regenerate_description(
|
||||
request: Request, event_id: str, params: RegenerateQueryParameters = Depends()
|
||||
):
|
||||
@@ -1137,14 +1158,22 @@ def delete_single_event(event_id: str, request: Request) -> dict:
|
||||
return {"success": True, "message": f"Event {event_id} deleted"}
|
||||
|
||||
|
||||
@router.delete("/events/{event_id}", response_model=GenericResponse)
|
||||
@router.delete(
|
||||
"/events/{event_id}",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def delete_event(request: Request, event_id: str):
|
||||
result = delete_single_event(event_id, request)
|
||||
status_code = 200 if result["success"] else 404
|
||||
return JSONResponse(content=result, status_code=status_code)
|
||||
|
||||
|
||||
@router.delete("/events/", response_model=EventMultiDeleteResponse)
|
||||
@router.delete(
|
||||
"/events/",
|
||||
response_model=EventMultiDeleteResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def delete_events(request: Request, body: EventsDeleteBody):
|
||||
if not body.event_ids:
|
||||
return JSONResponse(
|
||||
@@ -1170,7 +1199,11 @@ def delete_events(request: Request, body: EventsDeleteBody):
|
||||
return JSONResponse(content=response, status_code=200)
|
||||
|
||||
|
||||
@router.post("/events/{camera_name}/{label}/create", response_model=EventCreateResponse)
|
||||
@router.post(
|
||||
"/events/{camera_name}/{label}/create",
|
||||
response_model=EventCreateResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def create_event(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
@@ -1226,7 +1259,11 @@ def create_event(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/events/{event_id}/end", response_model=GenericResponse)
|
||||
@router.put(
|
||||
"/events/{event_id}/end",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def end_event(request: Request, event_id: str, body: EventsEndBody):
|
||||
try:
|
||||
end_time = body.end_time or datetime.datetime.now().timestamp()
|
||||
|
||||
@@ -6,11 +6,12 @@ import string
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from peewee import DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.auth import require_role
|
||||
from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody
|
||||
from frigate.api.defs.request.export_rename_body import ExportRenameBody
|
||||
from frigate.api.defs.tags import Tags
|
||||
@@ -130,7 +131,9 @@ def export_recording(
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/export/{event_id}/rename")
|
||||
@router.patch(
|
||||
"/export/{event_id}/rename", dependencies=[Depends(require_role(["admin"]))]
|
||||
)
|
||||
def export_rename(event_id: str, body: ExportRenameBody):
|
||||
try:
|
||||
export: Export = Export.get(Export.id == event_id)
|
||||
@@ -158,7 +161,7 @@ def export_rename(event_id: str, body: ExportRenameBody):
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/export/{event_id}")
|
||||
@router.delete("/export/{event_id}", dependencies=[Depends(require_role(["admin"]))])
|
||||
def export_delete(event_id: str):
|
||||
try:
|
||||
export: Export = Export.get(Export.id == event_id)
|
||||
|
||||
@@ -12,6 +12,7 @@ from fastapi.responses import JSONResponse
|
||||
from peewee import Case, DoesNotExist, fn, operator
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.auth import require_role
|
||||
from frigate.api.defs.query.review_query_parameters import (
|
||||
ReviewActivityMotionQueryParams,
|
||||
ReviewQueryParams,
|
||||
@@ -343,7 +344,11 @@ def set_multiple_reviewed(body: ReviewModifyMultipleBody):
|
||||
)
|
||||
|
||||
|
||||
@router.post("/reviews/delete", response_model=GenericResponse)
|
||||
@router.post(
|
||||
"/reviews/delete",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
)
|
||||
def delete_reviews(body: ReviewModifyMultipleBody):
|
||||
list_of_ids = body.ids
|
||||
reviews = (
|
||||
|
||||
Reference in New Issue
Block a user