refactoring-backend #3
@@ -128,6 +128,11 @@ _NamePath = Annotated[
|
||||
"",
|
||||
response_model=ActionListResponse,
|
||||
summary="List all available actions with active/inactive status",
|
||||
responses={
|
||||
200: {"description": "Action list returned", "model": ActionListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def list_actions(
|
||||
request: Request,
|
||||
@@ -165,6 +170,12 @@ async def list_actions(
|
||||
"/{name}",
|
||||
response_model=ActionConfig,
|
||||
summary="Return full parsed detail for a single action",
|
||||
responses={
|
||||
200: {"description": "Action config returned", "model": ActionConfig},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Action not found in action.d/"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_action(
|
||||
request: Request,
|
||||
@@ -205,6 +216,14 @@ async def get_action(
|
||||
response_model=ActionConfig,
|
||||
summary="Update an action's .local override with new lifecycle command values",
|
||||
dependencies=[Depends(_check_action_update_rate_limit)],
|
||||
responses={
|
||||
200: {"description": "Action updated", "model": ActionConfig},
|
||||
400: {"description": "Invalid action name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Action not found"},
|
||||
429: {"description": "Rate limit exceeded for action update operations"},
|
||||
500: {"description": "Failed to write .local file"},
|
||||
},
|
||||
)
|
||||
async def update_action(
|
||||
request: Request,
|
||||
@@ -246,6 +265,14 @@ async def update_action(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new user-defined action",
|
||||
dependencies=[Depends(_check_action_create_rate_limit)],
|
||||
responses={
|
||||
201: {"description": "Action created", "model": ActionConfig},
|
||||
400: {"description": "Invalid action name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "Action already exists"},
|
||||
429: {"description": "Rate limit exceeded for action create operations"},
|
||||
500: {"description": "Failed to write .local file"},
|
||||
},
|
||||
)
|
||||
async def create_action(
|
||||
request: Request,
|
||||
@@ -284,6 +311,15 @@ async def create_action(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete a user-created action's .local file",
|
||||
dependencies=[Depends(_check_action_delete_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Action deleted successfully"},
|
||||
400: {"description": "Invalid action name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Action not found"},
|
||||
409: {"description": "Action is a shipped default (conf-only)"},
|
||||
429: {"description": "Rate limit exceeded for action delete operations"},
|
||||
500: {"description": "Failed to delete .local file"},
|
||||
},
|
||||
)
|
||||
async def delete_action(
|
||||
request: Request,
|
||||
|
||||
@@ -45,6 +45,13 @@ router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
||||
"/login",
|
||||
response_model=LoginResponse,
|
||||
summary="Authenticate with the master password",
|
||||
responses={
|
||||
200: {"description": "Login successful", "model": LoginResponse},
|
||||
401: {"description": "Invalid password"},
|
||||
422: {"description": "Validation error — invalid request body"},
|
||||
429: {"description": "Too many login attempts, retry after delay"},
|
||||
503: {"description": "Setup not complete"},
|
||||
},
|
||||
)
|
||||
async def login(
|
||||
body: LoginRequest,
|
||||
@@ -116,6 +123,10 @@ async def login(
|
||||
"/session",
|
||||
response_model=SessionValidResponse,
|
||||
summary="Validate the current session",
|
||||
responses={
|
||||
200: {"description": "Session valid", "model": SessionValidResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def validate_session(
|
||||
_: AuthDep,
|
||||
@@ -142,6 +153,10 @@ async def validate_session(
|
||||
"/logout",
|
||||
response_model=LogoutResponse,
|
||||
summary="Revoke the current session",
|
||||
responses={
|
||||
200: {"description": "Logout successful", "model": LogoutResponse},
|
||||
401: {"description": "Session missing or invalid (silently successful)"},
|
||||
},
|
||||
)
|
||||
async def logout(
|
||||
request: Request,
|
||||
|
||||
@@ -101,6 +101,11 @@ def _check_unban_rate_limit(
|
||||
"/active",
|
||||
response_model=ActiveBanListResponse,
|
||||
summary="List all currently banned IPs across all jails",
|
||||
responses={
|
||||
200: {"description": "Active ban list returned", "model": ActiveBanListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_active_bans(
|
||||
request: Request,
|
||||
@@ -144,6 +149,15 @@ async def get_active_bans(
|
||||
response_model=JailCommandResponse,
|
||||
summary="Ban an IP address in a specific jail",
|
||||
dependencies=[Depends(_check_ban_rate_limit)],
|
||||
responses={
|
||||
201: {"description": "IP banned successfully", "model": JailCommandResponse},
|
||||
400: {"description": "Invalid IP address"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "Ban command failed in fail2ban"},
|
||||
429: {"description": "Rate limit exceeded for ban operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def ban_ip(
|
||||
request: Request,
|
||||
@@ -182,6 +196,15 @@ async def ban_ip(
|
||||
response_model=JailCommandResponse,
|
||||
summary="Unban an IP address from one or all jails",
|
||||
dependencies=[Depends(_check_unban_rate_limit)],
|
||||
responses={
|
||||
200: {"description": "IP unbanned successfully", "model": JailCommandResponse},
|
||||
400: {"description": "Invalid IP address"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "Unban command failed in fail2ban"},
|
||||
429: {"description": "Rate limit exceeded for unban operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def unban_ip(
|
||||
request: Request,
|
||||
@@ -225,6 +248,11 @@ async def unban_ip(
|
||||
"/all",
|
||||
response_model=UnbanAllResponse,
|
||||
summary="Unban every currently banned IP across all jails",
|
||||
responses={
|
||||
200: {"description": "All bans cleared", "model": UnbanAllResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def unban_all(
|
||||
request: Request,
|
||||
|
||||
@@ -97,6 +97,10 @@ def _check_blocklist_import_rate_limit(
|
||||
"",
|
||||
response_model=BlocklistListResponse,
|
||||
summary="List all blocklist sources",
|
||||
responses={
|
||||
200: {"description": "Blocklist sources returned", "model": BlocklistListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def list_blocklists(
|
||||
blocklist_ctx: BlocklistServiceContextDep,
|
||||
@@ -120,6 +124,11 @@ async def list_blocklists(
|
||||
response_model=BlocklistSource,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Add a new blocklist source",
|
||||
responses={
|
||||
201: {"description": "Blocklist source created", "model": BlocklistSource},
|
||||
400: {"description": "URL validation failed"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def create_blocklist(
|
||||
payload: BlocklistSourceCreate,
|
||||
@@ -157,6 +166,11 @@ async def create_blocklist(
|
||||
response_model=ImportRunResult,
|
||||
summary="Trigger a manual blocklist import",
|
||||
dependencies=[Depends(_check_blocklist_import_rate_limit)],
|
||||
responses={
|
||||
200: {"description": "Import completed", "model": ImportRunResult},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
429: {"description": "Rate limit exceeded for blocklist import"},
|
||||
},
|
||||
)
|
||||
async def run_import_now(
|
||||
http_session: HttpSessionDep,
|
||||
@@ -193,6 +207,10 @@ async def run_import_now(
|
||||
"/schedule",
|
||||
response_model=ScheduleInfo,
|
||||
summary="Get the current import schedule",
|
||||
responses={
|
||||
200: {"description": "Schedule info returned", "model": ScheduleInfo},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def get_schedule(
|
||||
blocklist_ctx: BlocklistServiceContextDep,
|
||||
@@ -219,6 +237,10 @@ async def get_schedule(
|
||||
"/schedule",
|
||||
response_model=ScheduleInfo,
|
||||
summary="Update the import schedule",
|
||||
responses={
|
||||
200: {"description": "Schedule updated", "model": ScheduleInfo},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def update_schedule(
|
||||
payload: ScheduleConfig,
|
||||
@@ -255,6 +277,10 @@ async def update_schedule(
|
||||
"/log",
|
||||
response_model=ImportLogListResponse,
|
||||
summary="Get the paginated import log",
|
||||
responses={
|
||||
200: {"description": "Import log returned", "model": ImportLogListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def get_import_log(
|
||||
blocklist_ctx: BlocklistServiceContextDep,
|
||||
@@ -291,6 +317,11 @@ async def get_import_log(
|
||||
"/{source_id}",
|
||||
response_model=BlocklistSource,
|
||||
summary="Get a single blocklist source",
|
||||
responses={
|
||||
200: {"description": "Blocklist source returned", "model": BlocklistSource},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Blocklist source not found"},
|
||||
},
|
||||
)
|
||||
async def get_blocklist(
|
||||
source_id: int,
|
||||
@@ -317,6 +348,12 @@ async def get_blocklist(
|
||||
"/{source_id}",
|
||||
response_model=BlocklistSource,
|
||||
summary="Update a blocklist source",
|
||||
responses={
|
||||
200: {"description": "Blocklist source updated", "model": BlocklistSource},
|
||||
400: {"description": "URL validation failed"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Blocklist source not found"},
|
||||
},
|
||||
)
|
||||
async def update_blocklist(
|
||||
source_id: int,
|
||||
@@ -355,6 +392,11 @@ async def update_blocklist(
|
||||
"/{source_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete a blocklist source",
|
||||
responses={
|
||||
204: {"description": "Blocklist source deleted successfully"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Blocklist source not found"},
|
||||
},
|
||||
)
|
||||
async def delete_blocklist(
|
||||
source_id: int,
|
||||
@@ -380,6 +422,12 @@ async def delete_blocklist(
|
||||
"/{source_id}/preview",
|
||||
response_model=PreviewResponse,
|
||||
summary="Preview the contents of a blocklist source",
|
||||
responses={
|
||||
200: {"description": "Blocklist preview returned", "model": PreviewResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Blocklist source not found"},
|
||||
502: {"description": "URL could not be reached"},
|
||||
},
|
||||
)
|
||||
async def preview_blocklist(
|
||||
source_id: int,
|
||||
|
||||
@@ -111,6 +111,11 @@ def _validate_log_target(value: str) -> None:
|
||||
"/global",
|
||||
response_model=GlobalConfigResponse,
|
||||
summary="Return global fail2ban settings",
|
||||
responses={
|
||||
200: {"description": "Global config returned", "model": GlobalConfigResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_global_config(
|
||||
_request: Request,
|
||||
@@ -140,6 +145,13 @@ async def get_global_config(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update global fail2ban settings",
|
||||
dependencies=[Depends(_check_config_update_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Global config updated successfully"},
|
||||
400: {"description": "Set command rejected or log_target invalid"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
429: {"description": "Rate limit exceeded for config update operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def update_global_config(
|
||||
_request: Request,
|
||||
@@ -171,6 +183,12 @@ async def update_global_config(
|
||||
"/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Reload fail2ban to apply configuration changes",
|
||||
responses={
|
||||
204: {"description": "Fail2ban reloaded successfully"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "Reload command failed in fail2ban"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def reload_fail2ban(
|
||||
_request: Request,
|
||||
@@ -199,6 +217,13 @@ async def reload_fail2ban(
|
||||
"/restart",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Restart the fail2ban service",
|
||||
responses={
|
||||
204: {"description": "Fail2ban restarted successfully"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "Stop command failed in fail2ban"},
|
||||
502: {"description": "fail2ban unreachable for stop command"},
|
||||
503: {"description": "fail2ban did not come back online within 10s"},
|
||||
},
|
||||
)
|
||||
async def restart_fail2ban(
|
||||
_request: Request,
|
||||
@@ -252,6 +277,11 @@ async def restart_fail2ban(
|
||||
"/regex-test",
|
||||
response_model=RegexTestResponse,
|
||||
summary="Test a fail regex pattern against a sample log line",
|
||||
responses={
|
||||
200: {"description": "Regex test result", "model": RegexTestResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
422: {"description": "Invalid regex pattern"},
|
||||
},
|
||||
)
|
||||
async def regex_test(
|
||||
_auth: AuthDep,
|
||||
@@ -281,6 +311,11 @@ async def regex_test(
|
||||
"/preview-log",
|
||||
response_model=LogPreviewResponse,
|
||||
summary="Preview log file lines against a regex pattern",
|
||||
responses={
|
||||
200: {"description": "Log preview result", "model": LogPreviewResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
422: {"description": "Invalid regex pattern"},
|
||||
},
|
||||
)
|
||||
async def preview_log(
|
||||
_auth: AuthDep,
|
||||
@@ -310,6 +345,10 @@ async def preview_log(
|
||||
"/map-color-thresholds",
|
||||
response_model=MapColorThresholdsResponse,
|
||||
summary="Get map color threshold configuration",
|
||||
responses={
|
||||
200: {"description": "Color thresholds returned", "model": MapColorThresholdsResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def get_map_color_thresholds(
|
||||
_request: Request,
|
||||
@@ -334,6 +373,12 @@ async def get_map_color_thresholds(
|
||||
response_model=MapColorThresholdsResponse,
|
||||
summary="Update map color threshold configuration",
|
||||
dependencies=[Depends(_check_config_update_rate_limit)],
|
||||
responses={
|
||||
200: {"description": "Color thresholds updated", "model": MapColorThresholdsResponse},
|
||||
400: {"description": "Validation error (thresholds not properly ordered)"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
429: {"description": "Rate limit exceeded for config update operations"},
|
||||
},
|
||||
)
|
||||
async def update_map_color_thresholds(
|
||||
_request: Request,
|
||||
@@ -365,6 +410,12 @@ async def update_map_color_thresholds(
|
||||
"/fail2ban-log",
|
||||
response_model=Fail2BanLogResponse,
|
||||
summary="Read the tail of the fail2ban daemon log file",
|
||||
responses={
|
||||
200: {"description": "Log file lines returned", "model": Fail2BanLogResponse},
|
||||
400: {"description": "Log target not a file or path outside allowed directory"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_fail2ban_log(
|
||||
_request: Request,
|
||||
@@ -416,6 +467,11 @@ async def get_fail2ban_log(
|
||||
"/service-status",
|
||||
response_model=ServiceStatusResponse,
|
||||
summary="Return fail2ban service health status with log configuration",
|
||||
responses={
|
||||
200: {"description": "Service status returned", "model": ServiceStatusResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_service_status(
|
||||
_request: Request,
|
||||
|
||||
@@ -56,6 +56,11 @@ _DEFAULT_RANGE: TimeRange = "24h"
|
||||
"/status",
|
||||
response_model=ServerStatusResponse,
|
||||
summary="Return the cached fail2ban server status",
|
||||
responses={
|
||||
200: {"description": "Server status returned", "model": ServerStatusResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_server_status(
|
||||
server_status: ServerStatusDep,
|
||||
@@ -84,6 +89,11 @@ async def get_server_status(
|
||||
"/bans",
|
||||
response_model=DashboardBanListResponse,
|
||||
summary="Return a paginated list of recent bans",
|
||||
responses={
|
||||
200: {"description": "Ban list returned", "model": DashboardBanListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_dashboard_bans(
|
||||
_auth: AuthDep,
|
||||
@@ -145,6 +155,11 @@ async def get_dashboard_bans(
|
||||
"/bans/by-country",
|
||||
response_model=BansByCountryResponse,
|
||||
summary="Return ban counts aggregated by country",
|
||||
responses={
|
||||
200: {"description": "Ban counts by country returned", "model": BansByCountryResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_bans_by_country(
|
||||
_auth: AuthDep,
|
||||
@@ -205,6 +220,11 @@ async def get_bans_by_country(
|
||||
"/bans/trend",
|
||||
response_model=BanTrendResponse,
|
||||
summary="Return ban counts aggregated into time buckets",
|
||||
responses={
|
||||
200: {"description": "Ban trend data returned", "model": BanTrendResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_ban_trend(
|
||||
_auth: AuthDep,
|
||||
@@ -259,6 +279,11 @@ async def get_ban_trend(
|
||||
"/bans/by-jail",
|
||||
response_model=BansByJailResponse,
|
||||
summary="Return ban counts aggregated by jail",
|
||||
responses={
|
||||
200: {"description": "Ban counts by jail returned", "model": BansByJailResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_bans_by_jail(
|
||||
_auth: AuthDep,
|
||||
|
||||
@@ -75,6 +75,11 @@ _NamePath = Annotated[
|
||||
"/jail-files",
|
||||
response_model=JailConfigFilesResponse,
|
||||
summary="List all jail config files",
|
||||
responses={
|
||||
200: {"description": "Jail config files returned", "model": JailConfigFilesResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def list_jail_config_files(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -97,6 +102,13 @@ async def list_jail_config_files(
|
||||
"/jail-files/{filename}",
|
||||
response_model=JailConfigFileContent,
|
||||
summary="Return a single jail config file with its content",
|
||||
responses={
|
||||
200: {"description": "Jail config file returned", "model": JailConfigFileContent},
|
||||
400: {"description": "Filename unsafe"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "File not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def get_jail_config_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -123,6 +135,13 @@ async def get_jail_config_file(
|
||||
"/jail-files/{filename}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Overwrite a jail.d config file with new raw content",
|
||||
responses={
|
||||
204: {"description": "File overwritten successfully"},
|
||||
400: {"description": "Filename unsafe or content invalid"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "File not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def write_jail_config_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -151,6 +170,13 @@ async def write_jail_config_file(
|
||||
"/jail-files/{filename}/enabled",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Enable or disable a jail configuration file",
|
||||
responses={
|
||||
204: {"description": "Enabled state updated successfully"},
|
||||
400: {"description": "Filename unsafe or operation failed"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "File not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def set_jail_config_file_enabled(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -182,6 +208,13 @@ async def set_jail_config_file_enabled(
|
||||
response_model=ConfFileContent,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new jail.d config file",
|
||||
responses={
|
||||
201: {"description": "File created", "model": ConfFileContent},
|
||||
400: {"description": "Name unsafe or content exceeds size limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "File with that name already exists"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def create_jail_config_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -220,6 +253,13 @@ async def create_jail_config_file(
|
||||
"/filters/{name}/raw",
|
||||
response_model=ConfFileContent,
|
||||
summary="Return a filter definition file's raw content",
|
||||
responses={
|
||||
200: {"description": "Filter file returned", "model": ConfFileContent},
|
||||
400: {"description": "Name unsafe"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "File not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def get_filter_file_raw(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -250,6 +290,13 @@ async def get_filter_file_raw(
|
||||
"/filters/{name}/raw",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update a filter definition file (raw content)",
|
||||
responses={
|
||||
204: {"description": "Filter file updated successfully"},
|
||||
400: {"description": "Name unsafe or content exceeds size limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "File not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def write_filter_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -276,6 +323,13 @@ async def write_filter_file(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=ConfFileContent,
|
||||
summary="Create a new filter definition file (raw content)",
|
||||
responses={
|
||||
201: {"description": "Filter file created", "model": ConfFileContent},
|
||||
400: {"description": "Name invalid or content exceeds limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "File with that name already exists"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def create_filter_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -314,6 +368,11 @@ async def create_filter_file(
|
||||
"/actions",
|
||||
response_model=ConfFilesResponse,
|
||||
summary="List all action definition files",
|
||||
responses={
|
||||
200: {"description": "Action files returned", "model": ConfFilesResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def list_action_files(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -333,6 +392,13 @@ async def list_action_files(
|
||||
"/actions/{name}/raw",
|
||||
response_model=ConfFileContent,
|
||||
summary="Return an action definition file with its content",
|
||||
responses={
|
||||
200: {"description": "Action file returned", "model": ConfFileContent},
|
||||
400: {"description": "Name unsafe"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "File not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def get_action_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -359,6 +425,13 @@ async def get_action_file(
|
||||
"/actions/{name}/raw",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update an action definition file",
|
||||
responses={
|
||||
204: {"description": "Action file updated successfully"},
|
||||
400: {"description": "Name unsafe or content exceeds size limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "File not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def write_action_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -385,6 +458,13 @@ async def write_action_file(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=ConfFileContent,
|
||||
summary="Create a new action definition file",
|
||||
responses={
|
||||
201: {"description": "Action file created", "model": ConfFileContent},
|
||||
400: {"description": "Name invalid or content exceeds limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "File with that name already exists"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def create_action_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -423,6 +503,13 @@ async def create_action_file(
|
||||
"/filters/{name}/parsed",
|
||||
response_model=FilterConfig,
|
||||
summary="Return a filter file parsed into a structured model",
|
||||
responses={
|
||||
200: {"description": "Filter config returned", "model": FilterConfig},
|
||||
400: {"description": "Name unsafe"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Filter file not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def get_parsed_filter(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -453,6 +540,13 @@ async def get_parsed_filter(
|
||||
"/filters/{name}/parsed",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update a filter file from a structured model",
|
||||
responses={
|
||||
204: {"description": "Filter file updated successfully"},
|
||||
400: {"description": "Name unsafe or content exceeds size limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Filter file not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def update_parsed_filter(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -486,6 +580,13 @@ async def update_parsed_filter(
|
||||
"/actions/{name}/parsed",
|
||||
response_model=ActionConfig,
|
||||
summary="Return an action file parsed into a structured model",
|
||||
responses={
|
||||
200: {"description": "Action config returned", "model": ActionConfig},
|
||||
400: {"description": "Name unsafe"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Action file not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def get_parsed_action(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -516,6 +617,13 @@ async def get_parsed_action(
|
||||
"/actions/{name}/parsed",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update an action file from a structured model",
|
||||
responses={
|
||||
204: {"description": "Action file updated successfully"},
|
||||
400: {"description": "Name unsafe or content exceeds size limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Action file not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def update_parsed_action(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -549,6 +657,13 @@ async def update_parsed_action(
|
||||
"/jail-files/{filename}/parsed",
|
||||
response_model=JailFileConfig,
|
||||
summary="Return a jail.d file parsed into a structured model",
|
||||
responses={
|
||||
200: {"description": "Jail file config returned", "model": JailFileConfig},
|
||||
400: {"description": "Filename unsafe"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail file not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def get_parsed_jail_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
@@ -579,6 +694,13 @@ async def get_parsed_jail_file(
|
||||
"/jail-files/{filename}/parsed",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update a jail.d file from a structured model",
|
||||
responses={
|
||||
204: {"description": "Jail file updated successfully"},
|
||||
400: {"description": "Filename unsafe or content exceeds size limit"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail file not found"},
|
||||
503: {"description": "Config directory unavailable"},
|
||||
},
|
||||
)
|
||||
async def update_parsed_jail_file(
|
||||
config_dir: Fail2BanConfigDirDep,
|
||||
|
||||
@@ -125,6 +125,11 @@ _FilterNamePath = Annotated[
|
||||
"",
|
||||
response_model=FilterListResponse,
|
||||
summary="List all available filters with active/inactive status",
|
||||
responses={
|
||||
200: {"description": "Filter list returned", "model": FilterListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def list_filters(
|
||||
request: Request,
|
||||
@@ -163,6 +168,12 @@ async def list_filters(
|
||||
"/{name}",
|
||||
response_model=FilterConfig,
|
||||
summary="Return full parsed detail for a single filter",
|
||||
responses={
|
||||
200: {"description": "Filter config returned", "model": FilterConfig},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Filter not found in filter.d/"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_filter(
|
||||
request: Request,
|
||||
@@ -208,6 +219,15 @@ _FilterNamePath = Annotated[
|
||||
response_model=FilterConfig,
|
||||
summary="Update a filter's .local override with new regex/pattern values",
|
||||
dependencies=[Depends(_check_filter_update_rate_limit)],
|
||||
responses={
|
||||
200: {"description": "Filter updated", "model": FilterConfig},
|
||||
400: {"description": "Invalid filter name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Filter not found"},
|
||||
422: {"description": "Regex pattern failed to compile"},
|
||||
429: {"description": "Rate limit exceeded for filter update operations"},
|
||||
500: {"description": "Failed to write .local file"},
|
||||
},
|
||||
)
|
||||
async def update_filter(
|
||||
request: Request,
|
||||
@@ -260,6 +280,15 @@ async def update_filter(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new user-defined filter",
|
||||
dependencies=[Depends(_check_filter_create_rate_limit)],
|
||||
responses={
|
||||
201: {"description": "Filter created", "model": FilterConfig},
|
||||
400: {"description": "Invalid filter name or regex too long"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "Filter already exists"},
|
||||
422: {"description": "Regex pattern failed to compile"},
|
||||
429: {"description": "Rate limit exceeded for filter create operations"},
|
||||
500: {"description": "Failed to write .local file"},
|
||||
},
|
||||
)
|
||||
async def create_filter(
|
||||
request: Request,
|
||||
@@ -309,6 +338,15 @@ async def create_filter(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete a user-created filter's .local file",
|
||||
dependencies=[Depends(_check_filter_delete_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Filter deleted successfully"},
|
||||
400: {"description": "Invalid filter name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Filter not found"},
|
||||
409: {"description": "Filter is a shipped default (conf-only)"},
|
||||
429: {"description": "Rate limit exceeded for filter delete operations"},
|
||||
500: {"description": "Failed to delete .local file"},
|
||||
},
|
||||
)
|
||||
async def delete_filter(
|
||||
request: Request,
|
||||
|
||||
@@ -30,6 +30,12 @@ _IpPath = Annotated[str, Path(description="IPv4 or IPv6 address to look up.")]
|
||||
"/lookup/{ip}",
|
||||
response_model=IpLookupResponse,
|
||||
summary="Look up ban status and geo information for an IP",
|
||||
responses={
|
||||
200: {"description": "IP lookup result returned", "model": IpLookupResponse},
|
||||
400: {"description": "Invalid IP address"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def lookup_ip(
|
||||
_auth: AuthDep,
|
||||
@@ -75,6 +81,10 @@ async def lookup_ip(
|
||||
"/stats",
|
||||
response_model=GeoCacheStatsResponse,
|
||||
summary="Geo cache diagnostic counters",
|
||||
responses={
|
||||
200: {"description": "Geo cache stats returned", "model": GeoCacheStatsResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def geo_stats(
|
||||
_auth: AuthDep,
|
||||
@@ -99,6 +109,10 @@ async def geo_stats(
|
||||
"/re-resolve",
|
||||
summary="Re-resolve all IPs whose country could not be determined",
|
||||
response_model=GeoReResolveResponse,
|
||||
responses={
|
||||
200: {"description": "Re-resolve result", "model": GeoReResolveResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def re_resolve_geo(
|
||||
_auth: AuthDep,
|
||||
|
||||
@@ -28,7 +28,15 @@ router: APIRouter = APIRouter(prefix="/api/v1/health", tags=["Health"])
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
|
||||
|
||||
@router.get("", summary="Application health check", response_model=HealthResponse)
|
||||
@router.get(
|
||||
"",
|
||||
summary="Application health check",
|
||||
response_model=HealthResponse,
|
||||
responses={
|
||||
200: {"description": "All components healthy"},
|
||||
503: {"description": "fail2ban offline or component degraded"},
|
||||
},
|
||||
)
|
||||
async def health_check(
|
||||
app_state: AppStateDep,
|
||||
server_status: ServerStatusDep,
|
||||
|
||||
@@ -41,6 +41,11 @@ router: APIRouter = APIRouter(prefix="/api/v1/history", tags=["History"])
|
||||
"",
|
||||
response_model=HistoryListResponse,
|
||||
summary="Return a paginated list of historical bans",
|
||||
responses={
|
||||
200: {"description": "History list returned", "model": HistoryListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_history(
|
||||
request: Request,
|
||||
@@ -120,6 +125,11 @@ async def get_history(
|
||||
"/archive",
|
||||
response_model=HistoryListResponse,
|
||||
summary="Return a paginated list of archived historical bans",
|
||||
responses={
|
||||
200: {"description": "Archived history list returned", "model": HistoryListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_history_archive(
|
||||
request: Request,
|
||||
@@ -157,6 +167,12 @@ async def get_history_archive(
|
||||
"/{ip}",
|
||||
response_model=IpDetailResponse,
|
||||
summary="Return the full ban history for a single IP address",
|
||||
responses={
|
||||
200: {"description": "IP history detail returned", "model": IpDetailResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "No history found for this IP"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_ip_history(
|
||||
request: Request,
|
||||
|
||||
@@ -213,6 +213,11 @@ _NamePath = Annotated[str, Path(description='Jail name as configured in fail2ban
|
||||
"",
|
||||
response_model=JailConfigListResponse,
|
||||
summary="List configuration for all active jails",
|
||||
responses={
|
||||
200: {"description": "Jail configs returned", "model": JailConfigListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_jail_configs(
|
||||
request: Request,
|
||||
@@ -241,6 +246,11 @@ async def get_jail_configs(
|
||||
"/inactive",
|
||||
response_model=InactiveJailListResponse,
|
||||
summary="List all inactive jails discovered in config files",
|
||||
responses={
|
||||
200: {"description": "Inactive jail list returned", "model": InactiveJailListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_inactive_jails(
|
||||
request: Request,
|
||||
@@ -268,6 +278,10 @@ async def get_inactive_jails(
|
||||
"/pending-recovery",
|
||||
response_model=PendingRecovery | None,
|
||||
summary="Return active crash-recovery record if one exists",
|
||||
responses={
|
||||
200: {"description": "Recovery record or null", "model": PendingRecovery},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
},
|
||||
)
|
||||
async def get_pending_recovery(
|
||||
_auth: AuthDep,
|
||||
@@ -293,6 +307,12 @@ async def get_pending_recovery(
|
||||
"/{name}",
|
||||
response_model=JailConfigResponse,
|
||||
summary="Return configuration for a single jail",
|
||||
responses={
|
||||
200: {"description": "Jail config returned", "model": JailConfigResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_jail_config(
|
||||
request: Request,
|
||||
@@ -325,6 +345,15 @@ async def get_jail_config(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update jail configuration",
|
||||
dependencies=[Depends(_check_jail_update_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Jail config updated successfully"},
|
||||
400: {"description": "Set command rejected or invalid regex"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
422: {"description": "Regex pattern failed to compile"},
|
||||
429: {"description": "Rate limit exceeded for jail update operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def update_jail_config(
|
||||
request: Request,
|
||||
@@ -365,6 +394,14 @@ async def update_jail_config(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Add a log file path to an existing jail",
|
||||
dependencies=[Depends(_check_jail_create_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Log path added successfully"},
|
||||
400: {"description": "Command rejected or path invalid"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
429: {"description": "Rate limit exceeded for jail create operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def add_log_path(
|
||||
request: Request,
|
||||
@@ -400,6 +437,15 @@ async def add_log_path(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Remove a monitored log path from a jail",
|
||||
dependencies=[Depends(_check_jail_delete_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Log path removed successfully"},
|
||||
400: {"description": "Command rejected"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
422: {"description": "Log path outside allowed directories"},
|
||||
429: {"description": "Rate limit exceeded for jail delete operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def delete_log_path(
|
||||
request: Request,
|
||||
@@ -440,6 +486,15 @@ async def delete_log_path(
|
||||
response_model=JailActivationResponse,
|
||||
summary="Activate an inactive jail",
|
||||
dependencies=[Depends(_check_jail_activate_rate_limit)],
|
||||
responses={
|
||||
200: {"description": "Jail activated", "model": JailActivationResponse},
|
||||
400: {"description": "Invalid jail name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found in config files"},
|
||||
409: {"description": "Jail already active"},
|
||||
429: {"description": "Rate limit exceeded for jail activate operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def activate_jail(
|
||||
app: AppDep,
|
||||
@@ -494,6 +549,15 @@ async def activate_jail(
|
||||
response_model=JailActivationResponse,
|
||||
summary="Deactivate an active jail",
|
||||
dependencies=[Depends(_check_jail_deactivate_rate_limit)],
|
||||
responses={
|
||||
200: {"description": "Jail deactivated", "model": JailActivationResponse},
|
||||
400: {"description": "Invalid jail name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found in config files"},
|
||||
409: {"description": "Jail already inactive"},
|
||||
429: {"description": "Rate limit exceeded for jail deactivate operations"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def deactivate_jail(
|
||||
_auth: AuthDep,
|
||||
@@ -536,6 +600,15 @@ async def deactivate_jail(
|
||||
"/{name}/local",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete the jail.d override file for an inactive jail",
|
||||
responses={
|
||||
204: {"description": "Override file deleted successfully"},
|
||||
400: {"description": "Invalid jail name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found in config files"},
|
||||
409: {"description": "Jail currently active"},
|
||||
500: {"description": "File cannot be deleted"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def delete_jail_local_override(
|
||||
request: Request,
|
||||
@@ -578,6 +651,12 @@ async def delete_jail_local_override(
|
||||
"/{name}/validate",
|
||||
response_model=JailValidationResult,
|
||||
summary="Validate jail configuration before activation",
|
||||
responses={
|
||||
200: {"description": "Validation result", "model": JailValidationResult},
|
||||
400: {"description": "Invalid jail name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found in config files"},
|
||||
},
|
||||
)
|
||||
async def validate_jail(
|
||||
request: Request,
|
||||
@@ -609,6 +688,12 @@ async def validate_jail(
|
||||
"/{name}/rollback",
|
||||
response_model=RollbackResponse,
|
||||
summary="Disable a bad jail config and restart fail2ban",
|
||||
responses={
|
||||
200: {"description": "Rollback completed", "model": RollbackResponse},
|
||||
400: {"description": "Invalid jail name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
500: {"description": "Failed to write .local override file"},
|
||||
},
|
||||
)
|
||||
async def rollback_jail(
|
||||
_auth: AuthDep,
|
||||
@@ -654,6 +739,14 @@ async def rollback_jail(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Assign a filter to a jail",
|
||||
dependencies=[Depends(_check_jail_create_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Filter assigned successfully"},
|
||||
400: {"description": "Invalid jail name or filter name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail or filter not found"},
|
||||
429: {"description": "Rate limit exceeded for jail create operations"},
|
||||
500: {"description": "Failed to write .local override file"},
|
||||
},
|
||||
)
|
||||
async def assign_filter_to_jail(
|
||||
request: Request,
|
||||
@@ -689,6 +782,14 @@ async def assign_filter_to_jail(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Add an action to a jail",
|
||||
dependencies=[Depends(_check_jail_create_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Action added successfully"},
|
||||
400: {"description": "Invalid jail name or action name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail or action not found"},
|
||||
429: {"description": "Rate limit exceeded for jail create operations"},
|
||||
500: {"description": "Failed to write .local override file"},
|
||||
},
|
||||
)
|
||||
async def assign_action_to_jail(
|
||||
request: Request,
|
||||
@@ -725,6 +826,14 @@ async def assign_action_to_jail(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Remove an action from a jail",
|
||||
dependencies=[Depends(_check_jail_delete_rate_limit)],
|
||||
responses={
|
||||
204: {"description": "Action removed successfully"},
|
||||
400: {"description": "Invalid jail name or action name"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found in config files"},
|
||||
429: {"description": "Rate limit exceeded for jail delete operations"},
|
||||
500: {"description": "Failed to write .local override file"},
|
||||
},
|
||||
)
|
||||
async def remove_action_from_jail(
|
||||
request: Request,
|
||||
|
||||
@@ -57,6 +57,11 @@ _NamePath = Annotated[str, Path(description="Jail name as configured in fail2ban
|
||||
"",
|
||||
response_model=JailListResponse,
|
||||
summary="List all active fail2ban jails",
|
||||
responses={
|
||||
200: {"description": "Jails list returned", "model": JailListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_jails(
|
||||
_auth: AuthDep,
|
||||
@@ -85,6 +90,12 @@ async def get_jails(
|
||||
"/{name}",
|
||||
response_model=JailDetailResponse,
|
||||
summary="Return full detail for a single jail",
|
||||
responses={
|
||||
200: {"description": "Jail detail returned", "model": JailDetailResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_jail(
|
||||
_auth: AuthDep,
|
||||
@@ -129,6 +140,12 @@ async def get_jail(
|
||||
"/reload-all",
|
||||
response_model=JailCommandResponse,
|
||||
summary="Reload all fail2ban jails",
|
||||
responses={
|
||||
200: {"description": "All jails reloaded", "model": JailCommandResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def reload_all_jails(
|
||||
_auth: AuthDep,
|
||||
@@ -157,6 +174,13 @@ async def reload_all_jails(
|
||||
"/{name}/start",
|
||||
response_model=JailCommandResponse,
|
||||
summary="Start a stopped jail",
|
||||
responses={
|
||||
200: {"description": "Jail started", "model": JailCommandResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def start_jail(
|
||||
_auth: AuthDep,
|
||||
@@ -185,6 +209,12 @@ async def start_jail(
|
||||
"/{name}/stop",
|
||||
response_model=JailCommandResponse,
|
||||
summary="Stop a running jail",
|
||||
responses={
|
||||
200: {"description": "Jail stopped", "model": JailCommandResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def stop_jail(
|
||||
_auth: AuthDep,
|
||||
@@ -216,6 +246,13 @@ async def stop_jail(
|
||||
"/{name}/idle",
|
||||
response_model=JailCommandResponse,
|
||||
summary="Toggle idle mode for a jail",
|
||||
responses={
|
||||
200: {"description": "Idle mode toggled", "model": JailCommandResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def toggle_idle(
|
||||
_auth: AuthDep,
|
||||
@@ -253,6 +290,13 @@ async def toggle_idle(
|
||||
"/{name}/reload",
|
||||
response_model=JailCommandResponse,
|
||||
summary="Reload a single jail",
|
||||
responses={
|
||||
200: {"description": "Jail reloaded", "model": JailCommandResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def reload_jail(
|
||||
_auth: AuthDep,
|
||||
@@ -294,6 +338,12 @@ class _IgnoreSelfRequest(IgnoreIpRequest):
|
||||
"/{name}/ignoreip",
|
||||
response_model=IgnoreListResponse,
|
||||
summary="List the ignore IPs for a jail",
|
||||
responses={
|
||||
200: {"description": "Ignore list returned", "model": IgnoreListResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_ignore_list(
|
||||
_auth: AuthDep,
|
||||
@@ -322,6 +372,14 @@ async def get_ignore_list(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=JailCommandResponse,
|
||||
summary="Add an IP or network to the ignore list",
|
||||
responses={
|
||||
201: {"description": "IP added to ignore list", "model": JailCommandResponse},
|
||||
400: {"description": "IP or network invalid"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def add_ignore_ip(
|
||||
_auth: AuthDep,
|
||||
@@ -359,6 +417,13 @@ async def add_ignore_ip(
|
||||
"/{name}/ignoreip",
|
||||
response_model=JailCommandResponse,
|
||||
summary="Remove an IP or network from the ignore list",
|
||||
responses={
|
||||
200: {"description": "IP removed from ignore list", "model": JailCommandResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def del_ignore_ip(
|
||||
_auth: AuthDep,
|
||||
@@ -392,6 +457,13 @@ async def del_ignore_ip(
|
||||
"/{name}/ignoreself",
|
||||
response_model=JailCommandResponse,
|
||||
summary="Toggle the ignoreself option for a jail",
|
||||
responses={
|
||||
200: {"description": "ignoreself toggled", "model": JailCommandResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
409: {"description": "fail2ban reports operation failed"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def toggle_ignore_self(
|
||||
_auth: AuthDep,
|
||||
@@ -434,6 +506,13 @@ async def toggle_ignore_self(
|
||||
"/{name}/banned",
|
||||
response_model=JailBannedIpsResponse,
|
||||
summary="Return paginated currently-banned IPs for a single jail",
|
||||
responses={
|
||||
200: {"description": "Banned IPs returned", "model": JailBannedIpsResponse},
|
||||
400: {"description": "page or page_size out of range"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
404: {"description": "Jail not found"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_jail_banned_ips(
|
||||
_auth: AuthDep,
|
||||
|
||||
@@ -30,6 +30,11 @@ router: APIRouter = APIRouter(prefix="/api/v1/server", tags=["Server"])
|
||||
"/settings",
|
||||
response_model=ServerSettingsResponse,
|
||||
summary="Return fail2ban server-level settings",
|
||||
responses={
|
||||
200: {"description": "Server settings returned", "model": ServerSettingsResponse},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def get_server_settings(
|
||||
request: Request,
|
||||
@@ -59,6 +64,12 @@ async def get_server_settings(
|
||||
"/settings",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Update fail2ban server-level settings",
|
||||
responses={
|
||||
204: {"description": "Settings updated successfully"},
|
||||
400: {"description": "Set command rejected by fail2ban"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def update_server_settings(
|
||||
request: Request,
|
||||
@@ -88,6 +99,12 @@ async def update_server_settings(
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Flush and re-open fail2ban log files",
|
||||
response_model=FlushLogsResponse,
|
||||
responses={
|
||||
200: {"description": "Logs flushed successfully", "model": FlushLogsResponse},
|
||||
400: {"description": "Command rejected by fail2ban"},
|
||||
401: {"description": "Session missing, expired, or invalid"},
|
||||
502: {"description": "fail2ban unreachable"},
|
||||
},
|
||||
)
|
||||
async def flush_logs(
|
||||
request: Request,
|
||||
|
||||
@@ -23,9 +23,12 @@ router = APIRouter(prefix="/api/v1/setup", tags=["setup"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
"/",
|
||||
response_model=SetupStatusResponse,
|
||||
summary="Check whether setup has been completed",
|
||||
responses={
|
||||
200: {"description": "Setup status returned", "model": SetupStatusResponse},
|
||||
},
|
||||
)
|
||||
async def get_setup_status(app: AppDep) -> SetupStatusResponse:
|
||||
"""Return whether the initial setup wizard has been completed.
|
||||
@@ -43,6 +46,11 @@ async def get_setup_status(app: AppDep) -> SetupStatusResponse:
|
||||
response_model=SetupResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Run the initial setup wizard",
|
||||
responses={
|
||||
201: {"description": "Setup completed successfully", "model": SetupResponse},
|
||||
400: {"description": "Validation error in request body"},
|
||||
409: {"description": "Setup already completed"},
|
||||
},
|
||||
)
|
||||
async def post_setup(
|
||||
app: AppDep,
|
||||
@@ -88,6 +96,9 @@ async def post_setup(
|
||||
"/timezone",
|
||||
response_model=SetupTimezoneResponse,
|
||||
summary="Return the configured IANA timezone",
|
||||
responses={
|
||||
200: {"description": "Timezone returned", "model": SetupTimezoneResponse},
|
||||
},
|
||||
)
|
||||
async def get_timezone(settings: SettingsDep) -> SetupTimezoneResponse:
|
||||
"""Return the IANA timezone configured during the initial setup wizard.
|
||||
|
||||
Reference in New Issue
Block a user